diff --git a/src/libraries/System.IO.Compression/src/Resources/Strings.resx b/src/libraries/System.IO.Compression/src/Resources/Strings.resx index 8f70a3e2dea3d2..d2fb9df005cfba 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -426,6 +426,10 @@ WinZip AES encryption is not supported on the browser platform. + + Unable to generate cryptographically secure random bytes. + + The entry's encryption method is not supported. diff --git a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj index c71e8f1e4b181f..9af63969bfaef5 100644 --- a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj +++ b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj @@ -66,8 +66,12 @@ - - + + + + + + @@ -111,7 +115,7 @@ - + diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/Interop.GetCryptographicallySecureRandomBytes.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/Interop.GetCryptographicallySecureRandomBytes.cs new file mode 100644 index 00000000000000..bc93ee82bac327 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/Interop.GetCryptographicallySecureRandomBytes.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetCryptographicallySecureRandomBytes")] + internal static unsafe partial int GetCryptographicallySecureRandomBytes(byte* buffer, int length); + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/WinZipAes.PlatformNotSupported.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/WinZipAes.PlatformNotSupported.cs new file mode 100644 index 00000000000000..75c9de825295e3 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/WinZipAes.PlatformNotSupported.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Compression +{ + internal readonly struct WinZipAesKeyMaterial; + + internal static class WinZipAesStream + { + internal static int GetSaltSize(int keySizeBits) => throw CreateException(); + + internal static WinZipAesKeyMaterial CreateKey(ReadOnlySpan 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 CreateAsync(Stream baseStream, WinZipAesKeyMaterial keyMaterial, long totalStreamSize, bool encrypting, bool leaveOpen = false, CancellationToken cancellationToken = default) => throw CreateException(); + + private static PlatformNotSupportedException CreateException() => new(SR.WinZipEncryptionNotSupportedOnBrowser); + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs index 6a74dc6f308525..274c09cd07e9c2 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs @@ -728,7 +728,7 @@ private async Task WriteLocalFileHeaderAndDataIfNeededAsync(bool forceWrite, Can ushort verifierLow2Bytes = (ushort)ZipHelper.DateTimeToDosTime(_lastModified.DateTime); - ZipCryptoStream encryptionStream = ZipCryptoStream.Create( + Stream encryptionStream = ZipCryptoStream.Create( baseStream: _archive.ArchiveStream, keys: _derivedZipCryptoKeyMaterial.Value, passwordVerifierLow2Bytes: verifierLow2Bytes, @@ -782,7 +782,7 @@ private async Task WriteLocalFileHeaderAndDataIfNeededAsync(bool forceWrite, Can // The AES extra field stores the real compression method bool useDeflate = _compressionLevel != CompressionLevel.NoCompression; - WinZipAesStream encryptionStream = WinZipAesStream.Create( + Stream encryptionStream = WinZipAesStream.Create( baseStream: _archive.ArchiveStream, keyMaterial: _derivedAesKeyMaterial.Value, totalStreamSize: -1, diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs index f6f0dfc2af3106..76c1f2c4692df3 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs @@ -1776,7 +1776,7 @@ private unsafe void WriteLocalFileHeaderAndDataIfNeeded(bool forceWrite) ushort verifierLow2Bytes = (ushort)ZipHelper.DateTimeToDosTime(_lastModified.DateTime); - using (ZipCryptoStream encryptionStream = ZipCryptoStream.Create( + using (Stream encryptionStream = ZipCryptoStream.Create( baseStream: _archive.ArchiveStream, keys: _derivedZipCryptoKeyMaterial.Value, passwordVerifierLow2Bytes: verifierLow2Bytes, @@ -1807,7 +1807,7 @@ private unsafe void WriteLocalFileHeaderAndDataIfNeeded(bool forceWrite) bool useDeflate = _compressionLevel != CompressionLevel.NoCompression; - using (WinZipAesStream encryptionStream = WinZipAesStream.Create( + using (Stream encryptionStream = WinZipAesStream.Create( baseStream: _archive.ArchiveStream, keyMaterial: _derivedAesKeyMaterial.Value, totalStreamSize: -1, diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.Random.BrowserOrWasi.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.Random.BrowserOrWasi.cs new file mode 100644 index 00000000000000..68df99653c4d70 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.Random.BrowserOrWasi.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Compression +{ + internal sealed partial class ZipCryptoStream + { + private static unsafe void FillHeaderRandomBytes(Span header) + { + // System.Security.Cryptography's RandomNumberGenerator is not referenced on browser and is + // not supported on wasi, so call the shared native CSPRNG directly. It is backed by + // crypto.getRandomValues() on browser and by wasi:random (getentropy/__wasi_random_get) on wasi. + Span randomBytes = header.Slice(0, 10); + fixed (byte* pRandomBytes = randomBytes) + { + if (Interop.Sys.GetCryptographicallySecureRandomBytes(pRandomBytes, randomBytes.Length) != 0) + { + throw new IOException(SR.UnableToGenerateRandomBytes); + } + } + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.Random.Default.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.Random.Default.cs new file mode 100644 index 00000000000000..56c63770d6fa69 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.Random.Default.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; + +namespace System.IO.Compression +{ + internal sealed partial class ZipCryptoStream + { + private static void FillHeaderRandomBytes(Span header) + { + RandomNumberGenerator.Fill(header.Slice(0, 10)); + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.cs index 3243c56566ebb0..64450569184a34 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCryptoStream.cs @@ -4,14 +4,13 @@ using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; -using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace System.IO.Compression { - internal sealed class ZipCryptoStream : Stream + internal sealed partial class ZipCryptoStream : Stream { private const int EncryptionBufferSize = 4096; @@ -138,7 +137,7 @@ internal static ZipCryptoKeys CreateKey(ReadOnlySpan password) } finally { - CryptographicOperations.ZeroMemory(passwordBytes); + Array.Clear(passwordBytes); ArrayPool.Shared.Return(passwordBytes); } @@ -150,7 +149,7 @@ private void CalculateHeader(Span header) Debug.Assert(header.Length == 12); // bytes 0..9 random - RandomNumberGenerator.Fill(header.Slice(0, 10)); + FillHeaderRandomBytes(header); // bytes 10..11 verifier if (_crc32ForHeader.HasValue) diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs index 8ab7802ff61ac5..e79f9eeead138e 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs @@ -1052,6 +1052,26 @@ public async Task DecryptEntries_MixedEncryptions(bool async) await DisposeZipArchive(async, archive); } + [Theory] + [MemberData(nameof(Get_Booleans_Data))] + [PlatformSpecific(TestPlatforms.Browser)] + public async Task DecryptZipCryptoEntry_Browser(bool async) + { + const string Password = "S3cur3P@ssw0rd"; + using Stream archiveStream = await StreamHelpers.CreateTempCopyStream(passwordProtected("PasswordProtected_MixedEncryptions.zip")); + ZipArchive archive = await CreateZipArchive(async, archiveStream, ZipArchiveMode.Read); + + ZipArchiveEntry entry = archive.GetEntry("hello.txt"); + Assert.NotNull(entry); + Assert.Equal(ZipEncryptionMethod.ZipCrypto, entry.EncryptionMethod); + + using Stream entryStream = await OpenEntryStream(async, entry, Password); + using StreamReader reader = new(entryStream); + Assert.Equal("Hello", reader.ReadToEnd().TrimEnd()); + + await DisposeZipArchive(async, archive); + } + [Theory] [MemberData(nameof(Get_Booleans_Data))] [SkipOnPlatform(TestPlatforms.Browser, "WinZip AES encryption is not supported on browser.")] diff --git a/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs index 3e107ad21bb180..e0d1fad124f520 100644 --- a/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs @@ -115,6 +115,9 @@ public async Task ZipArchiveInteropTest() Configuration config = Configuration.Debug; ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "ZipArchiveInteropTest", extraProperties: "true"); BuildProject(info, config, new BuildOptions(AssertAppBundle: false)); + BuildPaths paths = GetBuildPaths(config, forPublish: false); + string pinvokeTableFileName = IsCoreClrRuntime ? "callhelpers-pinvoke.cpp" : "pinvoke-table.h"; + Assert.DoesNotContain("System_Security_Cryptography", File.ReadAllText(Path.Combine(paths.ObjWasmDir, pinvokeTableFileName))); RunResult result = await RunForBuildWithDotnetRun(new BrowserRunOptions(config, TestScenario: "ZipArchiveInteropTest")); Assert.Collection( result.TestOutput,