Skip to content

Add Zip Archives password support#122093

Open
alinpahontu2912 wants to merge 94 commits into
dotnet:mainfrom
alinpahontu2912:zip_password
Open

Add Zip Archives password support#122093
alinpahontu2912 wants to merge 94 commits into
dotnet:mainfrom
alinpahontu2912:zip_password

Conversation

@alinpahontu2912

@alinpahontu2912 alinpahontu2912 commented Dec 2, 2025

Copy link
Copy Markdown
Member

Fixes #1545

Big Milestones and status:

  • Read Encrypted Entries (ZipCrypto, WinZip AES)
  • Create Encrypted Entries (ZipCrypto, Winzip AES)
  • Update Mode
  • Add runtime assets for checking compat with WinRar/7zip/WinZip/etc
  • Security check for Cryptography methods
  • Final API design

@rzikm rzikm self-requested a review December 11, 2025 12:22
Copilot AI review requested due to automatic review settings April 30, 2026 10:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings May 7, 2026 10:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated 4 comments.

Comment thread src/libraries/System.IO.Compression/src/System/IO/Compression/WinZipAesStream.cs Outdated
Comment thread src/libraries/System.IO.Compression/src/System.IO.Compression.csproj Outdated
Copilot AI review requested due to automatic review settings May 7, 2026 13:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 42 changed files in this pull request and generated 7 comments.

