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

using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace System.Security.Cryptography.Pkcs
{
    internal static class Pkcs12Kdf
    {
        private const byte CipherKeyId = 1;
        private const byte IvId = 2;
        private const byte MacKeyId = 3;

        // This is based on the table in https://tools.ietf.org/html/rfc7292#appendix-B.2.
        // It is small enough that it's cheaper to just use an array and linear search rather than dictionary lookup.
        private static readonly (HashAlgorithmName, int, int)[] s_uvLookup = new[]
        {
            (HashAlgorithmName.MD5, 128, 512),
            (HashAlgorithmName.SHA1, 160, 512),
            (HashAlgorithmName.SHA256, 256, 512),
            (HashAlgorithmName.SHA384, 384, 1024),
            (HashAlgorithmName.SHA512, 512, 1024),
        };

        internal static void DeriveCipherKey(
            ReadOnlySpan<char> password,
            HashAlgorithmName hashAlgorithm,
            int iterationCount,
            ReadOnlySpan<byte> salt,
            Span<byte> destination)
        {
            Derive(
                password,
                hashAlgorithm,
                iterationCount,
                CipherKeyId,
                salt,
                destination);
        }

        internal static void DeriveIV(
            ReadOnlySpan<char> password,
            HashAlgorithmName hashAlgorithm,
            int iterationCount,
            ReadOnlySpan<byte> salt,
            Span<byte> destination)
        {
            Derive(
                password,
                hashAlgorithm,
                iterationCount,
                IvId,
                salt,
                destination);
        }

        internal static void DeriveMacKey(
            ReadOnlySpan<char> password,
            HashAlgorithmName hashAlgorithm,
            int iterationCount,
            ReadOnlySpan<byte> salt,
            Span<byte> destination)
        {
            Derive(
                password,
                hashAlgorithm,
                iterationCount,
                MacKeyId,
                salt,
                destination);
        }

        private static unsafe void Derive(
            ReadOnlySpan<char> password,
            HashAlgorithmName hashAlgorithm,
            int iterationCount,
            byte id,
            ReadOnlySpan<byte> salt,
            Span<byte> destination)
        {
            // https://tools.ietf.org/html/rfc7292#appendix-B.2
            Debug.Assert(iterationCount >= 1);

            int u = -1, v = -1;
            foreach ((HashAlgorithmName, int, int) huv in s_uvLookup)
            {
                if (huv.Item1 == hashAlgorithm)
                {
                    u = huv.Item2;
                    v = huv.Item3;
                    break;
                }
            }

            if (u == -1)
            {
                throw new CryptographicException(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithm.Name);
            }

            Debug.Assert(v <= 1024);

            //  1. Construct a string, D (the "diversifier"), by concatenating v/8 copies of ID.
            int vBytes = v >> 3;
            Span<byte> D = stackalloc byte[vBytes];
            D.Fill(id);

            // 2.  Concatenate copies of the salt together to create a string S of
            // length v(ceiling(s/ v)) bits(the final copy of the salt may be
            // truncated to create S). Note that if the salt is the empty
            // string, then so is S.
            int SLen = ((salt.Length - 1 + vBytes) / vBytes) * vBytes;

            // The password is a null-terminated UTF-16BE version of the input.
            int passLen = checked((password.Length + 1) * 2);

            // If password contains a null ref then the span represents the null string (as opposed to
            // an empty string), and the P block should then have size 0 in the next step.
#if NETSTANDARD
#pragma warning disable CA2265 // Do not compare Span<T> to 'default'
            if (password == default)
#pragma warning restore CA2265
#else
            if (Unsafe.IsNullRef(ref MemoryMarshal.GetReference(password)))
#endif
            {
                passLen = 0;
            }

            // 3.  Concatenate copies of the password together to create a string P
            // of length v(ceiling(p/v)) bits (the final copy of the password
            // may be truncated to create P).  Note that if the password is the
            // empty string, then so is P.
            //
            // (The RFC quote considers the trailing '\0' to be part of the string,
            // so "empty string" from this RFC means "null string" in C#, and C#'s
            // "empty string" is not 'empty' in this context.)
            int PLen = checked(((passLen - 1 + vBytes) / vBytes) * vBytes);

            // 4.  Set I=S||P to be the concatenation of S and P.
            int ILen = checked(SLen + PLen);
            scoped Span<byte> I;
            byte[]? IRented = null;

            if ((uint)ILen <= 1024)
            {
                I = stackalloc byte[ILen];
            }
            else
            {
                IRented = CryptoPool.Rent(ILen);
                I = IRented.AsSpan(0, ILen);
            }

            IncrementalHash hash = IncrementalHash.CreateHash(hashAlgorithm);

            try
            {
                CircularCopy(salt, I.Slice(0, SLen));
                CircularCopyUtf16BE(password, I.Slice(SLen));

                int uBytes = u >> 3;

                Span<byte> hashBuf = stackalloc byte[uBytes];
                Span<byte> bBuf = stackalloc byte[vBytes];

                // 5.  Set c=ceiling(n/u).
                // 6.  For i=1, 2, ..., c, do the following:
                // (later we're going to start writing A_i values as output,
                // they mean "while work remains").
                while (true)
                {
                    // A.  Set A_i=H^r(D||I). (i.e., the r-th hash of D||I,
                    // H(H(H(... H(D || I))))
                    hash.AppendData(D);
                    hash.AppendData(I);

                    for (int j = iterationCount; j > 0; j--)
                    {
                        if (!hash.TryGetHashAndReset(hashBuf, out int bytesWritten) || bytesWritten != hashBuf.Length)
                        {
                            Debug.Fail($"Hash output wrote {bytesWritten} bytes when {hashBuf.Length} was expected");
                            throw new CryptographicException();
                        }

                        if (j != 1)
                        {
                            hash.AppendData(hashBuf);
                        }
                    }

                    // 7.  Concatenate A_1, A_2, ..., A_c together to form a pseudorandom
                    // bit string, A.
                    //
                    // 8.  Use the first n bits of A as the output of this entire process.

                    if (hashBuf.Length >= destination.Length)
                    {
                        hashBuf.Slice(0, destination.Length).CopyTo(destination);
                        return;
                    }

                    hashBuf.CopyTo(destination);
                    destination = destination.Slice(hashBuf.Length);

                    // B.  Concatenate copies of A_i to create a string B of length v
                    // bits(the final copy of Ai may be truncated to create B).
                    CircularCopy(hashBuf, bBuf);

                    // C.  Treating I as a concatenation I_0, I_1, ..., I_(k-1) of v-bit
                    // blocks, where k = ceiling(s / v) + ceiling(p / v), modify I by
                    // setting I_j = (I_j + B + 1) mod 2 ^ v for each j.
                    for (int j = (I.Length / vBytes) - 1; j >= 0; j--)
                    {
                        Span<byte> I_j = I.Slice(j * vBytes, vBytes);
                        AddPlusOne(I_j, bBuf);
                    }
                }
            }
            finally
            {
                CryptographicOperations.ZeroMemory(I);

                if (IRented != null)
                {
                    CryptoPool.Return(IRented, clearSize: 0);
                }

                hash.Dispose();
            }
        }

        private static void AddPlusOne(Span<byte> into, ReadOnlySpan<byte> addend)
        {
            Debug.Assert(into.Length == addend.Length);

            int carry = 1;

            for (int i = into.Length - 1; i >= 0; i--)
            {
                int tmp = carry + into[i] + addend[i];
                into[i] = (byte)tmp;
                carry = tmp >> 8;
            }
        }

        private static void CircularCopy(ReadOnlySpan<byte> bytes, Span<byte> destination)
        {
            Debug.Assert(bytes.Length > 0);

            while (destination.Length > 0)
            {
                if (destination.Length >= bytes.Length)
                {
                    bytes.CopyTo(destination);
                    destination = destination.Slice(bytes.Length);
                }
                else
                {
                    bytes.Slice(0, destination.Length).CopyTo(destination);
                    return;
                }
            }
        }

        private static void CircularCopyUtf16BE(ReadOnlySpan<char> password, Span<byte> destination)
        {
            int fullCopyLen = password.Length * 2;
            Encoding bigEndianUnicode = System.Text.Encoding.BigEndianUnicode;
            Debug.Assert(destination.Length % 2 == 0);

            while (destination.Length > 0)
            {
                if (destination.Length >= fullCopyLen)
                {
                    int count = bigEndianUnicode.GetBytes(password, destination);

                    if (count != fullCopyLen)
                    {
                        Debug.Fail($"Unexpected written byte count ({count} vs {fullCopyLen})");
                        throw new CryptographicException();
                    }

                    destination = destination.Slice(count);
                    Span<byte> nullTerminator = destination.Slice(0, Math.Min(2, destination.Length));
                    nullTerminator.Clear();
                    destination = destination.Slice(nullTerminator.Length);
                }
                else
                {
                    ReadOnlySpan<char> trimmed = password.Slice(0, destination.Length / 2);

                    int count = bigEndianUnicode.GetBytes(trimmed, destination);

                    if (count != destination.Length)
                    {
                        Debug.Fail($"Partial copy wrote {count} bytes of {destination.Length} expected");
                        throw new CryptographicException();
                    }

                    return;
                }
            }
        }
    }
}