File: System\Security\Cryptography\HpkeManagedKemAdapter.cs
Web Access
Project: 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.Buffers.Binary;
using System.Diagnostics;

namespace System.Security.Cryptography
{
    internal abstract class HpkeManagedKemAdapter : IDisposable
    {
        protected const int PrkStackBufferSize = SHA512.HashSizeInBytes;

        // HPKE draft, Section 4.4: version prefix for LabeledExtract and LabeledExpand.
        // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4
        private static ReadOnlySpan<byte> VersionLabel => "HPKE-v1"u8;

        // HPKE draft, Section 4.5: ExtractAndExpand labels for the PRK and KEM shared secret.
        // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.5
        private static ReadOnlySpan<byte> EaePrkLabel => "eae_prk"u8;
        private static ReadOnlySpan<byte> SharedSecretLabel => "shared_secret"u8;

        internal HpkeSuite Suite { get; }
        protected HpkeKdfMetadata KeyDerivationKdf => Suite.KemMetadata.KemKdf;

        protected HpkeManagedKemAdapter(HpkeSuite suite)
        {
            Suite = suite;
        }

        internal static HpkeManagedKemAdapter Create(HpkeSuite suite)
        {
            switch (suite.KemAlgorithm)
            {
                case HpkeKem.DHKEM_P256_HKDF_SHA256:
                case HpkeKem.DHKEM_P384_HKDF_SHA384:
                case HpkeKem.DHKEM_P521_HKDF_SHA512:
                    return new HpkeECDiffieHellmanKemAdapter(suite);
                case HpkeKem.DHKEM_X25519_HKDF_SHA256:
                    return new HpkeX25519DiffieHellmanKemAdapter(suite);
                default:
                    throw new PlatformNotSupportedException();
            }
        }

        internal void Generate()
        {
            const int MaxStackIkmSize = 128;
            Span<byte> ikmStack = stackalloc byte[MaxStackIkmSize];

            try
            {
                // https://datatracker.ietf.org/doc/draft-ietf-hpke-hpke/ 7.1.3
                // For all of the above instances of DHKEM, the GenerateKeyPair can be
                // implemented as DeriveKeyPair(random(Nsk)).
                Span<byte> ikm = ikmStack.Slice(0, Suite.KemMetadata.Nsk);
                RandomNumberGenerator.Fill(ikm);
                DeriveKeyPair(ikm);
            }
            finally
            {
                CryptographicOperations.ZeroMemory(ikmStack);
            }
        }

        // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.5
        protected void ExtractAndExpand(
            ReadOnlySpan<byte> secretAgreement,
            ReadOnlySpan<byte> encapsulatedSecret,
            Span<byte> sharedSecret)
        {
            int contextLength = checked(encapsulatedSecret.Length + Suite.EncapsulationKeySizeInBytes);

            // We don't support any KDFs with a hash (Nh) > 64. Since our supported KDFs is a closed set assume 64
            // will work.
            if (KeyDerivationKdf.Nh > PrkStackBufferSize)
            {
                Debug.Fail($"{KeyDerivationKdf.Nh} is unexpectedly bigger than {PrkStackBufferSize}.");
                throw new CryptographicException();
            }

            Span<byte> prkBuffer = stackalloc byte[PrkStackBufferSize];
            Span<byte> prk = prkBuffer.Slice(0, KeyDerivationKdf.Nh);

            try
            {
                const int MaxStackContextLength = 512;
                Span<byte> contextBuffer = stackalloc byte[MaxStackContextLength];
                Span<byte> context = contextBuffer.Slice(0, contextLength);

                // kem_context = enc || pkR. Both components are serialized public keys.
                encapsulatedSecret.CopyTo(context);
                ExportEncapsulationKey(context.Slice(encapsulatedSecret.Length));

                LabeledExtract(ReadOnlySpan<byte>.Empty, EaePrkLabel, secretAgreement, prk);
                LabeledExpand(prk, SharedSecretLabel, context, sharedSecret);
            }
            finally
            {
                CryptographicOperations.ZeroMemory(prkBuffer);
            }
        }

        // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4
        protected void LabeledExtract(
            ReadOnlySpan<byte> salt,
            ReadOnlySpan<byte> label,
            ReadOnlySpan<byte> ikm,
            Span<byte> prk)
        {
            Debug.Assert(KeyDerivationKdf.IsTwoStage);
            Debug.Assert(prk.Length == KeyDerivationKdf.Nh);

            ReadOnlySpan<byte> suiteId = Suite.KemMetadata.SuiteId;
            int labeledIkmLength = checked(VersionLabel.Length + suiteId.Length + label.Length + ikm.Length);

            using (CryptoPoolLease labeledIkm = CryptoPoolLease.Rent(labeledIkmLength))
            {
                Span<byte> destination = labeledIkm.Span;
                VersionLabel.CopyTo(destination);
                int offset = VersionLabel.Length;
                suiteId.CopyTo(destination.Slice(offset));
                offset += suiteId.Length;
                label.CopyTo(destination.Slice(offset));
                offset += label.Length;
                ikm.CopyTo(destination.Slice(offset));

                int written = HKDF.Extract(KeyDerivationKdf.HkdfHashAlgorithm, destination, salt, prk);
                Debug.Assert(written == prk.Length);
            }
        }

        protected void LabeledExpand(
            ReadOnlySpan<byte> prk,
            ReadOnlySpan<byte> label,
            ReadOnlySpan<byte> info,
            Span<byte> output)
        {
            Debug.Assert(KeyDerivationKdf.IsTwoStage);
            Debug.Assert(prk.Length == KeyDerivationKdf.Nh);
            Debug.Assert(output.Length <= ushort.MaxValue);

            ReadOnlySpan<byte> suiteId = Suite.KemMetadata.SuiteId;
            int labeledInfoLength =
                checked(sizeof(ushort) + VersionLabel.Length + suiteId.Length + label.Length + info.Length);
            const int MaxStackLabeledInfoLength = 512;
            Span<byte> labeledInfoBuffer = stackalloc byte[MaxStackLabeledInfoLength];
            Span<byte> destination = labeledInfoBuffer.Slice(0, labeledInfoLength);
            BinaryPrimitives.WriteUInt16BigEndian(destination, checked((ushort)output.Length));
            int offset = sizeof(ushort);
            VersionLabel.CopyTo(destination.Slice(offset));
            offset += VersionLabel.Length;
            suiteId.CopyTo(destination.Slice(offset));
            offset += suiteId.Length;
            label.CopyTo(destination.Slice(offset));
            offset += label.Length;
            info.CopyTo(destination.Slice(offset));

            HKDF.Expand(KeyDerivationKdf.HkdfHashAlgorithm, prk, output, destination);
        }

        internal abstract void DeriveKeyPair(ReadOnlySpan<byte> ikm);
        internal abstract void Encapsulate(Span<byte> encapsulatedSecret, Span<byte> sharedSecret);
        internal abstract void Decapsulate(ReadOnlySpan<byte> encapsulatedSecret, Span<byte> sharedSecret);
        internal abstract void ImportDecapsulationKey(ReadOnlySpan<byte> decapsulationKey);
        internal abstract void ImportEncapsulationKey(ReadOnlySpan<byte> encapsulationKey);
        internal abstract void ExportDecapsulationKey(Span<byte> destination);
        internal abstract void ExportEncapsulationKey(Span<byte> destination);
        public abstract void Dispose();
    }
}