File: System\IO\Compression\WinZipAesKeyMaterial.cs
Web Access
Project: src\runtime\src\libraries\System.IO.Compression\src\System.IO.Compression.csproj (System.IO.Compression)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;

namespace System.IO.Compression
{
    /// <summary>
    /// Represents the parsed components of WinZip AES key material.
    /// The key material layout is: [salt][encryption key][HMAC key][password verifier (2 bytes)].
    /// </summary>
    internal readonly struct WinZipAesKeyMaterial
    {
        public byte[] Salt { get; }
        public byte[] EncryptionKey { get; }
        public byte[] HmacKey { get; }
        public byte[] PasswordVerifier { get; }
        public int KeySizeBits { get; }
        public int SaltSize { get; }

        private WinZipAesKeyMaterial(byte[] salt, byte[] encryptionKey, byte[] hmacKey, byte[] passwordVerifier, int keySizeBits)
        {
            if (OperatingSystem.IsBrowser())
            {
                throw new PlatformNotSupportedException(SR.WinZipEncryptionNotSupportedOnBrowser);
            }
            Salt = salt;
            EncryptionKey = encryptionKey;
            HmacKey = hmacKey;
            PasswordVerifier = passwordVerifier;
            KeySizeBits = keySizeBits;
            SaltSize = GetSaltSize(keySizeBits);
        }

        /// <summary>
        /// Derives key material from a password and optional salt using PBKDF2-SHA1.
        /// </summary>
        internal static unsafe WinZipAesKeyMaterial Create(ReadOnlySpan<char> password, byte[]? salt, int keySizeBits)
        {
            if (OperatingSystem.IsBrowser())
            {
                throw new PlatformNotSupportedException(SR.WinZipEncryptionNotSupportedOnBrowser);
            }
            int saltSize = GetSaltSize(keySizeBits);
            int keySizeBytes = keySizeBits / 8;
            int totalKeySize = checked(keySizeBytes + keySizeBytes + 2);

            byte[] saltBytes;
            if (salt is null)
            {
                saltBytes = new byte[saltSize];
                RandomNumberGenerator.Fill(saltBytes);
            }
            else
            {
                Debug.Assert(salt.Length == saltSize, $"Salt must be {saltSize} bytes for AES-{keySizeBits}.");
                saltBytes = salt;
            }

            int maxPasswordByteCount = Encoding.UTF8.GetMaxByteCount(password.Length);
            byte[] rentedPasswordBytes = ArrayPool<byte>.Shared.Rent(maxPasswordByteCount);
            Debug.Assert(totalKeySize <= 66, "totalKeySize should be at most 66 bytes (AES-256: 32 + 32 + 2)");
            Span<byte> derivedKey = stackalloc byte[totalKeySize];

            try
            {
                int actualByteCount = Encoding.UTF8.GetBytes(password, rentedPasswordBytes);
                Span<byte> passwordSpan = rentedPasswordBytes.AsSpan(0, actualByteCount);

                Rfc2898DeriveBytes.Pbkdf2(
                    passwordSpan,
                    saltBytes,
                    derivedKey,
                    1000, // iteration count specified by the WinZip AE-1/AE-2 specification
                    HashAlgorithmName.SHA1);

                // Slice the derived key directly into its components instead of
                // round-tripping through a combined array and Parse.
                byte[] encryptionKey = derivedKey.Slice(0, keySizeBytes).ToArray();
                byte[] hmacKey = derivedKey.Slice(keySizeBytes, keySizeBytes).ToArray();
                byte[] passwordVerifier = derivedKey.Slice(keySizeBytes + keySizeBytes, 2).ToArray();

                return new WinZipAesKeyMaterial(saltBytes, encryptionKey, hmacKey, passwordVerifier, keySizeBits);
            }
            finally
            {
                CryptographicOperations.ZeroMemory(rentedPasswordBytes);
                CryptographicOperations.ZeroMemory(derivedKey);
                ArrayPool<byte>.Shared.Return(rentedPasswordBytes);
            }
        }

        internal static int GetSaltSize(int keySizeBits) => keySizeBits / 16;
    }
}