diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md
index 13e7e9c8aef8e0..8a45d572d09b19 100644
--- a/docs/project/list-of-diagnostics.md
+++ b/docs/project/list-of-diagnostics.md
@@ -333,3 +333,4 @@ Diagnostic id values for experimental APIs must not be recycled, as that could s
| __`SYSLIB5006`__ | .NET 10 | TBD | Types for Post-Quantum Cryptography (PQC) are experimental. |
| __`SYSLIB5007`__ | .NET 11 | TBD | Low-level TLS engine types (`TlsContext`, `TlsSession`) in `System.Net.Security` are experimental. |
| __`SYSLIB5008`__ | .NET 11 | TBD | `SocketsHttpHandler` connection eviction control and `HttpRequestMessage.ConnectionId` APIs are experimental. |
+| __`SYSLIB5009`__ | .NET 11 | TBD | Types for HPKE (Hybrid Public Key Encryption) are experimental. |
diff --git a/src/libraries/Common/src/System/Experimentals.cs b/src/libraries/Common/src/System/Experimentals.cs
index f196bfd2d50a29..dfc10357776001 100644
--- a/src/libraries/Common/src/System/Experimentals.cs
+++ b/src/libraries/Common/src/System/Experimentals.cs
@@ -39,6 +39,9 @@ internal static class Experimentals
// SocketsHttpHandler connection eviction control and HttpRequestMessage.ConnectionId APIs are experimental.
internal const string SocketsHttpHandlerExperimentalDiagId = "SYSLIB5008";
+ // Types for HPKE (Hybrid Public Key Encryption) are experimental.
+ internal const string HpkeExperimentalDiagId = "SYSLIB5009";
+
// When adding a new diagnostic ID, add it to the table in docs\project\list-of-diagnostics.md as well.
// Keep new const identifiers above this comment.
}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs
new file mode 100644
index 00000000000000..a113a9fd27317a
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/Hpke.cs
@@ -0,0 +1,1519 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Security.Cryptography
+{
+ ///
+ /// Represents a Hybrid Public Key Encryption (HPKE) key.
+ ///
+ [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
+ public abstract class Hpke : IDisposable
+ {
+ // These inputs limits are somewhat arbitrary however 256 MB of any IKM, salt, info, etc. is excessively large.
+ // Keeping them at 256 MB or less allows avoiding overflowing some contiguous buffers. This is not a promise
+ // that 256 MB is accepted, either. Some HPKE suites have smaller limits, like single-stage keying material
+ // cannot be larger than 2^16.
+ internal const int MaximumInputSizeInBytes = 256 * 1024 * 1024;
+
+ private bool _disposed;
+
+ ///
+ /// Gets the cipher suite associated with this key.
+ ///
+ ///
+ /// The cipher suite associated with this key.
+ ///
+ public HpkeSuite Suite { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified cipher suite.
+ ///
+ ///
+ /// The cipher suite associated with this key.
+ ///
+ ///
+ /// is .
+ ///
+ protected Hpke(HpkeSuite suite)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+ Suite = suite;
+ }
+
+ ///
+ /// Determines whether the specified cipher suite is supported on the current platform.
+ ///
+ ///
+ /// The cipher suite to check.
+ ///
+ ///
+ /// if the cipher suite is supported; otherwise, .
+ ///
+ ///
+ /// is .
+ ///
+ public static bool IsSupported(HpkeSuite suite)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+ return HpkeImplementation.IsSupportedImpl(suite);
+ }
+
+ ///
+ /// Derives an HPKE key for the specified cipher suite from input keying material.
+ ///
+ ///
+ /// The cipher suite for the derived key.
+ ///
+ ///
+ /// The input keying material from which to derive the key.
+ ///
+ ///
+ /// The derived HPKE key.
+ ///
+ ///
+ /// or is .
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KEM.
+ ///
+ ///
+ /// is not supported on the current platform.
+ ///
+ public static Hpke DeriveKey(HpkeSuite suite, byte[] ikm)
+ {
+ ArgumentNullException.ThrowIfNull(ikm);
+ return DeriveKey(suite, new ReadOnlySpan(ikm));
+ }
+
+ ///
+ /// Derives an HPKE key for the specified cipher suite from input keying material.
+ ///
+ ///
+ /// The cipher suite for the derived key.
+ ///
+ ///
+ /// The input keying material from which to derive the key.
+ ///
+ ///
+ /// The derived HPKE key.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KEM.
+ ///
+ ///
+ /// is not supported on the current platform.
+ ///
+ public static Hpke DeriveKey(HpkeSuite suite, ReadOnlySpan ikm)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+
+ if (ikm.Length > HpkeKemMetadata.MaximumInputKeyingMaterialLength)
+ {
+ throw new ArgumentException(
+ SR.Format(
+ SR.Argument_HpkeIkmTooLong,
+ HpkeKemMetadata.MaximumInputKeyingMaterialLength),
+ nameof(ikm));
+ }
+
+ ThrowIfNotSupported(suite);
+ return HpkeImplementation.DeriveKeyImpl(suite, ikm);
+ }
+
+ ///
+ /// Generates a new HPKE key for the specified cipher suite.
+ ///
+ ///
+ /// The cipher suite for the new key.
+ ///
+ ///
+ /// A new HPKE key.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// is not supported on the current platform.
+ ///
+ public static Hpke GenerateKey(HpkeSuite suite)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+ ThrowIfNotSupported(suite);
+ return HpkeImplementation.GenerateKeyImpl(suite);
+ }
+
+ ///
+ /// Imports an HPKE key pair from a serialized decapsulation key.
+ ///
+ ///
+ /// The cipher suite associated with the key.
+ ///
+ ///
+ /// The serialized decapsulation key.
+ ///
+ ///
+ /// A new HPKE key containing the decapsulation key and its corresponding encapsulation key.
+ ///
+ ///
+ /// or is .
+ ///
+ ///
+ /// is not exactly bytes long.
+ ///
+ ///
+ /// The decapsulation key is invalid, or an error occurred while importing the key.
+ ///
+ ///
+ /// is not supported on the current platform.
+ ///
+ public static Hpke ImportDecapsulationKey(HpkeSuite suite, byte[] source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ return ImportDecapsulationKey(suite, new ReadOnlySpan(source));
+ }
+
+ ///
+ /// Imports an HPKE key pair from a serialized decapsulation key.
+ ///
+ ///
+ /// The cipher suite associated with the key.
+ ///
+ ///
+ /// The serialized decapsulation key.
+ ///
+ ///
+ /// A new HPKE key containing the decapsulation key and its corresponding encapsulation key.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// is not exactly bytes long.
+ ///
+ ///
+ /// The decapsulation key is invalid, or an error occurred while importing the key.
+ ///
+ ///
+ /// is not supported on the current platform.
+ ///
+ public static Hpke ImportDecapsulationKey(HpkeSuite suite, ReadOnlySpan source)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+
+ if (source.Length != suite.DecapsulationKeySizeInBytes)
+ {
+ throw new ArgumentException(SR.Argument_PrivateKeyWrongSizeForAlgorithm, nameof(source));
+ }
+
+ ThrowIfNotSupported(suite);
+ return HpkeImplementation.ImportDecapsulationKeyImpl(suite, source);
+ }
+
+ ///
+ /// Imports an HPKE key from a serialized encapsulation key.
+ ///
+ ///
+ /// The cipher suite associated with the key.
+ ///
+ ///
+ /// The serialized encapsulation key.
+ ///
+ ///
+ /// A new HPKE key containing only the encapsulation key.
+ ///
+ ///
+ /// or is .
+ ///
+ ///
+ /// is not exactly bytes long.
+ ///
+ ///
+ /// The encapsulation key is invalid, or an error occurred while importing the key.
+ ///
+ ///
+ /// is not supported on the current platform.
+ ///
+ public static Hpke ImportEncapsulationKey(HpkeSuite suite, byte[] source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ return ImportEncapsulationKey(suite, new ReadOnlySpan(source));
+ }
+
+ ///
+ /// Imports an HPKE key from a serialized encapsulation key.
+ ///
+ ///
+ /// The cipher suite associated with the key.
+ ///
+ ///
+ /// The serialized encapsulation key.
+ ///
+ ///
+ /// A new HPKE key containing only the encapsulation key.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// is not exactly bytes long.
+ ///
+ ///
+ /// The encapsulation key is invalid, or an error occurred while importing the key.
+ ///
+ ///
+ /// is not supported on the current platform.
+ ///
+ public static Hpke ImportEncapsulationKey(HpkeSuite suite, ReadOnlySpan source)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+
+ if (source.Length != suite.EncapsulationKeySizeInBytes)
+ {
+ throw new ArgumentException(SR.Argument_PublicKeyWrongSizeForAlgorithm, nameof(source));
+ }
+
+ ThrowIfNotSupported(suite);
+ return HpkeImplementation.ImportEncapsulationKeyImpl(suite, source);
+ }
+
+ ///
+ /// Exports the decapsulation key.
+ ///
+ ///
+ /// A new byte array containing the serialized decapsulation key, with a length of
+ /// bytes.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] ExportDecapsulationKey()
+ {
+ ThrowIfDisposed();
+ byte[] key = new byte[Suite.DecapsulationKeySizeInBytes];
+
+ try
+ {
+ ExportDecapsulationKeyCore(key);
+ return key;
+ }
+ catch
+ {
+ CryptographicOperations.ZeroMemory(key);
+ throw;
+ }
+ }
+
+ ///
+ /// Exports the decapsulation key into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the serialized decapsulation key.
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void ExportDecapsulationKey(Span destination)
+ {
+ if (destination.Length != Suite.DecapsulationKeySizeInBytes)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, Suite.DecapsulationKeySizeInBytes),
+ nameof(destination));
+ }
+
+ ThrowIfDisposed();
+ ExportDecapsulationKeyCore(destination);
+ }
+
+ ///
+ /// When overridden in a derived class, exports the decapsulation key into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the decapsulation key.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, or an error occurred while exporting the key.
+ ///
+ protected abstract void ExportDecapsulationKeyCore(Span destination);
+
+ ///
+ /// Exports the encapsulation key.
+ ///
+ ///
+ /// The encapsulation key.
+ ///
+ ///
+ /// An error occurred while exporting the key.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] ExportEncapsulationKey()
+ {
+ ThrowIfDisposed();
+ byte[] key = new byte[Suite.EncapsulationKeySizeInBytes];
+ ExportEncapsulationKeyCore(key);
+ return key;
+ }
+
+ ///
+ /// Exports the encapsulation key into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the encapsulation key.
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ ///
+ /// An error occurred while exporting the key.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void ExportEncapsulationKey(Span destination)
+ {
+ if (destination.Length != Suite.EncapsulationKeySizeInBytes)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulationKeySizeInBytes),
+ nameof(destination));
+ }
+
+ ThrowIfDisposed();
+ ExportEncapsulationKeyCore(destination);
+ }
+
+ ///
+ /// When overridden in a derived class, exports the encapsulation key into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the encapsulation key.
+ ///
+ ///
+ /// An error occurred while exporting the key.
+ ///
+ protected abstract void ExportEncapsulationKeyCore(Span destination);
+
+ ///
+ /// Encrypts a single message using Base mode.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// When this method returns, contains a new byte array containing the encapsulated secret to send
+ /// to the recipient.
+ ///
+ ///
+ /// When this method returns, contains a new byte array containing the ciphertext.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient.
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// The ciphertext length would exceed .
+ ///
+ ///
+ /// An error occurred during encryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void Seal(
+ ReadOnlySpan plaintext,
+ out byte[] encapsulatedSecret,
+ out byte[] ciphertext,
+ ReadOnlySpan associatedData = default,
+ ReadOnlySpan info = default)
+ {
+ ThrowIfInfoExceedsLimit(info);
+ int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length);
+ ThrowIfDisposed();
+
+ byte[] ciphertextBuffer = new byte[ciphertextLength];
+ byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes];
+
+ SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info);
+
+ encapsulatedSecret = encapsulatedSecretBuffer;
+ ciphertext = ciphertextBuffer;
+ }
+
+ ///
+ /// Encrypts a single message using Base mode.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// When this method returns, contains a new byte array containing the encapsulated secret to send
+ /// to the recipient.
+ ///
+ ///
+ /// When this method returns, contains a new byte array containing the ciphertext.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting,
+ /// or to use no additional authenticated data.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient,
+ /// or to use an empty context.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// The ciphertext length would exceed .
+ ///
+ ///
+ /// An error occurred during encryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void Seal(
+ byte[] plaintext,
+ out byte[] encapsulatedSecret,
+ out byte[] ciphertext,
+ byte[]? associatedData = null,
+ byte[]? info = null)
+ {
+ ArgumentNullException.ThrowIfNull(plaintext);
+ ThrowIfInfoExceedsLimit(info);
+ int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length);
+ ThrowIfDisposed();
+
+ byte[] ciphertextBuffer = new byte[ciphertextLength];
+ byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes];
+
+ // associatedData and info null's implicity convert to empty span.
+ SealCore(plaintext, encapsulatedSecretBuffer, ciphertextBuffer, associatedData, info);
+
+ encapsulatedSecret = encapsulatedSecretBuffer;
+ ciphertext = ciphertextBuffer;
+ }
+
+ ///
+ /// Encrypts a single message into the provided buffers using Base mode.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// The buffer to receive the encapsulated secret to send to the recipient.
+ ///
+ ///
+ /// The buffer to receive the ciphertext.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient.
+ ///
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ /// -or-
+ ///
+ /// is not exactly the length returned by
+ /// for .
+ ///
+ /// -or-
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// The ciphertext length would exceed .
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// An error occurred during encryption.
+ ///
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void Seal(
+ ReadOnlySpan plaintext,
+ Span encapsulatedSecret,
+ Span ciphertext,
+ ReadOnlySpan associatedData = default,
+ ReadOnlySpan info = default)
+ {
+ ThrowIfInfoExceedsLimit(info);
+
+ if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes),
+ nameof(encapsulatedSecret));
+ }
+
+ int expectedCiphertextLength = Suite.GetCiphertextLength(plaintext.Length);
+
+ if (ciphertext.Length != expectedCiphertextLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, expectedCiphertextLength),
+ nameof(ciphertext));
+ }
+
+ if (encapsulatedSecret.Overlaps(ciphertext) ||
+ plaintext.Overlaps(encapsulatedSecret) ||
+ associatedData.Overlaps(encapsulatedSecret) ||
+ info.Overlaps(encapsulatedSecret) ||
+ plaintext.Overlaps(ciphertext) ||
+ associatedData.Overlaps(ciphertext) ||
+ info.Overlaps(ciphertext))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ SealCore(plaintext, encapsulatedSecret, ciphertext, associatedData, info);
+ }
+
+ ///
+ /// When overridden in a derived class, encrypts a single message using Base mode.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// The buffer to receive the encapsulated secret.
+ ///
+ ///
+ /// The buffer to receive the ciphertext.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting.
+ ///
+ ///
+ /// The application context.
+ ///
+ ///
+ /// An error occurred during encryption.
+ ///
+ protected abstract void SealCore(
+ ReadOnlySpan plaintext,
+ Span encapsulatedSecret,
+ Span ciphertext,
+ ReadOnlySpan associatedData,
+ ReadOnlySpan info);
+
+ ///
+ /// Decrypts a single HPKE ciphertext using Base mode.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The ciphertext.
+ ///
+ ///
+ /// The additional authenticated data, which must match the value used by the sender.
+ ///
+ ///
+ /// The application context, which must match the value used by the sender.
+ ///
+ ///
+ /// A new byte array containing the plaintext.
+ ///
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ /// -or-
+ ///
+ /// is shorter than bytes.
+ ///
+ /// -or-
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// The ciphertext's contents could not be verified.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred during decryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] Open(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan ciphertext,
+ ReadOnlySpan associatedData = default,
+ ReadOnlySpan info = default)
+ {
+ int plaintextLength = ValidateOpenInputs(encapsulatedSecret, ciphertext, info);
+ ThrowIfDisposed();
+ byte[] plaintext = new byte[plaintextLength];
+
+ try
+ {
+ OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info);
+ return plaintext;
+ }
+ catch
+ {
+ CryptographicOperations.ZeroMemory(plaintext);
+ throw;
+ }
+ }
+
+ ///
+ /// Decrypts a single HPKE ciphertext using Base mode.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The ciphertext.
+ ///
+ ///
+ /// The additional authenticated data, which must match the value used by the sender,
+ /// or to use no additional authenticated data.
+ ///
+ ///
+ /// The application context, which must match the value used by the sender,
+ /// or to use an empty context.
+ ///
+ ///
+ /// A new byte array containing the plaintext.
+ ///
+ ///
+ /// or is .
+ ///
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ /// -or-
+ ///
+ /// is shorter than bytes.
+ ///
+ /// -or-
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// The ciphertext's contents could not be verified.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred during decryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] Open(
+ byte[] encapsulatedSecret,
+ byte[] ciphertext,
+ byte[]? associatedData = null,
+ byte[]? info = null)
+ {
+ ArgumentNullException.ThrowIfNull(encapsulatedSecret);
+ ArgumentNullException.ThrowIfNull(ciphertext);
+ return Open(
+ new ReadOnlySpan(encapsulatedSecret),
+ ciphertext,
+ new ReadOnlySpan(associatedData),
+ info);
+ }
+
+ ///
+ /// Decrypts a single HPKE ciphertext into the provided buffer using Base mode.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The ciphertext.
+ ///
+ ///
+ /// The buffer to receive the plaintext.
+ ///
+ ///
+ /// The additional authenticated data, which must match the value used by the sender.
+ ///
+ ///
+ /// The application context, which must match the value used by the sender.
+ ///
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ /// -or-
+ ///
+ /// is shorter than bytes.
+ ///
+ /// -or-
+ ///
+ /// The length of is not exactly the length of
+ /// minus .
+ ///
+ /// -or-
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// The ciphertext's contents could not be verified.
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred during decryption.
+ ///
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void Open(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan ciphertext,
+ Span plaintext,
+ ReadOnlySpan associatedData = default,
+ ReadOnlySpan info = default)
+ {
+ int plaintextLength = ValidateOpenInputs(encapsulatedSecret, ciphertext, info);
+
+ if (plaintext.Length != plaintextLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, plaintextLength),
+ nameof(plaintext));
+ }
+
+ if (encapsulatedSecret.Overlaps(plaintext) ||
+ ciphertext.Overlaps(plaintext) ||
+ associatedData.Overlaps(plaintext) ||
+ info.Overlaps(plaintext))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ OpenCore(encapsulatedSecret, ciphertext, plaintext, associatedData, info);
+ }
+
+ ///
+ /// When overridden in a derived class, decrypts a single HPKE ciphertext
+ /// using Base mode.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The ciphertext.
+ ///
+ ///
+ /// The buffer to receive the plaintext.
+ ///
+ ///
+ /// The additional authenticated data.
+ ///
+ ///
+ /// The application context.
+ ///
+ ///
+ /// The ciphertext's contents could not be verified.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred during decryption.
+ ///
+ protected abstract void OpenCore(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan ciphertext,
+ Span plaintext,
+ ReadOnlySpan associatedData,
+ ReadOnlySpan info);
+
+ ///
+ /// Creates an HPKE sender context using Base mode.
+ ///
+ ///
+ /// When this method returns, contains the encapsulated secret to send to the recipient.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient.
+ ///
+ ///
+ /// A new sender context for this key's cipher suite.
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// An error occurred while creating the sender.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public HpkeSender CreateSender(out byte[] encapsulatedSecret, ReadOnlySpan info = default)
+ {
+ ThrowIfInfoExceedsLimit(info);
+ ThrowIfDisposed();
+
+ byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes];
+ HpkeSender sender = CreateSenderCore(encapsulatedSecretBuffer, info);
+ encapsulatedSecret = encapsulatedSecretBuffer;
+ return sender;
+ }
+
+ ///
+ /// Creates an HPKE sender context using Base mode and writes the encapsulated secret
+ /// into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the encapsulated secret to send to the recipient.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient.
+ ///
+ ///
+ /// A new sender context for this key's cipher suite.
+ ///
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ /// -or-
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// An error occurred while creating the sender.
+ ///
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public HpkeSender CreateSender(Span encapsulatedSecret, ReadOnlySpan info = default)
+ {
+ ThrowIfInfoExceedsLimit(info);
+
+ if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes),
+ nameof(encapsulatedSecret));
+ }
+
+ if (info.Overlaps(encapsulatedSecret))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ return CreateSenderCore(encapsulatedSecret, info);
+ }
+
+ ///
+ /// When overridden in a derived class, creates an HPKE sender context using Base mode.
+ ///
+ ///
+ /// The buffer to receive the encapsulated secret.
+ ///
+ ///
+ /// The application context.
+ ///
+ ///
+ /// A new sender context for this key's cipher suite.
+ ///
+ ///
+ /// An error occurred while creating the sender.
+ ///
+ protected abstract HpkeSender CreateSenderCore(Span encapsulatedSecret, ReadOnlySpan info);
+
+ ///
+ /// Creates an HPKE recipient context using Base mode.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The application context, which must match the value used by the sender.
+ ///
+ ///
+ /// A new recipient context for this key's cipher suite.
+ ///
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ /// -or-
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred while creating the recipient.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public HpkeRecipient CreateRecipient(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan info = default)
+ {
+ ThrowIfInfoExceedsLimit(info);
+ ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret);
+ ThrowIfDisposed();
+ return CreateRecipientCore(encapsulatedSecret, info);
+ }
+
+ ///
+ /// Creates an HPKE recipient context using Base mode.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The application context, which must match the value used by the sender,
+ /// or to use an empty context.
+ ///
+ ///
+ /// A new recipient context for this key's cipher suite.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ ///
+ /// is not exactly
+ /// bytes long.
+ ///
+ /// -or-
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred while creating the recipient.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public HpkeRecipient CreateRecipient(byte[] encapsulatedSecret, byte[]? info = null)
+ {
+ ArgumentNullException.ThrowIfNull(encapsulatedSecret);
+ return CreateRecipient(new ReadOnlySpan(encapsulatedSecret), info);
+ }
+
+ ///
+ /// When overridden in a derived class, creates an HPKE recipient context using Base mode.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The application context.
+ ///
+ ///
+ /// A new recipient context for this key's cipher suite.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred while creating the recipient.
+ ///
+ protected abstract HpkeRecipient CreateRecipientCore(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan info);
+
+ ///
+ /// Creates an HPKE sender context using a pre-shared key.
+ ///
+ ///
+ /// The pre-shared key, which must be at least 32 bytes long.
+ ///
+ ///
+ /// The nonempty identifier for the pre-shared key.
+ ///
+ ///
+ /// When this method returns, contains the encapsulated secret to send to the recipient.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient.
+ ///
+ ///
+ /// A new sender context for this key's cipher suite.
+ ///
+ ///
+ /// is shorter than 32 bytes, is empty,
+ /// or an input exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// An error occurred while creating the sender.
+ ///
+ ///
+ /// Creating a PSK sender is not supported on the current platform.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public HpkeSender CreatePskSender(
+ ReadOnlySpan psk,
+ ReadOnlySpan pskId,
+ out byte[] encapsulatedSecret,
+ ReadOnlySpan info = default)
+ {
+ ThrowIfInvalidPskInputs(psk, pskId);
+ ThrowIfInfoExceedsLimit(info);
+ ThrowIfDisposed();
+
+ byte[] encapsulatedSecretBuffer = new byte[Suite.EncapsulatedSecretSizeInBytes];
+ HpkeSender sender = CreatePskSenderCore(encapsulatedSecretBuffer, info, psk, pskId);
+ encapsulatedSecret = encapsulatedSecretBuffer;
+ return sender;
+ }
+
+ ///
+ /// Creates an HPKE sender context using a pre-shared key.
+ ///
+ ///
+ /// The pre-shared key, which must be at least 32 bytes long.
+ ///
+ ///
+ /// The nonempty identifier for the pre-shared key.
+ ///
+ ///
+ /// When this method returns, contains the encapsulated secret to send to the recipient.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient,
+ /// or to use an empty context.
+ ///
+ ///
+ /// A new sender context for this key's cipher suite.
+ ///
+ ///
+ /// or is .
+ ///
+ ///
+ /// is shorter than 32 bytes, is empty,
+ /// or an input exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// An error occurred while creating the sender.
+ ///
+ ///
+ /// Creating a PSK sender is not supported on the current platform.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public HpkeSender CreatePskSender(
+ byte[] psk,
+ byte[] pskId,
+ out byte[] encapsulatedSecret,
+ byte[]? info = null)
+ {
+ ArgumentNullException.ThrowIfNull(psk);
+ ArgumentNullException.ThrowIfNull(pskId);
+ return CreatePskSender(new ReadOnlySpan(psk), pskId, out encapsulatedSecret, info);
+ }
+
+ ///
+ /// Creates an HPKE sender context using a pre-shared key and writes the encapsulated secret into the provided buffer.
+ ///
+ ///
+ /// The pre-shared key, which must be at least 32 bytes long.
+ ///
+ ///
+ /// The nonempty identifier for the pre-shared key.
+ ///
+ ///
+ /// The buffer to receive the encapsulated secret to send to the recipient.
+ ///
+ ///
+ /// The application context, which must match the value used by the recipient.
+ ///
+ ///
+ /// A new sender context for this key's cipher suite.
+ ///
+ ///
+ /// is shorter than 32 bytes, is empty,
+ /// an input exceeds the maximum length supported by the cipher suite's KDF,
+ /// or is not exactly
+ /// bytes long.
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// An error occurred while creating the sender.
+ ///
+ ///
+ ///
+ /// Creating a PSK sender is not supported on the current platform.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public HpkeSender CreatePskSender(
+ ReadOnlySpan psk,
+ ReadOnlySpan pskId,
+ Span encapsulatedSecret,
+ ReadOnlySpan info = default)
+ {
+ ThrowIfInvalidPskInputs(psk, pskId);
+ ThrowIfInfoExceedsLimit(info);
+
+ if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, Suite.EncapsulatedSecretSizeInBytes),
+ nameof(encapsulatedSecret));
+ }
+
+ if (psk.Overlaps(encapsulatedSecret) ||
+ pskId.Overlaps(encapsulatedSecret) ||
+ info.Overlaps(encapsulatedSecret))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ return CreatePskSenderCore(encapsulatedSecret, info, psk, pskId);
+ }
+
+ ///
+ /// When overridden in a derived class, creates an HPKE sender context using a pre-shared key.
+ ///
+ ///
+ /// The buffer to receive the encapsulated secret.
+ ///
+ ///
+ /// The application context.
+ ///
+ ///
+ /// The pre-shared key.
+ ///
+ ///
+ /// The identifier for the pre-shared key.
+ ///
+ ///
+ /// A new sender context for this key's cipher suite.
+ ///
+ ///
+ /// An error occurred while creating the sender.
+ ///
+ ///
+ /// Creating a PSK sender is not supported on the current platform.
+ ///
+ protected abstract HpkeSender CreatePskSenderCore(
+ Span encapsulatedSecret,
+ ReadOnlySpan info,
+ ReadOnlySpan psk,
+ ReadOnlySpan pskId);
+
+ ///
+ /// Creates an HPKE recipient context using a pre-shared key.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The pre-shared key, which must be at least 32 bytes long.
+ ///
+ ///
+ /// The nonempty identifier for the pre-shared key.
+ ///
+ ///
+ /// The application context, which must match the value used by the sender.
+ ///
+ ///
+ /// A new recipient context for this key's cipher suite.
+ ///
+ ///
+ /// is shorter than 32 bytes, is empty,
+ /// an input exceeds the maximum length supported by the cipher suite's KDF,
+ /// or is not exactly
+ /// bytes long.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred while creating the recipient.
+ ///
+ ///
+ /// Creating a PSK recipient is not supported on the current platform.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy.
+ /// The sender and recipient must use the same pre-shared key and identifier.
+ ///
+ public HpkeRecipient CreatePskRecipient(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan psk,
+ ReadOnlySpan pskId,
+ ReadOnlySpan info = default)
+ {
+ ThrowIfInvalidPskInputs(psk, pskId);
+ ThrowIfInfoExceedsLimit(info);
+ ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret);
+ ThrowIfDisposed();
+ return CreatePskRecipientCore(encapsulatedSecret, info, psk, pskId);
+ }
+
+ ///
+ /// Creates an HPKE recipient context using a pre-shared key.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The pre-shared key, which must be at least 32 bytes long.
+ ///
+ ///
+ /// The nonempty identifier for the pre-shared key.
+ ///
+ ///
+ /// The application context, which must match the value used by the sender,
+ /// or to use an empty context.
+ ///
+ ///
+ /// A new recipient context for this key's cipher suite.
+ ///
+ ///
+ /// , , or
+ /// is .
+ ///
+ ///
+ /// is shorter than 32 bytes, is empty,
+ /// an input exceeds the maximum length supported by the cipher suite's KDF,
+ /// or is not exactly
+ /// bytes long.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred while creating the recipient.
+ ///
+ ///
+ /// Creating a PSK recipient is not supported on the current platform.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// The caller must ensure that the pre-shared key has at least 32 bytes of entropy.
+ /// The sender and recipient must use the same pre-shared key and identifier.
+ ///
+ public HpkeRecipient CreatePskRecipient(
+ byte[] encapsulatedSecret,
+ byte[] psk,
+ byte[] pskId,
+ byte[]? info = null)
+ {
+ ArgumentNullException.ThrowIfNull(encapsulatedSecret);
+ ArgumentNullException.ThrowIfNull(psk);
+ ArgumentNullException.ThrowIfNull(pskId);
+ return CreatePskRecipient(new ReadOnlySpan(encapsulatedSecret), psk, pskId, info);
+ }
+
+ ///
+ /// When overridden in a derived class, creates an HPKE recipient context using a pre-shared key.
+ ///
+ ///
+ /// The encapsulated secret produced by the sender.
+ ///
+ ///
+ /// The application context.
+ ///
+ ///
+ /// The pre-shared key.
+ ///
+ ///
+ /// The identifier for the pre-shared key.
+ ///
+ ///
+ /// A new recipient context for this key's cipher suite.
+ ///
+ ///
+ /// The current instance does not contain a decapsulation key, the encapsulated secret is invalid,
+ /// or an error occurred while creating the recipient.
+ ///
+ ///
+ /// Creating a PSK recipient is not supported on the current platform.
+ ///
+ ///
+ /// The calling method has verified that this instance is not disposed, the encapsulated secret
+ /// has the exact required length, the pre-shared key is at least 32 bytes long, the identifier is nonempty,
+ /// and all inputs satisfy the KDF's length limits. Implementations must return an initialized recipient
+ /// for .
+ ///
+ protected abstract HpkeRecipient CreatePskRecipientCore(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan info,
+ ReadOnlySpan psk,
+ ReadOnlySpan pskId);
+
+ ///
+ /// Releases all resources used by the class.
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ///
+ /// Called by the Dispose() and Finalize() methods to release the managed and unmanaged
+ /// resources used by the current instance of the class.
+ ///
+ ///
+ /// to release managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ private static void ThrowIfNotSupported(HpkeSuite suite)
+ {
+ if (!IsSupported(suite))
+ {
+ throw new PlatformNotSupportedException();
+ }
+ }
+
+ private void ThrowIfInfoExceedsLimit(ReadOnlySpan info)
+ {
+ if (info.Length > Suite.KdfMetadata.MaximumInfoLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkeKdfInfoLength, Suite.KdfMetadata.MaximumInfoLength),
+ nameof(info));
+ }
+ }
+
+ private void ThrowIfInvalidPskInputs(ReadOnlySpan psk, ReadOnlySpan pskId)
+ {
+ // A shorter key cannot meet HPKE's requirement for 32 bytes of PSK entropy.
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1.2
+ const int MinimumPskLength = 32;
+
+ if (psk.Length < MinimumPskLength)
+ {
+ throw new ArgumentException(SR.Format(SR.Argument_HpkePskTooShort, MinimumPskLength), nameof(psk));
+ }
+
+ if (pskId.IsEmpty)
+ {
+ throw new ArgumentException(SR.Argument_HpkePskIdEmpty, nameof(pskId));
+ }
+
+ if (psk.Length > Suite.KdfMetadata.MaximumPskLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkePskTooLong, Suite.KdfMetadata.MaximumPskLength),
+ nameof(psk));
+ }
+
+ if (pskId.Length > Suite.KdfMetadata.MaximumPskIdLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkePskIdTooLong, Suite.KdfMetadata.MaximumPskIdLength),
+ nameof(pskId));
+ }
+ }
+
+ private void ThrowIfInvalidEncapsulatedSecretLength(ReadOnlySpan encapsulatedSecret)
+ {
+ if (encapsulatedSecret.Length != Suite.EncapsulatedSecretSizeInBytes)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkeEncapsulatedSecretLength, Suite.EncapsulatedSecretSizeInBytes),
+ nameof(encapsulatedSecret));
+ }
+ }
+
+ private int ValidateOpenInputs(
+ ReadOnlySpan encapsulatedSecret,
+ ReadOnlySpan ciphertext,
+ ReadOnlySpan info)
+ {
+ ThrowIfInfoExceedsLimit(info);
+ ThrowIfInvalidEncapsulatedSecretLength(encapsulatedSecret);
+
+ int tagSize = Suite.AeadTagSizeInBytes;
+
+ if (ciphertext.Length < tagSize)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkeCiphertextTooShort, tagSize),
+ nameof(ciphertext));
+ }
+
+ return ciphertext.Length - tagSize;
+ }
+
+ private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs
new file mode 100644
index 00000000000000..e589c151a42983
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeAead.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Security.Cryptography
+{
+ ///
+ /// Specifies an authenticated encryption with associated data (AEAD) algorithm for an HPKE cipher suite.
+ ///
+ ///
+ [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
+ public enum HpkeAead
+ {
+ ///
+ /// Indicates that authenticated encryption uses AES-GCM with a 128-bit key.
+ ///
+ AES_128_GCM = 0x0001,
+
+ ///
+ /// Indicates that authenticated encryption uses AES-GCM with a 256-bit key.
+ ///
+ AES_256_GCM = 0x0002,
+
+ ///
+ /// Indicates that authenticated encryption uses ChaCha20-Poly1305.
+ ///
+ ChaCha20Poly1305 = 0x0003,
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs
new file mode 100644
index 00000000000000..cd68e18eb518d0
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeAeadMetadata.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Security.Cryptography
+{
+ internal sealed partial class HpkeAeadMetadata
+ {
+ internal HpkeAead Aead { get; }
+ internal int Nk { get; }
+ internal int Nn { get; }
+ internal int Nt { get; }
+ internal string Name { get; }
+
+ private HpkeAeadMetadata(HpkeAead aead, int nk, int nn, int nt, string name)
+ {
+ Aead = aead;
+ Nk = nk;
+ Nn = nn;
+ Nt = nt;
+ Name = name;
+ }
+
+ internal static HpkeAeadMetadata? Create(HpkeAead aead)
+ {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.3
+ switch (aead)
+ {
+ case HpkeAead.AES_128_GCM:
+ return new HpkeAeadMetadata(aead, nk: 16, nn: 12, nt: 16, name: "AES-128-GCM");
+ case HpkeAead.AES_256_GCM:
+ return new HpkeAeadMetadata(aead, nk: 32, nn: 12, nt: 16, name: "AES-256-GCM");
+ case HpkeAead.ChaCha20Poly1305:
+ return new HpkeAeadMetadata(aead, nk: 32, nn: 12, nt: 16, name: "ChaCha20Poly1305");
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs
new file mode 100644
index 00000000000000..b7e0684736cb19
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdf.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Security.Cryptography
+{
+ ///
+ /// Specifies a key derivation function (KDF) for an HPKE cipher suite.
+ ///
+ ///
+ [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
+ public enum HpkeKdf
+ {
+ ///
+ /// Indicates that key derivation uses HKDF with SHA-256.
+ ///
+ HKDF_SHA256 = 0x0001,
+
+ ///
+ /// Indicates that key derivation uses HKDF with SHA-384.
+ ///
+ HKDF_SHA384 = 0x0002,
+
+ ///
+ /// Indicates that key derivation uses HKDF with SHA-512.
+ ///
+ HKDF_SHA512 = 0x0003,
+
+ ///
+ /// Indicates that key derivation uses SHAKE128.
+ ///
+ SHAKE128 = 0x0010,
+
+ ///
+ /// Indicates that key derivation uses SHAKE256.
+ ///
+ SHAKE256 = 0x0011
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs
new file mode 100644
index 00000000000000..0b304bf2764ad7
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKdfMetadata.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+
+namespace System.Security.Cryptography
+{
+ internal sealed partial class HpkeKdfMetadata
+ {
+ internal HpkeKdf Kdf { get; }
+ internal int Nh { get; }
+ internal bool IsTwoStage { get; }
+ internal string Name { get; }
+ internal int MaximumExporterContextLength { get; }
+ internal int MaximumInfoLength { get; }
+ internal int MaximumPskLength { get; }
+ internal int MaximumPskIdLength { get; }
+
+ // HKDF is limited to 255 hash blocks; HPKE encodes SHAKE output lengths in two bytes.
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-4.4
+ internal int MaximumExportLength => IsTwoStage ? 255 * Nh : ushort.MaxValue;
+
+ private HpkeKdfMetadata(HpkeKdf kdf, int nh, bool isTwoStage, string name)
+ {
+ Debug.Assert(nh <= 64, "Nh value is larger than 64.");
+
+ Kdf = kdf;
+ Nh = nh;
+ IsTwoStage = isTwoStage;
+ Name = name;
+ MaximumExporterContextLength = Hpke.MaximumInputSizeInBytes;
+
+ if (IsTwoStage)
+ {
+ MaximumInfoLength = Hpke.MaximumInputSizeInBytes;
+ MaximumPskLength = Hpke.MaximumInputSizeInBytes;
+ MaximumPskIdLength = Hpke.MaximumInputSizeInBytes;
+ }
+ else
+ {
+ // One-stage KDFs length-prefix each of these inputs with a 16-bit length.
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-5.1
+ MaximumInfoLength = ushort.MaxValue;
+ MaximumPskLength = ushort.MaxValue;
+ MaximumPskIdLength = ushort.MaxValue;
+ }
+ }
+
+ internal static HpkeKdfMetadata? Create(HpkeKdf kdf)
+ {
+ switch (kdf)
+ {
+ // HKDF's limits exceed the maximum input size supported by the HPKE API.
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.2
+ case HpkeKdf.HKDF_SHA256:
+ return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: true, name: "HKDF-SHA256");
+ case HpkeKdf.HKDF_SHA384:
+ return new HpkeKdfMetadata(kdf, nh: 48, isTwoStage: true, name: "HKDF-SHA384");
+ case HpkeKdf.HKDF_SHA512:
+ return new HpkeKdfMetadata(kdf, nh: 64, isTwoStage: true, name: "HKDF-SHA512");
+ case HpkeKdf.SHAKE128:
+ return new HpkeKdfMetadata(kdf, nh: 32, isTwoStage: false, name: "SHAKE128");
+ case HpkeKdf.SHAKE256:
+ return new HpkeKdfMetadata(kdf, nh: 64, isTwoStage: false, name: "SHAKE256");
+
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs
new file mode 100644
index 00000000000000..1d460e7b90dba3
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKem.cs
@@ -0,0 +1,60 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Security.Cryptography
+{
+ ///
+ /// Specifies a key encapsulation mechanism (KEM) for an HPKE cipher suite.
+ ///
+ ///
+ [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
+ public enum HpkeKem
+ {
+ ///
+ /// Indicates that key encapsulation uses DHKEM with the NIST P-256 curve and HKDF-SHA-256.
+ ///
+ DHKEM_P256_HKDF_SHA256 = 0x0010,
+
+ ///
+ /// Indicates that key encapsulation uses DHKEM with the NIST P-384 curve and HKDF-SHA-384.
+ ///
+ DHKEM_P384_HKDF_SHA384 = 0x0011,
+
+ ///
+ /// Indicates that key encapsulation uses DHKEM with the NIST P-521 curve and HKDF-SHA-512.
+ ///
+ DHKEM_P521_HKDF_SHA512 = 0x0012,
+
+ ///
+ /// Indicates that key encapsulation uses DHKEM with X25519 and HKDF-SHA-256.
+ ///
+ DHKEM_X25519_HKDF_SHA256 = 0x0020,
+
+ ///
+ /// Indicates that key encapsulation uses ML-KEM-512.
+ ///
+ MLKEM_512 = 0x0040,
+
+ ///
+ /// Indicates that key encapsulation uses ML-KEM-768.
+ ///
+ MLKEM_768 = 0x0041,
+
+ ///
+ /// Indicates that key encapsulation uses ML-KEM-1024.
+ ///
+ MLKEM_1024 = 0x0042,
+
+ ///
+ /// Indicates that key encapsulation combines ML-KEM-768 with ECDH using the NIST P-256 curve.
+ ///
+ MLKEM768_P256 = 0x0050,
+
+ ///
+ /// Indicates that key encapsulation combines ML-KEM-1024 with ECDH using the NIST P-384 curve.
+ ///
+ MLKEM1024_P384 = 0x0051,
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs
new file mode 100644
index 00000000000000..51a9e5e0ecfd91
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeKemMetadata.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Security.Cryptography
+{
+ internal sealed partial class HpkeKemMetadata
+ {
+ internal const int MaximumInputKeyingMaterialLength = Hpke.MaximumInputSizeInBytes;
+
+ internal HpkeKem Kem { get; }
+ internal int Nsk { get; }
+ internal int Npk { get; }
+ internal int Nenc { get; }
+ internal int Nsecret { get; }
+ internal string Name { get; }
+
+ private HpkeKemMetadata(HpkeKem kem, int nsecret, int nenc, int npk, int nsk, string name)
+ {
+ Kem = kem;
+ Nsk = nsk;
+ Npk = npk;
+ Nenc = nenc;
+ Nsecret = nsecret;
+ Name = name;
+ Setup();
+ }
+
+ partial void Setup();
+
+ internal static HpkeKemMetadata? Create(HpkeKem kem)
+ {
+ switch (kem)
+ {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04#section-7.1
+ case HpkeKem.DHKEM_P256_HKDF_SHA256:
+ return new HpkeKemMetadata(kem, nsecret: 32, nenc: 65, npk: 65, nsk: 32, name: "DHKEM(P-256, HKDF-SHA256)");
+ case HpkeKem.DHKEM_P384_HKDF_SHA384:
+ return new HpkeKemMetadata(kem, nsecret: 48, nenc: 97, npk: 97, nsk: 48, name: "DHKEM(P-384, HKDF-SHA384)");
+ case HpkeKem.DHKEM_P521_HKDF_SHA512:
+ return new HpkeKemMetadata(kem, nsecret: 64, nenc: 133, npk: 133, nsk: 66, name: "DHKEM(P-521, HKDF-SHA512)");
+ case HpkeKem.DHKEM_X25519_HKDF_SHA256:
+ return new HpkeKemMetadata(kem, nsecret: 32, nenc: 32, npk: 32, nsk: 32, name: "DHKEM(X25519, HKDF-SHA256)");
+
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-8.1
+ // Nsk is the 64-byte seed, not the expanded ML-KEM decapsulation key.
+ case HpkeKem.MLKEM_512:
+ return new HpkeKemMetadata(kem, nsecret: 32, nenc: 768, npk: 800, nsk: 64, name: "ML-KEM-512");
+ case HpkeKem.MLKEM_768:
+ return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1088, npk: 1184, nsk: 64, name: "ML-KEM-768");
+ case HpkeKem.MLKEM_1024:
+ return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1568, npk: 1568, nsk: 64, name: "ML-KEM-1024");
+
+ // https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05#section-8.2
+ // Nsk is the 32-byte seed used to derive both component key pairs.
+ case HpkeKem.MLKEM768_P256:
+ return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1153, npk: 1249, nsk: 32, name: "MLKEM768-P256");
+ case HpkeKem.MLKEM1024_P384:
+ return new HpkeKemMetadata(kem, nsecret: 32, nenc: 1665, npk: 1665, nsk: 32, name: "MLKEM1024-P384");
+
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs
new file mode 100644
index 00000000000000..f48c09029eae0c
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeRecipient.cs
@@ -0,0 +1,404 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Security.Cryptography
+{
+ ///
+ /// Represents an HPKE recipient context for decrypting multiple messages and exporting secrets.
+ ///
+ [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
+ public abstract class HpkeRecipient : IDisposable
+ {
+ private bool _disposed;
+
+ ///
+ /// Gets the cipher suite associated with this recipient.
+ ///
+ ///
+ /// The cipher suite associated with this recipient.
+ ///
+ public HpkeSuite Suite { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified cipher suite.
+ ///
+ ///
+ /// The cipher suite associated with this recipient.
+ ///
+ ///
+ /// is .
+ ///
+ protected HpkeRecipient(HpkeSuite suite)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+ Suite = suite;
+ }
+
+ ///
+ /// Decrypts and authenticates a message using this recipient context.
+ ///
+ ///
+ /// The ciphertext, including its trailing authentication tag.
+ ///
+ ///
+ /// The additional authenticated data, which must match the value used by the sender.
+ ///
+ ///
+ /// A new byte array containing the authenticated plaintext.
+ ///
+ ///
+ /// is shorter than bytes.
+ ///
+ ///
+ /// The authentication tag could not be verified.
+ ///
+ ///
+ /// The recipient's message limit has been reached, or an error occurred during decryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// Messages must be supplied in the same order in which the corresponding sender context encrypted them.
+ ///
+ public byte[] Open(ReadOnlySpan ciphertext, ReadOnlySpan associatedData = default)
+ {
+ int plaintextLength = GetPlaintextLength(ciphertext);
+ ThrowIfDisposed();
+ byte[] plaintext = new byte[plaintextLength];
+
+ try
+ {
+ OpenCore(ciphertext, plaintext, associatedData);
+ return plaintext;
+ }
+ catch
+ {
+ CryptographicOperations.ZeroMemory(plaintext);
+ throw;
+ }
+ }
+
+ ///
+ /// Decrypts and authenticates a message using this recipient context.
+ ///
+ ///
+ /// The ciphertext, including its trailing authentication tag.
+ ///
+ ///
+ /// The additional authenticated data, which must match the value used by the sender,
+ /// or to use no additional authenticated data.
+ ///
+ ///
+ /// A new byte array containing the authenticated plaintext.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// is shorter than bytes.
+ ///
+ ///
+ /// The authentication tag could not be verified.
+ ///
+ ///
+ /// The recipient's message limit has been reached, or an error occurred during decryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// Messages must be supplied in the same order in which the corresponding sender context encrypted them.
+ ///
+ public byte[] Open(byte[] ciphertext, byte[]? associatedData = null)
+ {
+ ArgumentNullException.ThrowIfNull(ciphertext);
+ return Open(new ReadOnlySpan(ciphertext), new ReadOnlySpan(associatedData));
+ }
+
+ ///
+ /// Decrypts and authenticates a message into the provided buffer using this recipient context.
+ ///
+ ///
+ /// The ciphertext, including its trailing authentication tag.
+ ///
+ ///
+ /// The buffer to receive the authenticated plaintext.
+ ///
+ ///
+ /// The additional authenticated data, which must match the value used by the sender.
+ ///
+ ///
+ ///
+ /// is shorter than bytes.
+ ///
+ /// -or-
+ ///
+ /// The length of is not exactly the length of
+ /// minus .
+ ///
+ ///
+ ///
+ /// The authentication tag could not be verified.
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// The recipient's message limit has been reached, or an error occurred during decryption.
+ ///
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// Messages must be supplied in the same order in which the corresponding sender context encrypted them.
+ ///
+ public void Open(
+ ReadOnlySpan ciphertext,
+ Span plaintext,
+ ReadOnlySpan associatedData = default)
+ {
+ int plaintextLength = GetPlaintextLength(ciphertext);
+
+ if (plaintext.Length != plaintextLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, plaintextLength),
+ nameof(plaintext));
+ }
+
+ if (ciphertext.Overlaps(plaintext) || associatedData.Overlaps(plaintext))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ OpenCore(ciphertext, plaintext, associatedData);
+ }
+
+ ///
+ /// When overridden in a derived class, decrypts and authenticates a message using this recipient context.
+ ///
+ ///
+ /// The ciphertext, including its trailing authentication tag.
+ ///
+ ///
+ /// The buffer to receive the authenticated plaintext.
+ ///
+ ///
+ /// The additional authenticated data.
+ ///
+ ///
+ /// The authentication tag could not be verified.
+ ///
+ ///
+ /// The recipient's message limit has been reached, or an error occurred during decryption.
+ ///
+ protected abstract void OpenCore(
+ ReadOnlySpan ciphertext,
+ Span plaintext,
+ ReadOnlySpan associatedData);
+
+ ///
+ /// Derives an exported secret from this recipient context.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The length, in bytes, of the exported secret.
+ ///
+ ///
+ /// A new byte array containing the exported secret.
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// is negative or exceeds the maximum export length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] Export(ReadOnlySpan exporterContext, int length)
+ {
+ ThrowIfExporterContextExceedsLimit(exporterContext);
+ ArgumentOutOfRangeException.ThrowIfNegative(length);
+ int maximumLength = Suite.KdfMetadata.MaximumExportLength;
+
+ if (length > maximumLength)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(length),
+ SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength));
+ }
+
+ ThrowIfDisposed();
+ byte[] secret = new byte[length];
+
+ try
+ {
+ ExportCore(exporterContext, secret);
+ return secret;
+ }
+ catch
+ {
+ CryptographicOperations.ZeroMemory(secret);
+ throw;
+ }
+ }
+
+ ///
+ /// Derives an exported secret from this recipient context.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The length, in bytes, of the exported secret.
+ ///
+ ///
+ /// A new byte array containing the exported secret.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// is negative or exceeds the maximum export length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] Export(byte[] exporterContext, int length)
+ {
+ ArgumentNullException.ThrowIfNull(exporterContext);
+ return Export(new ReadOnlySpan(exporterContext), length);
+ }
+
+ ///
+ /// Derives an exported secret from this recipient context into the provided buffer.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The buffer to receive the exported secret.
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF,
+ /// or the length of exceeds the maximum export length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void Export(ReadOnlySpan exporterContext, Span destination)
+ {
+ ThrowIfExporterContextExceedsLimit(exporterContext);
+ int maximumLength = Suite.KdfMetadata.MaximumExportLength;
+
+ if (destination.Length > maximumLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength),
+ nameof(destination));
+ }
+
+ if (exporterContext.Overlaps(destination))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ ExportCore(exporterContext, destination);
+ }
+
+ ///
+ /// When overridden in a derived class, derives an exported secret from this recipient context.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The buffer to receive the exported secret.
+ ///
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ protected abstract void ExportCore(ReadOnlySpan exporterContext, Span destination);
+
+ ///
+ /// Releases all resources used by the class.
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ///
+ /// Releases the unmanaged resources used by this recipient and optionally releases its managed resources.
+ ///
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ private void ThrowIfExporterContextExceedsLimit(ReadOnlySpan exporterContext)
+ {
+ if (exporterContext.Length > Suite.KdfMetadata.MaximumExporterContextLength)
+ {
+ throw new ArgumentException(
+ SR.Format(
+ SR.Argument_HpkeExporterContextTooLong,
+ Suite.KdfMetadata.MaximumExporterContextLength),
+ nameof(exporterContext));
+ }
+ }
+
+ private int GetPlaintextLength(ReadOnlySpan ciphertext)
+ {
+ int tagSize = Suite.AeadTagSizeInBytes;
+
+ if (ciphertext.Length < tagSize)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkeCiphertextTooShort, tagSize),
+ nameof(ciphertext));
+ }
+
+ return ciphertext.Length - tagSize;
+ }
+
+ private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs
new file mode 100644
index 00000000000000..fb202be5403bda
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSender.cs
@@ -0,0 +1,369 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Security.Cryptography
+{
+ ///
+ /// Represents an HPKE sender context for encrypting multiple messages and exporting secrets.
+ ///
+ [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
+ public abstract class HpkeSender : IDisposable
+ {
+ private bool _disposed;
+
+ ///
+ /// Gets the cipher suite associated with this sender.
+ ///
+ ///
+ /// The cipher suite associated with this sender.
+ ///
+ public HpkeSuite Suite { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified cipher suite.
+ ///
+ ///
+ /// The cipher suite associated with this sender.
+ ///
+ ///
+ /// is .
+ ///
+ protected HpkeSender(HpkeSuite suite)
+ {
+ ArgumentNullException.ThrowIfNull(suite);
+ Suite = suite;
+ }
+
+ ///
+ /// Encrypts and authenticates a message using this sender context.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting.
+ ///
+ ///
+ /// A new byte array containing the ciphertext followed by its authentication tag.
+ ///
+ ///
+ /// The ciphertext length would exceed .
+ ///
+ ///
+ /// The sender's message limit has been reached, or an error occurred during encryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// Messages must be decrypted by the corresponding recipient context in the same order
+ /// in which they were encrypted.
+ ///
+ public byte[] Seal(ReadOnlySpan plaintext, ReadOnlySpan associatedData = default)
+ {
+ int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length);
+ ThrowIfDisposed();
+ byte[] ciphertext = new byte[ciphertextLength];
+ SealCore(plaintext, ciphertext, associatedData);
+ return ciphertext;
+ }
+
+ ///
+ /// Encrypts and authenticates a message using this sender context.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting,
+ /// or to use no additional authenticated data.
+ ///
+ ///
+ /// A new byte array containing the ciphertext followed by its authentication tag.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// The ciphertext length would exceed .
+ ///
+ ///
+ /// The sender's message limit has been reached, or an error occurred during encryption.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// Messages must be decrypted by the corresponding recipient context in the same order
+ /// in which they were encrypted.
+ ///
+ public byte[] Seal(byte[] plaintext, byte[]? associatedData = null)
+ {
+ ArgumentNullException.ThrowIfNull(plaintext);
+ return Seal(new ReadOnlySpan(plaintext), new ReadOnlySpan(associatedData));
+ }
+
+ ///
+ /// Encrypts and authenticates a message into the provided buffer using this sender context.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// The buffer to receive the ciphertext followed by its authentication tag.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting.
+ ///
+ ///
+ /// is not exactly the length returned by
+ /// for .
+ ///
+ ///
+ /// The ciphertext length would exceed .
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// The sender's message limit has been reached, or an error occurred during encryption.
+ ///
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ ///
+ /// Messages must be decrypted by the corresponding recipient context in the same order
+ /// in which they were encrypted.
+ ///
+ public void Seal(
+ ReadOnlySpan plaintext,
+ Span ciphertext,
+ ReadOnlySpan associatedData = default)
+ {
+ int ciphertextLength = Suite.GetCiphertextLength(plaintext.Length);
+
+ if (ciphertext.Length != ciphertextLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_DestinationImprecise, ciphertextLength),
+ nameof(ciphertext));
+ }
+
+ if (plaintext.Overlaps(ciphertext) || associatedData.Overlaps(ciphertext))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ SealCore(plaintext, ciphertext, associatedData);
+ }
+
+ ///
+ /// When overridden in a derived class, encrypts and authenticates a message using this sender context.
+ ///
+ ///
+ /// The message to encrypt.
+ ///
+ ///
+ /// The buffer to receive the ciphertext followed by its authentication tag.
+ ///
+ ///
+ /// The additional data to authenticate without encrypting.
+ ///
+ ///
+ /// The sender's message limit has been reached, or an error occurred during encryption.
+ ///
+ protected abstract void SealCore(
+ ReadOnlySpan plaintext,
+ Span ciphertext,
+ ReadOnlySpan associatedData);
+
+ ///
+ /// Derives an exported secret from this sender context.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The length, in bytes, of the exported secret.
+ ///
+ ///
+ /// A new byte array containing the exported secret.
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// is negative or exceeds the maximum export length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] Export(ReadOnlySpan exporterContext, int length)
+ {
+ ThrowIfExporterContextExceedsLimit(exporterContext);
+ ArgumentOutOfRangeException.ThrowIfNegative(length);
+ int maximumLength = Suite.KdfMetadata.MaximumExportLength;
+
+ if (length > maximumLength)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(length),
+ SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength));
+ }
+
+ ThrowIfDisposed();
+ byte[] secret = new byte[length];
+
+ try
+ {
+ ExportCore(exporterContext, secret);
+ return secret;
+ }
+ catch
+ {
+ CryptographicOperations.ZeroMemory(secret);
+ throw;
+ }
+ }
+
+ ///
+ /// Derives an exported secret from this sender context.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The length, in bytes, of the exported secret.
+ ///
+ ///
+ /// A new byte array containing the exported secret.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// is negative or exceeds the maximum export length supported by the cipher suite's KDF.
+ ///
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public byte[] Export(byte[] exporterContext, int length)
+ {
+ ArgumentNullException.ThrowIfNull(exporterContext);
+ return Export(new ReadOnlySpan(exporterContext), length);
+ }
+
+ ///
+ /// Derives an exported secret from this sender context into the provided buffer.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The buffer to receive the exported secret.
+ ///
+ ///
+ /// exceeds the maximum length supported by the cipher suite's KDF,
+ /// or the length of exceeds the maximum export length supported by the cipher suite's KDF.
+ ///
+ ///
+ ///
+ /// One or more provided buffers overlap.
+ ///
+ /// -or-
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ ///
+ ///
+ /// The object has already been disposed.
+ ///
+ public void Export(ReadOnlySpan exporterContext, Span destination)
+ {
+ ThrowIfExporterContextExceedsLimit(exporterContext);
+ int maximumLength = Suite.KdfMetadata.MaximumExportLength;
+
+ if (destination.Length > maximumLength)
+ {
+ throw new ArgumentException(
+ SR.Format(SR.Argument_HpkeExportLengthTooLarge, maximumLength),
+ nameof(destination));
+ }
+
+ if (exporterContext.Overlaps(destination))
+ {
+ throw new CryptographicException(SR.Cryptography_OverlappingBuffers);
+ }
+
+ ThrowIfDisposed();
+ ExportCore(exporterContext, destination);
+ }
+
+ ///
+ /// When overridden in a derived class, derives an exported secret from this sender context.
+ ///
+ ///
+ /// The application context used to derive the exported secret.
+ ///
+ ///
+ /// The buffer to receive the exported secret.
+ ///
+ ///
+ /// An error occurred while deriving the exported secret.
+ ///
+ protected abstract void ExportCore(ReadOnlySpan exporterContext, Span destination);
+
+ ///
+ /// Releases all resources used by the class.
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ///
+ /// Releases the unmanaged resources used by this sender and optionally releases its managed resources.
+ ///
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ private void ThrowIfExporterContextExceedsLimit(ReadOnlySpan exporterContext)
+ {
+ if (exporterContext.Length > Suite.KdfMetadata.MaximumExporterContextLength)
+ {
+ throw new ArgumentException(
+ SR.Format(
+ SR.Argument_HpkeExporterContextTooLong,
+ Suite.KdfMetadata.MaximumExporterContextLength),
+ nameof(exporterContext));
+ }
+ }
+
+ private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs
new file mode 100644
index 00000000000000..506cff80cd889e
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/HpkeSuite.cs
@@ -0,0 +1,203 @@
+// 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.CodeAnalysis;
+
+namespace System.Security.Cryptography
+{
+ ///
+ /// Represents a Hybrid Public Key Encryption (HPKE) cipher suite.
+ ///
+ [Experimental(Experimentals.HpkeExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
+ public sealed class HpkeSuite : IEquatable
+ {
+ private readonly byte[] _suiteId;
+
+ internal HpkeAeadMetadata AeadMetadata { get; }
+ internal HpkeKdfMetadata KdfMetadata { get; }
+ internal HpkeKemMetadata KemMetadata { get; }
+ internal ReadOnlySpan SuiteId => _suiteId;
+
+ ///
+ /// Initializes a new instance of the class with the specified algorithms.
+ ///
+ ///
+ /// One of the enumeration values that specifies the key encapsulation mechanism (KEM) for the cipher suite.
+ ///
+ ///
+ /// One of the enumeration values that specifies the key derivation function (KDF) for the cipher suite.
+ ///
+ ///
+ /// One of the enumeration values that specifies the authenticated encryption with associated data (AEAD)
+ /// algorithm for the cipher suite.
+ ///
+ ///
+ /// , , or is not a defined value
+ /// of its corresponding enumeration.
+ ///
+ public HpkeSuite(HpkeKem kem, HpkeKdf kdf, HpkeAead aead)
+ {
+ KemMetadata = HpkeKemMetadata.Create(kem) ?? throw new ArgumentOutOfRangeException(nameof(kem));
+ KdfMetadata = HpkeKdfMetadata.Create(kdf) ?? throw new ArgumentOutOfRangeException(nameof(kdf));
+ AeadMetadata = HpkeAeadMetadata.Create(aead) ?? throw new ArgumentOutOfRangeException(nameof(aead));
+
+ _suiteId = new byte[10];
+ "HPKE"u8.CopyTo(_suiteId);
+ BinaryPrimitives.WriteUInt16BigEndian(_suiteId.AsSpan(4), checked((ushort)kem));
+ BinaryPrimitives.WriteUInt16BigEndian(_suiteId.AsSpan(6), checked((ushort)kdf));
+ BinaryPrimitives.WriteUInt16BigEndian(_suiteId.AsSpan(8), checked((ushort)aead));
+ }
+
+ ///
+ /// Gets the authenticated encryption with associated data (AEAD) algorithm for the cipher suite.
+ ///
+ ///
+ /// The authenticated encryption with associated data (AEAD) algorithm for the cipher suite.
+ ///
+ public HpkeAead AeadAlgorithm => AeadMetadata.Aead;
+
+ ///
+ /// Gets the key derivation function (KDF) for the cipher suite.
+ ///
+ ///
+ /// The key derivation function (KDF) for the cipher suite.
+ ///
+ public HpkeKdf KdfAlgorithm => KdfMetadata.Kdf;
+
+ ///
+ /// Gets the key encapsulation mechanism (KEM) for the cipher suite.
+ ///
+ ///
+ /// The key encapsulation mechanism (KEM) for the cipher suite.
+ ///
+ public HpkeKem KemAlgorithm => KemMetadata.Kem;
+
+ ///
+ /// Gets the size of the authentication tag for the cipher suite, in bytes.
+ ///
+ ///
+ /// The size of the authentication tag for the cipher suite, in bytes.
+ ///
+ public int AeadTagSizeInBytes => AeadMetadata.Nt;
+
+ ///
+ /// Gets the size of the decapsulation key for the cipher suite, in bytes.
+ ///
+ ///
+ /// The size of the decapsulation key for the cipher suite, in bytes.
+ ///
+ public int DecapsulationKeySizeInBytes => KemMetadata.Nsk;
+
+ ///
+ /// Gets the size of an encapsulated secret for the cipher suite, in bytes.
+ ///
+ ///
+ /// The size of an encapsulated secret for the cipher suite, in bytes.
+ ///
+ public int EncapsulatedSecretSizeInBytes => KemMetadata.Nenc;
+
+ ///
+ /// Gets the size of the encapsulation key for the cipher suite, in bytes.
+ ///
+ ///
+ /// The size of the encapsulation key for the cipher suite, in bytes.
+ ///
+ public int EncapsulationKeySizeInBytes => KemMetadata.Npk;
+
+ ///
+ /// Gets the name of the cipher suite.
+ ///
+ ///
+ /// The name of the cipher suite.
+ ///
+ public string Name => field ??= $"{KemMetadata.Name} {KdfMetadata.Name} {AeadMetadata.Name}";
+
+ ///
+ /// Gets the length of the ciphertext produced by encrypting a plaintext of the specified length.
+ ///
+ ///
+ /// The length of the plaintext, in bytes.
+ ///
+ ///
+ /// The length of the ciphertext, in bytes.
+ ///
+ ///
+ /// is negative or the resulting ciphertext length cannot be
+ /// represented as a signed 32-bit integer.
+ ///
+ public int GetCiphertextLength(int plaintextLength)
+ {
+ int tagSize = AeadTagSizeInBytes;
+
+ if (plaintextLength < 0 || plaintextLength > int.MaxValue - tagSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(plaintextLength));
+ }
+
+ return plaintextLength + tagSize;
+ }
+
+ ///
+ /// Compares two objects.
+ ///
+ ///
+ /// An object to be compared to the current object.
+ ///
+ ///
+ /// if is not and specifies
+ /// the same algorithms as the current object; otherwise, .
+ ///
+ public bool Equals([NotNullWhen(true)] HpkeSuite? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ return AeadAlgorithm == other.AeadAlgorithm &&
+ KdfAlgorithm == other.KdfAlgorithm &&
+ KemAlgorithm == other.KemAlgorithm;
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj) => obj is HpkeSuite suite && Equals(suite);
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(KemAlgorithm, KdfAlgorithm, AeadAlgorithm);
+
+ ///
+ public override string ToString() => Name;
+
+ ///
+ /// Determines whether two objects specify the same algorithms.
+ ///
+ ///
+ /// An object that specifies a cipher suite.
+ ///
+ ///
+ /// A second object, to be compared to the object that is identified by the parameter.
+ ///
+ ///
+ /// if the objects are considered equal; otherwise, .
+ ///
+ public static bool operator ==(HpkeSuite? left, HpkeSuite? right)
+ {
+ return left is null ? right is null : left.Equals(right);
+ }
+
+ ///
+ /// Determines whether two objects do not specify the same algorithms.
+ ///
+ ///
+ /// An object that specifies a cipher suite.
+ ///
+ ///
+ /// A second object, to be compared to the object that is identified by the parameter.
+ ///
+ ///
+ /// if the objects are not considered equal; otherwise, .
+ ///
+ public static bool operator !=(HpkeSuite? left, HpkeSuite? right) => !(left == right);
+ }
+}
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs
new file mode 100644
index 00000000000000..b355c398d7d6bd
--- /dev/null
+++ b/src/libraries/Common/tests/System/Security/Cryptography/HpkeContractTests.cs
@@ -0,0 +1,1667 @@
+// 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.Runtime.CompilerServices;
+using Xunit;
+using Xunit.Sdk;
+
+namespace System.Security.Cryptography.Tests
+{
+ public static class HpkeContractTests
+ {
+ private static readonly HpkeSuite s_suite = new(HpkeKem.MLKEM_768, HpkeKdf.SHAKE256, HpkeAead.AES_128_GCM);
+
+ public static IEnumerable