Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ public StreamPackageFeature(Stream stream, PackageOpenMode openMode, bool isOwne
}
catch when (isOwned)
{
// Ensure that the stream if created is disposed before leaving the constructor so we don't hold onto it
if (_stream is not null && OpenXmlPackage.IsEncryptedOfficeFile(_stream))
{
_stream.Dispose();
throw new OpenXmlPackageException(ExceptionMessages.EncryptedPackageNotSupported);
}

_stream?.Dispose();
throw;
}
Expand Down
92 changes: 92 additions & 0 deletions src/DocumentFormat.OpenXml.Framework/Packaging/OpenXmlPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -612,5 +612,97 @@ public void Save()

/// <inheritdoc/>
public override IFeatureCollection Features => _features ??= new PackageFeatureCollection(this);

/// <summary>
/// Determines whether the provided stream represents an encrypted Office Open XML file.
/// </summary>
/// <param name="inputStream">The <see cref="Stream"/> to check. The stream must be seekable and not null.</param>
/// <returns>
/// <c>true</c> if the stream is an encrypted Office file (either OLE Compound File or contains an encrypted package part); otherwise, <c>false</c>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="inputStream"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="inputStream"/> is not seekable.</exception>
/// <remarks>
/// This method checks for the OLE Compound File signature at the start of the stream, which is used for encrypted Office files.
/// If not found, it attempts to open the stream as an OPC package and checks for the presence of an encrypted package part.
/// The stream position is restored after the check.
/// </remarks>
public static bool IsEncryptedOfficeFile(Stream inputStream)
{
if (inputStream is null)
{
throw new ArgumentNullException(nameof(inputStream));
}

if (!inputStream.CanSeek)
{
throw new ArgumentException("Stream must be seekable.");
}

long originalPosition = inputStream.Position;

try
{
byte[] header = new byte[8];
inputStream.Seek(0, SeekOrigin.Begin);
int read = inputStream.Read(header, 0, header.Length);
inputStream.Seek(originalPosition, SeekOrigin.Begin);

// OLE Compound File signature for encrypted Office files
if (read == 8 && header.SequenceEqual(new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }))
{
return true;
}

// If not OLE, try to open as package and check for encrypted part
try
{
using (var package = System.IO.Packaging.Package.Open(inputStream, FileMode.Open, FileAccess.Read))
{
foreach (var part in package.GetParts())
{
if (part.ContentType.Equals("application/vnd.openxmlformats-officedocument.encrypted-package", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
}
catch
{
return false;
}

return false;
}
finally
{
inputStream.Seek(originalPosition, SeekOrigin.Begin);
}
}

/// <summary>
/// Determines whether the file at the specified path is an encrypted Office Open XML file.
/// </summary>
/// <param name="filePath">The path to the file to check. Must not be null.</param>
/// <returns>
/// <c>true</c> if the file is an encrypted Office file (either OLE Compound File or contains an encrypted package part); otherwise, <c>false</c>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="filePath"/> is null.</exception>
/// <remarks>
/// This method opens the file at the specified path and checks its contents using <see cref="IsEncryptedOfficeFile(Stream)"/>.
/// </remarks>
public static bool IsEncryptedOfficeFile(string filePath)
{
if (filePath is null)
{
throw new ArgumentNullException(nameof(filePath));
}

using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
return IsEncryptedOfficeFile(fileStream);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1010,3 +1010,5 @@ DocumentFormat.OpenXml.OpenXmlPartWriterSettings.OpenXmlPartWriterSettings() ->
DocumentFormat.OpenXml.OpenXmlPartWriter.OpenXmlPartWriter(DocumentFormat.OpenXml.Packaging.OpenXmlPart! openXmlPart, DocumentFormat.OpenXml.OpenXmlPartWriterSettings! settings) -> void
DocumentFormat.OpenXml.OpenXmlPartWriter.OpenXmlPartWriter(System.IO.Stream! partStream, DocumentFormat.OpenXml.OpenXmlPartWriterSettings! settings) -> void
virtual DocumentFormat.OpenXml.OpenXmlCompositeElement.IsValidChild(DocumentFormat.OpenXml.OpenXmlElement! element) -> bool
static DocumentFormat.OpenXml.Packaging.OpenXmlPackage.IsEncryptedOfficeFile(System.IO.Stream! inputStream) -> bool
static DocumentFormat.OpenXml.Packaging.OpenXmlPackage.IsEncryptedOfficeFile(string! filePath) -> bool

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -414,4 +414,7 @@
<data name="FirstOrDefaultMaxOne" xml:space="preserve">
<value>The enumerable contained more than a single element when only zero or one are allowed.</value>
</data>
<data name="EncryptedPackageNotSupported" xml:space="preserve">
<value>Encrypted packages are not supported.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using System.Linq;
using Xunit;

using static DocumentFormat.OpenXml.Tests.TestAssets;

namespace DocumentFormat.OpenXml.Features.Tests;

public class StreamPackageFeatureTests
Expand Down Expand Up @@ -431,6 +433,17 @@ protected override void Dispose(bool disposing)
}
}

[Fact]
public void ThrowsForEncryptedOfficeFile()
{
using (Stream stream = GetStream(TestFiles.Encrypted_pptx, false))
{
// Act & Assert
var ex = Assert.Throws<OpenXmlPackageException>(() => new StreamPackageFeature(stream, PackageOpenMode.Read, isOwned: true));
Assert.Equal(ExceptionMessages.EncryptedPackageNotSupported, ex.Message);
}
}

private static readonly PartInfo Part1 = new(new("/part1", UriKind.Relative), "type1/content");
private static readonly PartInfo Part2 = new(new("/part2", UriKind.Relative), "type2/content");
private static readonly PartInfo PartRels = new(new("/_rels/.rels", UriKind.Relative), "application/vnd.openxmlformats-package.relationships+xml");
Expand Down
56 changes: 56 additions & 0 deletions test/DocumentFormat.OpenXml.Packaging.Tests/OpenXmlPackageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,5 +342,61 @@ public void SucceedWithMissingCalcChainPart()

Assert.NotNull(spd);
}

[Fact]
public void IsEncryptedOfficeFile_ReturnsTrue_ForEncryptedFile()
{
using (Stream stream = GetStream(TestFiles.Encrypted_pptx, false))
{
Assert.True(OpenXmlPackage.IsEncryptedOfficeFile(stream));
}
}

[Fact]
public void IsEncryptedOfficeFile_ReturnsFalse_ForUnencryptedFile()
{
using (Stream stream = GetStream(TestFiles.Presentation, false))
{
Assert.False(OpenXmlPackage.IsEncryptedOfficeFile(stream));
}
}

[Fact]
public void IsEncryptedOfficeFile_ThrowsArgumentNullException_ForNullStream()
{
Assert.Throws<ArgumentNullException>(() => OpenXmlPackage.IsEncryptedOfficeFile((Stream)null!));
}

[Fact]
public void IsEncryptedOfficeFile_ThrowsArgumentException_ForUnseekableStream()
{
var unseekable = new UnseekableStream();
Assert.Throws<ArgumentException>(() => OpenXmlPackage.IsEncryptedOfficeFile(unseekable));
}

private class UnseekableStream : MemoryStream
{
public override bool CanSeek => false;
}

[Fact]
public void IsEncryptedOfficeFile_ReturnsTrue_ForEncryptedFilePath()
{
string filePath = GetTestFilePath(TestFiles.Encrypted_pptx);
Assert.True(OpenXmlPackage.IsEncryptedOfficeFile(filePath));

// Clean up the test file path
File.Delete(filePath);
}

[Fact]
public void IsEncryptedOfficeFile_ReturnsFalse_ForUnencryptedFile_FromString()
{
string filePath = GetTestFilePath(TestFiles.Presentation);
Assert.False(OpenXmlPackage.IsEncryptedOfficeFile(filePath));

// Clean up the test file path
File.Delete(filePath);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public static class Templates
public const string Of16_09_unknownelement_docx = "TestFiles.Of16-09-UnknownElement.docx";

public const string Of16_10_symex_docx = "TestFiles.Of16-10-SymEx.docx";

public const string Encrypted_pptx = "TestFiles.encrypted_pptx.pptx";
}
}
}
20 changes: 20 additions & 0 deletions test/DocumentFormat.OpenXml.Tests.Assets/TestAssets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ public static Stream GetStream(string name, bool isEditable)
return isEditable ? stream.AsMemoryStream() : stream;
}

/// <summary>
/// Extracts an embedded test resource to a temporary file and returns its file path.
/// </summary>
/// <param name="resourceName">The name of the embedded resource to extract.</param>
/// <returns>The full path to the temporary file containing the resource data.</returns>
/// <remarks>
/// The caller is responsible for deleting the temporary file after use.
/// </remarks>
public static string GetTestFilePath(string resourceName)
{
string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + Path.GetExtension(resourceName));

using (Stream stream = GetStream(resourceName, false))
using (FileStream fileStream = File.Create(tempPath))
{
stream.CopyTo(fileStream);
return tempPath;
}
}

private static Stream AsMemoryStream(this Stream stream)
{
if (stream is MemoryStream ms)
Expand Down
Binary file not shown.
Loading