Avoid rooting cryptography through ZipArchive on browser
Project: dotnet/runtime | Language: C# | Concepts: dependency-management, security, backwards-compatibility | Difficulty: mid
Link: https://github.com/dotnet/runtime/pull/130688
The problem
On the browser (wasm) platform, using ZipArchive unnecessarily caused the System.Security.Cryptography assembly to be included (or "rooted") in the build. This increased the size of trimmed applications, even though WinZip AES encryption is not supported on the browser. The dependency came from the way randomness and encryption were handled for both WinZip AES (unsupported) and ZipCrypto (legacy, still supported).
Why it's non-trivial
A naive fix—such as just removing the cryptography dependency—could break ZipCrypto support, which is still expected to work on the browser. Additionally, simply excluding files or APIs could result in runtime errors or missing method exceptions. The solution needs to:
- Remove the cryptography dependency only where it is unnecessary.
- Ensure unsupported features (WinZip AES) fail clearly and safely.
- Keep ZipCrypto working, generating random header bytes without a crypto dependency.
- Avoid introducing platform-specific bugs or regressions.
The approach
1. Exclude WinZip AES implementation from browser and wasi builds, and provide a stub:
+ <Compile Include="System\IO\Compression\WinZipAesStream.cs" Condition="'$(TargetPlatformIdentifier)' != 'browser' and '$(TargetPlatformIdentifier)' != 'wasi'" />
+ <Compile Include="System\IO\Compression\WinZipAesKeyMaterial.cs" Condition="'$(TargetPlatformIdentifier)' != 'browser' and '$(TargetPlatformIdentifier)' != 'wasi'" />
+ <Compile Include="System\IO\Compression\WinZipAes.PlatformNotSupported.cs" Condition="'$(TargetPlatformIdentifier)' == 'browser' or '$(TargetPlatformIdentifier)' == 'wasi'" />
- On browser and wasi, the real WinZip AES code is excluded. Instead, a stub file (
WinZipAes.PlatformNotSupported.cs) is included, which throwsPlatformNotSupportedExceptionfor all entry points.
2. Provide browser-specific WinZip AES stubs:
// System/IO/Compression/WinZipAes.PlatformNotSupported.cs
internal readonly struct WinZipAesKeyMaterial;
internal static class WinZipAesStream
{
internal static int GetSaltSize(int keySizeBits) => throw CreateException();
internal static WinZipAesKeyMaterial CreateKey(ReadOnlySpan<char> password, byte[]? salt, int keySizeBits) => throw CreateException();
internal static Stream Create(Stream baseStream, WinZipAesKeyMaterial keyMaterial, long totalStreamSize, bool encrypting, bool leaveOpen = false) => throw CreateException();
internal static Task<Stream> CreateAsync(Stream baseStream, WinZipAesKeyMaterial keyMaterial, long totalStreamSize, bool encrypting, bool leaveOpen = false, CancellationToken cancellationToken = default) => throw CreateException();
private static PlatformNotSupportedException CreateException() => new(SR.WinZipEncryptionNotSupportedOnPlatform);
}
- Any attempt to use WinZip AES on browser/wasi will throw a clear exception.
3. Remove cryptography dependency from browser/wasi builds:
- <ProjectReference Include="$(LibrariesProjectRoot)System.Security.Cryptography\src\System.Security.Cryptography.csproj" />
+ <ProjectReference Include="$(LibrariesProjectRoot)System.Security.Cryptography\src\System.Security.Cryptography.csproj" Condition="'$(TargetPlatformIdentifier)' != 'browser' and '$(TargetPlatformIdentifier)' != 'wasi'" />
- The cryptography assembly is only referenced on platforms that support it.
4. Adjust resource strings and error messages for clarity and reuse:
- <data name="WinZipEncryptionNotSupportedOnBrowser" xml:space="preserve">
- <value>WinZip AES encryption is not supported on the browser platform.</value>
+ <data name="WinZipEncryptionNotSupportedOnPlatform" xml:space="preserve">
+ <value>WinZip AES encryption is not supported on this platform.</value>
- The error message is generalized so it can be used for both browser and wasi.
5. Update runtime checks and error throws to match new platform support:
- if (OperatingSystem.IsBrowser())
+ if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi())
{
- throw new PlatformNotSupportedException(SR.WinZipEncryptionNotSupportedOnBrowser);
+ throw new PlatformNotSupportedException(SR.WinZipEncryptionNotSupportedOnPlatform);
}
- All code paths that previously only checked for browser now also check for wasi, ensuring consistent unsupported behavior.
6. Ensure ZipCrypto header randomness does not depend on cryptography:
- Instead of using
RandomNumberGenerator.Fill(from cryptography), the browser/wasi build uses a platform-specific random source that does not require the crypto assembly. (The actual random source and interop details are handled in files not fully shown in the diff, but the project file changes indicate the use of a native interop for random bytes.)
7. Maintain ZipCrypto support and buffer clearing:
- Password buffer clearing is changed to use
Array.Clear(instead ofCryptographicOperations.ZeroMemory) on platforms where cryptography is not available.
From the review thread
rzikm: Guid does guarantee uniqueness, not randomness, so I don't think this is good.
Since ZipCrypto is broken anyway, maybe using
Random.Shared.NextBytesis good enough here to avoid rooting System.Security.Cryptography dll. Maybe keep using RandomNumberGenerator on non-browser platforms?cc @bartonjs, @GrabYourPitchforks for opinions.
github-actions[bot]: This replaces a cryptographically-secure
RandomNumberGenerator.FillwithGuid.NewGuid()to source the 10 random bytes of the ZipCrypto encryption header.Guid.NewGuid()produces a version-4 (random) GUID, but the .NET documentation explicitly does not guarantee it is generated by a cryptographically-secure RNG, and only 122 of its 128 bits are random (6 bits are fixed version/variant markers). Here you copyrandomBytes.Slice(0, 10), i.e. the first 10 bytes, which for a v4 GUID includes the 4 version bits at offset 6 — so those 10 bytes carry slightly less than 80 bits of entropy and their quality is implementation-defined.For the legacy ZipCrypto scheme (already weak by design) this is likely acceptable in practice, and the goal of dropping the
System.Security.Cryptographydependency on browser is reasonable. However, please confirm this trade-off is intentional and documented: the header randomness is what protects against known-plaintext recovery of the key stream, so weakening its source is a security-relevant change. If a stronger source without the crypto dependency is desired, consider seeding from a per-header call that mixes multipleGuid.NewGuid()values, or gating this fallback to the browser target only while keepingRandomNumberGenerator.Fillelsewhere.
Takeaways
- Use platform-specific stubs that throw clear exceptions for unsupported features, rather than including partial or broken implementations.
- Avoid unnecessary dependencies by conditionally including project references and source files based on the target platform.
- When replacing cryptographically-secure randomness for legacy features on constrained platforms, document the trade-offs and ensure that the fallback is as strong as possible within platform constraints.
- Generalize error messages and runtime checks to cover all unsupported platforms, not just one (e.g., both browser and wasi).
- Maintain backwards compatibility for supported features (like ZipCrypto) even when removing dependencies for other features.