Comment thread src/libraries/System.IO.Compression/src/System.IO.Compression.csproj Outdated
Comment thread src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Comment thread src/libraries/System.IO.Compression/src/System/IO/Compression/WinZipAesStream.cs Outdated
}
else
{
// For decryption: HMAC first (on ciphertext), then XOR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this data protocol correctly uses Encrypt-then-MAC (EtM), best practice (and internal security requirements) dictates that you should verify the MAC entirely before doing any decryption. As far as I can tell, this implementation allows decrypted data to be processed by a caller before the MAC is verified.

While that is the more performant approach, it will require a security process exception (ideally before the code is committed). Alternatively, change to verifying the MAC beforehand.

Maybe that discussion was already and and I'm not privy to it, but it's a flaw in implementation that I see here. cc: @blowdart @GrabYourPitchforks

Comment thread src/libraries/System.IO.Compression/src/System/IO/Compression/WinZipAesStream.cs Outdated
Comment thread src/libraries/System.IO.Compression/src/System/IO/Compression/WinZipAesStream.cs Outdated
Copilot AI review requested due to automatic review settings May 12, 2026 08:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 42 changed files in this pull request and generated 8 comments.

Comment thread src/libraries/System.IO.Compression/ref/System.IO.Compression.cs
Comment thread src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs
Comment on lines +17 to +18
private const string TestPassword = "test-password";
private const ushort PasswordVerifier = 0x1234;
Comment on lines +19 to +20
private const string TestPassword = "test-password";
private const ushort PasswordVerifier = 0x1234;
Comment on lines +37 to +38
private const string TestPassword = "test-password";

Comment on lines +972 to +975
public async Task DecryptEntries_SamePassword_7Zip(bool async)
{
string password = "S3cur3P@ssw0rd";
using Stream archiveStream = await StreamHelpers.CreateTempCopyStream(passwordProtected("PasswordProtected_7ZIP_SamePassword.zip"));
Copilot AI review requested due to automatic review settings May 12, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 42 changed files in this pull request and generated 7 comments.

Comment on lines +178 to +180
public System.Threading.Tasks.Task<System.IO.Stream> OpenAsync(System.IO.FileAccess access, System.ReadOnlySpan<char> password, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Threading.Tasks.Task<System.IO.Stream> OpenAsync(System.IO.FileAccess access, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Threading.Tasks.Task<System.IO.Stream> OpenAsync(System.ReadOnlySpan<char> password, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Comment on lines +778 to +786
var crcStream = GetDataCompressor(encryptionStream, leaveBackingStreamOpen: true, onClose: null, streamForPosition: _archive.ArchiveStream);
await using (crcStream.ConfigureAwait(false))
{
_storedUncompressedData.Seek(0, SeekOrigin.Begin);
await _storedUncompressedData.CopyToAsync(crcStream, cancellationToken).ConfigureAwait(false);
}

// Restore CompressionMethod - AesCompressionMethodValue is used directly when writing headers
CompressionMethod = savedMethod;
Comment on lines +834 to +845
BitFlagValues savedFlags = _generalPurposeBitFlag;
ZipEncryptionMethod savedEncryption = Encryption;
ZipCompressionMethod savedCompressionMethod = CompressionMethod;

// For AES entries: set CompressionMethod to Aes so header writes method 99,
// but clear _encryptionMethod so WriteLocalFileHeaderAsync doesn't create a new
// AES extra field (the original one in _lhUnknownExtraFields will be used).
if (savedEncryption is ZipEncryptionMethod.Aes128 or ZipEncryptionMethod.Aes192 or ZipEncryptionMethod.Aes256)
{
CompressionMethod = (ZipCompressionMethod)WinZipAesMethod;
Encryption = ZipEncryptionMethod.None;
}
Comment on lines +131 to +135
if (overwrite && File.Exists(destinationFileName))
{
tempPath = Path.GetTempFileName();
extractPath = tempPath;
}
Comment on lines +137 to +146
// When overwriting, extract to a temporary file first to avoid corrupting the destination file
// if an exception occurs during extraction (e.g., password-protected archive, corrupted data).
string extractPath = destinationFileName;
string? tempPath = null;

if (overwrite && File.Exists(destinationFileName))
{
tempPath = Path.GetTempFileName();
extractPath = tempPath;
}
ThrowIfInvalidArchive();
ValidateAccessForMode(access);

if (IsEncrypted && password.IsEmpty)
Comment on lines +242 to +247

ZipArchiveEntry lastEntry = _entries[_entries.Count - 1];
if (lastEntry.IsEncrypted)
{
await lastEntry.ReadEncryptionSaltIfNeededAsync(cancellationToken).ConfigureAwait(false);
}

@GrabYourPitchforks GrabYourPitchforks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding the submission until necessary docs are submitted.

Comment on lines +132 to +145
private byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
try
{
bytes.CopyTo(buffer);
return buffer;
}
catch
{
ArrayPool<byte>.Shared.Return(buffer);
throw;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
try
{
bytes.CopyTo(buffer);
return buffer;
}
catch
{
ArrayPool<byte>.Shared.Return(buffer);
throw;
}
}
private static byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
bytes.CopyTo(buffer);
return buffer;
}

CopyTo won't throw here, and if it did the cleanup doesn't matter since the fuzzer would stop anyway.

Comment on lines +92 to +105
private byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
try
{
bytes.CopyTo(buffer);
return buffer;
}
catch
{
ArrayPool<byte>.Shared.Return(buffer);
throw;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
try
{
bytes.CopyTo(buffer);
return buffer;
}
catch
{
ArrayPool<byte>.Shared.Return(buffer);
throw;
}
}
private static byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
bytes.CopyTo(buffer);
return buffer;
}

}

ZipArchiveEntry entry;
if (!password.IsEmpty && encryption != ZipEncryptionMethod.None)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!password.IsEmpty && encryption != ZipEncryptionMethod.None)
if (encryption != ZipEncryptionMethod.None)

Already checked above

Comment on lines +251 to +254
if (!password.IsEmpty)
ExtractToFile(source, fileDestinationPath, overwrite, password);
else
source.ExtractToFile(fileDestinationPath, overwrite: overwrite);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please use blocks with {} in such if/else cases.

/// The key material layout is: [salt][encryption key][HMAC key][password verifier (2 bytes)].
/// </summary>
[UnsupportedOSPlatform("browser")]
internal readonly struct WinZipAesKeyMaterial

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If using struct here is about perf, it may actually be worse since it's quite large and you're passing it around in arguments a bunch

Comment on lines +457 to +458
int inputOffset = 0;
int inputCount = buffer.Length;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of tracking the offsets and count, you can slice the buffer (same for the memory in async variant).
You get to drop the extra locals, and the loop condition is !IsEmpty.

Comment on lines +668 to +671
if (isAsync)
await WriteHeaderAsync(cancellationToken).ConfigureAwait(false);
else
WriteHeader();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: {} blocks (same in all other places)

Comment on lines +701 to +702
// Only flush if the base stream supports writing
if (_baseStream.CanWrite)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking this? Wouldn't it be the caller's error / the responsibility of the base stream to check?

We aren't doing the same in ZipCryptoStream for example

Comment on lines +992 to +998
private bool IsZipCryptoEncrypted => (_generalPurposeBitFlag & BitFlagValues.IsEncrypted) != 0 && (ushort)_headerCompressionMethod != WinZipAesMethod;

private bool UseAesEncryption()
{
return Encryption is ZipEncryptionMethod.Aes128 or ZipEncryptionMethod.Aes192 or ZipEncryptionMethod.Aes256;
}
private bool IsAesEncrypted => (ushort)_headerCompressionMethod == WinZipAesMethod;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Any reason between property vs method here?

Comment on lines +448 to +450
int offset = 0;

while (offset < buffer.Length)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here re: slicing instead of tracking the offset

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add password to ZipArchive

8 participants