Skip to content
Draft
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
11 changes: 10 additions & 1 deletion src/libraries/System.IO.Compression/ref/System.IO.Compression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ internal ZipArchiveEntry() { }
public void Delete() { }
public System.IO.Stream Open() { throw null; }
public System.IO.Stream Open(System.IO.FileAccess access) { 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.IO.Stream Open(System.IO.FileAccess access, System.ReadOnlySpan<char> password) { throw null; }
public System.IO.Stream Open(System.ReadOnlySpan<char> password) { throw null; }
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; }
public System.Threading.Tasks.Task<System.IO.Stream> OpenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public override string ToString() { throw null; }
Expand All @@ -204,6 +204,15 @@ public enum ZipEncryptionMethod
Aes192 = 3,
Aes256 = 4,
}
public sealed partial class ZipStreamReader : System.IAsyncDisposable, System.IDisposable
{
public ZipStreamReader(System.IO.Stream stream, bool leaveOpen = false) { }
public ZipStreamReader(System.IO.Stream stream, System.Text.Encoding? entryNameEncoding, bool leaveOpen = false) { }
public void Dispose() { }
Comment thread
alinpahontu2912 marked this conversation as resolved.
public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
public System.IO.Compression.ZipArchiveEntry? GetNextEntry(bool copyData = false) { throw null; }
public System.Threading.Tasks.ValueTask<System.IO.Compression.ZipArchiveEntry?> GetNextEntryAsync(bool copyData = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
Comment thread
alinpahontu2912 marked this conversation as resolved.
Comment thread
alinpahontu2912 marked this conversation as resolved.
Comment on lines +207 to +215
Comment on lines +207 to +215
public sealed partial class ZLibCompressionOptions
{
public static int DefaultWindowLog2 { get { throw null; } }
Expand Down
14 changes: 13 additions & 1 deletion src/libraries/System.IO.Compression/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -408,12 +408,24 @@
<data name="IO_NotSupported_UnwritableStream" xml:space="preserve">
<value>Stream does not support writing.</value>
</data>
<data name="ZipStreamStoredDataDescriptorNotSupported" xml:space="preserve">
<value>Stored compression entries with data descriptors cannot be read in forward-only mode because the compressed size is unknown.</value>
</data>
<data name="CrcMismatch" xml:space="preserve">
<value>The CRC32 checksum of the extracted data does not match the expected value from the archive.</value>
</data>
<data name="UnexpectedStreamLength" xml:space="preserve">
<value>The decompressed data length does not match the expected value from the archive.</value>
</data>
<data name="ZipStreamEncryptedDataDescriptorNotSupported" xml:space="preserve">
<value>Encrypted entries with data descriptors cannot be read in forward-only mode because the compressed size is unknown.</value>
</data>
<data name="ZipStreamEntryAlreadyConsumed" xml:space="preserve">
<value>The forward-only data stream for this entry has already been opened. A ZipStreamReader entry can only be read once.</value>
</data>
<data name="ZipStreamInvalidLocalFileHeader" xml:space="preserve">
<value>The ZIP archive contains an invalid local file header signature.</value>
</data>
<data name="EncryptionNotSpecified" xml:space="preserve">
<value>Encryption type is not specified.</value>
</data>
Expand All @@ -429,4 +441,4 @@
<data name="UnsupportedEncryptionMethod" xml:space="preserve">
<value>The entry's encryption method is not supported.</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent)</TargetFrameworks>
Expand Down Expand Up @@ -64,6 +64,7 @@
<Compile Include="System\IO\Compression\ZLibStream.cs" />
<Compile Include="System\IO\Compression\ZipCompressionMethod.cs" />
<Compile Include="$(CommonPath)System\Obsoletions.cs" Link="Common\System\Obsoletions.cs" />
<Compile Include="System\IO\Compression\ZipStreamReader.cs" />
<Compile Include="System\IO\Compression\ZipEncryptionMethod.cs" />
<Compile Include="System\IO\Compression\ZipCryptoStream.cs" />
<Compile Include="System\IO\Compression\WinZipAesStream.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ internal static WinZipAesKeyMaterial CreateKey(ReadOnlySpan<char> password, byte
/// <summary>
/// Creates a WinZipAesStream synchronously. Reads and validates the header for decryption.
/// </summary>
internal static WinZipAesStream Create(Stream baseStream, WinZipAesKeyMaterial keyMaterial, long totalStreamSize, bool encrypting, bool leaveOpen = false)
internal static WinZipAesStream Create(Stream baseStream, WinZipAesKeyMaterial keyMaterial, long totalStreamSize, bool encrypting, bool leaveOpen = false, bool saltAlreadyRead = false)
{
if (OperatingSystem.IsBrowser())
{
Expand All @@ -75,7 +75,7 @@ internal static WinZipAesStream Create(Stream baseStream, WinZipAesKeyMaterial k

if (!encrypting)
{
ReadAndValidateHeaderCore(isAsync: false, baseStream, keyMaterial, CancellationToken.None).GetAwaiter().GetResult();
ReadAndValidateHeaderCore(isAsync: false, baseStream, keyMaterial, saltAlreadyRead, CancellationToken.None).GetAwaiter().GetResult();
}

return new WinZipAesStream(baseStream, keyMaterial, totalStreamSize, encrypting, leaveOpen);
Expand All @@ -84,7 +84,7 @@ internal static WinZipAesStream Create(Stream baseStream, WinZipAesKeyMaterial k
/// <summary>
/// Creates a WinZipAesStream asynchronously. Reads and validates the header for decryption.
/// </summary>
internal static async Task<WinZipAesStream> CreateAsync(Stream baseStream, WinZipAesKeyMaterial keyMaterial, long totalStreamSize, bool encrypting, bool leaveOpen = false, CancellationToken cancellationToken = default)
internal static async Task<WinZipAesStream> CreateAsync(Stream baseStream, WinZipAesKeyMaterial keyMaterial, long totalStreamSize, bool encrypting, bool leaveOpen = false, bool saltAlreadyRead = false, CancellationToken cancellationToken = default)
{
if (OperatingSystem.IsBrowser())
{
Expand All @@ -95,57 +95,57 @@ internal static async Task<WinZipAesStream> CreateAsync(Stream baseStream, WinZi

if (!encrypting)
{
await ReadAndValidateHeaderCore(isAsync: true, baseStream, keyMaterial, cancellationToken).ConfigureAwait(false);
await ReadAndValidateHeaderCore(isAsync: true, baseStream, keyMaterial, saltAlreadyRead, cancellationToken).ConfigureAwait(false);
}

return new WinZipAesStream(baseStream, keyMaterial, totalStreamSize, encrypting, leaveOpen);
}

/// <summary>
/// Reads and validates the WinZip AES header (salt + password verifier) from the stream.
/// When <paramref name="saltAlreadyRead"/> is <see langword="true"/>, the salt has already been
/// consumed from the stream by the caller (e.g. the forward-only reader, which must read it to
/// derive the key before this stream is created), so only the password verifier is read here.
/// </summary>
private static async Task ReadAndValidateHeaderCore(bool isAsync, Stream baseStream, WinZipAesKeyMaterial keyMaterial, CancellationToken cancellationToken)
private static async Task ReadAndValidateHeaderCore(bool isAsync, Stream baseStream, WinZipAesKeyMaterial keyMaterial, bool saltAlreadyRead, CancellationToken cancellationToken)
{
if (OperatingSystem.IsBrowser())
{
throw new PlatformNotSupportedException(SR.WinZipEncryptionNotSupportedOnBrowser);
}
int saltSize = keyMaterial.SaltSize;

// Read salt from stream
byte[] fileSalt = new byte[saltSize];
if (isAsync)
{
await baseStream.ReadExactlyAsync(fileSalt, cancellationToken).ConfigureAwait(false);
}
else
if (!saltAlreadyRead)
{
baseStream.ReadExactly(fileSalt);
// Read and verify the salt. In WinZip AES the salt is stored in the archive header and
// is not secret; FixedTimeEquals is used here for consistency.
byte[] fileSalt = new byte[keyMaterial.SaltSize];
await ReadExactly(fileSalt).ConfigureAwait(false);

if (!CryptographicOperations.FixedTimeEquals(fileSalt, keyMaterial.Salt))
{
throw new InvalidDataException(SR.LocalFileHeaderCorrupt);
}
}

// Read the 2-byte password verifier from stream
// Read and compare the 2-byte password verifier. This is a weak check (only 2 bytes) used
// to fail fast on an obviously wrong password; it is not a security guarantee.
byte[] verifier = new byte[2];
if (isAsync)
{
await baseStream.ReadExactlyAsync(verifier, cancellationToken).ConfigureAwait(false);
}
else
{
baseStream.ReadExactly(verifier);
}
await ReadExactly(verifier).ConfigureAwait(false);

// Verify the salt matches. In WinZip AES, the salt is stored in the archive
// header and is not secret; FixedTimeEquals is used here for consistency.
if (!CryptographicOperations.FixedTimeEquals(fileSalt, keyMaterial.Salt))
if (!CryptographicOperations.FixedTimeEquals(verifier, keyMaterial.PasswordVerifier))
{
throw new InvalidDataException(SR.LocalFileHeaderCorrupt);
throw new InvalidDataException(SR.InvalidPassword);
}

// Compare the 2-byte password verifier. This is a weak check (only 2 bytes) used to
// fail fast on an obviously wrong password; it is not a security guarantee.
if (!CryptographicOperations.FixedTimeEquals(verifier, keyMaterial.PasswordVerifier))
ValueTask ReadExactly(byte[] buffer)
{
throw new InvalidDataException(SR.InvalidPassword);
if (isAsync)
{
return baseStream.ReadExactlyAsync(buffer, cancellationToken);
}

baseStream.ReadExactly(buffer);
return ValueTask.CompletedTask;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public partial class ZipArchiveEntry
public Task<Stream> OpenAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (_isForwardReadEntry)
{
return OpenForwardReadModeAsync(default, cancellationToken);
}
Comment on lines 22 to +29

ThrowIfInvalidArchive();
return OpenAsyncCore(InferAccessFromMode(), default, cancellationToken);
}
Expand All @@ -48,6 +54,17 @@ public Task<Stream> OpenAsync(CancellationToken cancellationToken = default)
public Task<Stream> OpenAsync(FileAccess access, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (_isForwardReadEntry)
{
if (access != FileAccess.Read)
{
throw new InvalidOperationException(SR.CannotBeWrittenInReadMode);
}

return OpenForwardReadModeAsync(default, cancellationToken);
}

ThrowIfInvalidArchive();
ValidateAccessForMode(access);
return OpenAsyncCore(access, default, cancellationToken);
Expand Down Expand Up @@ -75,6 +92,17 @@ public Task<Stream> OpenAsync(FileAccess access, CancellationToken cancellationT
public Task<Stream> OpenAsync(FileAccess access, ReadOnlySpan<char> password, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (_isForwardReadEntry)
{
if (access != FileAccess.Read)
{
throw new InvalidOperationException(SR.CannotBeWrittenInReadMode);
}

return OpenForwardReadModeAsync(password, cancellationToken);
}

ThrowIfInvalidArchive();
ValidateAccessForMode(access);

Expand Down Expand Up @@ -103,6 +131,12 @@ public Task<Stream> OpenAsync(FileAccess access, ReadOnlySpan<char> password, Ca
public Task<Stream> OpenAsync(ReadOnlySpan<char> password, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (_isForwardReadEntry)
{
return OpenForwardReadModeAsync(password, cancellationToken);
}

ThrowIfInvalidArchive();

if (IsEncrypted && password.IsEmpty)
Expand Down Expand Up @@ -145,6 +179,92 @@ private Task<Stream> OpenAsyncCore(FileAccess access, ReadOnlySpan<char> passwor
}
}

// Async counterpart of OpenForwardReadMode. Non-encrypted entries complete synchronously; encrypted
// entries read the encryption header (AES salt / ZipCrypto header) asynchronously from the raw stream.
private Task<Stream> OpenForwardReadModeAsync(ReadOnlySpan<char> password, CancellationToken cancellationToken)
{
if (_forwardDataStream is null)
{
return Task.FromResult<Stream>(Stream.Null);
}

if (_forwardStreamOpened)
{
throw new InvalidOperationException(SR.ZipStreamEntryAlreadyConsumed);
}

if (!IsEncrypted)
{
_forwardStreamOpened = true;
return Task.FromResult<Stream>(_forwardDataStream);
}

// Validate before marking the single-use stream consumed so that an attempt which reads no
// bytes (missing password or unsupported scheme) can be retried with a valid password.
if (Encryption == ZipEncryptionMethod.Unknown)
{
throw new NotSupportedException(SR.UnsupportedEncryptionMethod);
}

if (password.IsEmpty)
{
throw new InvalidDataException(SR.PasswordRequired);
}
Comment on lines +209 to +212

_forwardStreamOpened = true;

Stream rawStream = _forwardDataStream;

if (IsAesEncrypted)
{
if (OperatingSystem.IsBrowser())
{
throw new PlatformNotSupportedException(SR.WinZipEncryptionNotSupportedOnBrowser);
}

int keySizeBits = GetAesKeySizeBits(Encryption);
// The salt must be read from the stream before the key can be derived, so the password
// has to cross the async boundary; copy it out of the span into a local array.
return OpenForwardAesAsyncCore(rawStream, password.ToArray(), keySizeBits, cancellationToken);
}

if (IsZipCryptoEncrypted)
{
byte expectedCheckByte = CalculateZipCryptoCheckByte();
ZipCryptoKeys keyMaterial = ZipCryptoStream.CreateKey(password);
return OpenForwardZipCryptoAsyncCore(rawStream, keyMaterial, expectedCheckByte, cancellationToken);
}

throw new NotSupportedException(SR.UnsupportedEncryptionMethod);

async Task<Stream> OpenForwardAesAsyncCore(Stream stream, char[] pwd, int keySizeBits, CancellationToken ct)
{
int saltSize = WinZipAesStream.GetSaltSize(keySizeBits);
byte[] salt = new byte[saltSize];
await stream.ReadExactlyAsync(salt, ct).ConfigureAwait(false);

WinZipAesKeyMaterial keyMaterial = WinZipAesStream.CreateKey(pwd, salt, keySizeBits);
Stream decrypted = await WinZipAesStream.CreateAsync(
baseStream: stream,
keyMaterial: keyMaterial,
totalStreamSize: _compressedSize,
encrypting: false,
// The forward reader owns the raw stream and drains/disposes it when it advances,
// so the decryption stream must not dispose it on the caller's behalf.
leaveOpen: true,
saltAlreadyRead: true,
cancellationToken: ct).ConfigureAwait(false);

return BuildDecompressionPipeline(decrypted);
}

async Task<Stream> OpenForwardZipCryptoAsyncCore(Stream stream, ZipCryptoKeys keyMaterial, byte checkByte, CancellationToken ct)
{
Stream decrypted = await ZipCryptoStream.CreateAsync(stream, keyMaterial, checkByte, encrypting: false, ct, leaveOpen: true).ConfigureAwait(false);
return BuildDecompressionPipeline(decrypted);
}
}

internal async Task<long> GetOffsetOfCompressedDataAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Expand Down
Loading
Loading