diff --git a/docs/api/nsec.cryptography.aeaddetachedalgorithm.md b/docs/api/nsec.cryptography.aeaddetachedalgorithm.md new file mode 100644 index 00000000..ca4f095a --- /dev/null +++ b/docs/api/nsec.cryptography.aeaddetachedalgorithm.md @@ -0,0 +1,241 @@ +# AeadDetachedAlgorithm Class + +Represents an authenticated encryption with associated data (AEAD) algorithm. + + public abstract class AeadDetachedAlgorithm : Algorithm + + +## Inheritance Hierarchy + +* [[Algorithm|Algorithm Class]] + * **AeadDetachedAlgorithm** + * Aes256Gcm + + +## [TOC] Summary + + +## Static Properties + + +### Aes256Gcm + +Gets the AES256-GCM AEAD algorithm. + + public static Aes256GcmDetached Aes256Gcm { get; } + +#### Exceptions + +PlatformNotSupportedException +: The platform does not support hardware-accelerated AES. + +#### Remarks + +The AES-GCM implementation in NSec is hardware-accelerated and may not be +available on all architectures. Support can be determined at runtime using +the static `IsSupported` property of the `NSec.Cryptography.AesDetached256Gcm` class. + + +## Properties + + +### KeySize + +Gets the size of the key used for encryption and decryption. + + public int KeySize { get; } + +#### Property Value + +The key size, in bytes. + + +### NonceSize + +Gets the size of the nonce used for encryption and decryption. + + public int NonceSize { get; } + +#### Property Value + +The nonce size, in bytes. + + +### TagSize + +Gets the size of the authentication tag. + + public int TagSize { get; } + +#### Property Value + +The authentication tag size, in bytes. + + +## Methods + + +### Encrypt(Key, ReadOnlySpan, ReadOnlySpan, ReadOnlySpan, Span, Span) + +Encrypts the specified plaintext using the specified key, nonce, and associated +data, and fills the specified span of bytes with the ciphertext, which includes +an authentication tag. + + public void Encrypt( + Key key, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag) + +#### Parameters + +key +: The [[Key|Key Class]] to use for encryption. + This must be a cryptographically strong key as created by the + [[Key.Create|Key Class#Create]] class, not a password. + +nonce +: The nonce to use for encryption. + The same nonce must not be used more than once to encrypt data with the + specified key. + +!!! Note + Using the same nonce with the same key more than once leads to + catastrophic loss of security. + +: To prevent nonce reuse when encrypting multiple plaintexts with the same key, + it is recommended to increment the previous nonce. A randomly generated + nonce is unsafe unless the [[nonce size|AeadDetachedAlgorithm Class#NonceSize]] + is at least 24 bytes. + +associatedData +: Optional additional data to be authenticated during decryption. + +plaintext +: The data to encrypt. + +ciphertext +: The span to fill with the encrypted data. + The length of the span must be equal to `plaintext.Length`. +: `ciphertext` must not overlap in memory with `plaintext`, except if + `ciphertext` and `plaintext` point at exactly the same memory location + (in-place encryption). + +tag +: The span to fill with the authentication tag. + The length of the span must be equal to [[TagSize|AeadDetachedAlgorithm Class#TagSize]]. + +#### Exceptions + +ArgumentNullException +: `key` is `null`. + +ArgumentException +: `key.Algorithm` is not the same object as the current + [[AeadDetachedAlgorithm|AeadDetachedAlgorithm Class]] object. + +ArgumentException +: `nonce.Length` is not equal to [[NonceSize|AeadDetachedAlgorithm Class#NonceSize]]. + +ArgumentException +: `ciphertext.Length` is not equal to `plaintext.Length`. + +ArgumentException +: `ciphertext` overlaps in memory with `plaintext`. + +ArgumentException +: `tag.Length` is not equal to [[TagSize|AeadDetachedAlgorithm Class#TagSize]]. + +ObjectDisposedException +: `key` has been disposed. + + +### Decrypt(Key, ReadOnlySpan, ReadOnlySpan, ReadOnlySpan, ReadOnlySpan, Span) + +Decrypts and authenticates the specified ciphertext using the specified key, +nonce, and associated data. If successful, fills the specified span of bytes +with the decrypted plaintext. + + public bool Decrypt( + Key key, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext) + +#### Parameters + +key +: The [[Key|Key Class]] to use for decryption. + Authentication fails if this is not the same key that was used for + encryption. + +nonce +: The nonce to use for decryption. + Authentication fails if this is not the same nonce that was used for + encryption. + +associatedData +: Optional additional data to authenticate. + Authentication fails if this is not the same additional data that was used + for encryption. + +ciphertext +: The encrypted data to authenticate and decrypt. + Authentication fails if the integrity of the data was compromised. + +tag +: The data used to authenticate the encrypted data and additional data + Authentication fails if the encryptes data was compromised or additional data is not the same that was used for encryption. + +plaintext +: The span to fill with the decrypted and authenticated data. + The length of the span must be equal to `ciphertext.Length`. +: `plaintext` must not overlap in memory with `ciphertext`, except if + `plaintext` and `ciphertext` point at exactly the same memory location + (in-place decryption). + +#### Return Value + +`true` if decryption and authentication succeed; otherwise, `false`. + +#### Exceptions + +ArgumentNullException +: `key` is `null`. + +ArgumentException +: `key.Algorithm` is not the same object as the current + [[AeadDetachedAlgorithm|AeadDetachedAlgorithm Class]] object. + +ArgumentException +: `tag.Length` is not equal to [[TagSize|AeadDetachedAlgorithm Class#TagSize]]. + +ArgumentException +: `plaintext.Length` is not equal to `ciphertext.Length`. + +ArgumentException +: `plaintext` overlaps in memory with `ciphertext`. + +ObjectDisposedException +: `key` has been disposed. + + +## Thread Safety + +All members of this type are thread safe. + + +## Purity + +All methods yield the same result for the same arguments. + + +## See Also + +* API Reference + * [[Algorithm Class]] + * [[Key Class]] diff --git a/src/Cryptography/AeadDetachedAlgorithm.cs b/src/Cryptography/AeadDetachedAlgorithm.cs new file mode 100644 index 00000000..5facffca --- /dev/null +++ b/src/Cryptography/AeadDetachedAlgorithm.cs @@ -0,0 +1,159 @@ +using System; +using System.Diagnostics; +using System.Threading; +using static Interop.Libsodium; + +namespace NSec.Cryptography +{ + // + // An authenticated encryption with associated data (AEAD) algorithm + // + // Candidates + // + // | Algorithm | Reference | Key Size | Nonce Size | Tag Size | Max. Plaintext Size | + // | ------------------ | --------- | -------- | ---------- | -------- | ------------------- | + // | AES-256-GCM | RFC 5116 | 32 | 12 | 16 | 2^36-31 | + // + public abstract class AeadDetachedAlgorithm : Algorithm + { + private static Aes256GcmDetached? s_Aes256Gcm; + + private readonly int _keySize; + private readonly int _nonceSize; + private readonly int _tagSize; + + private protected AeadDetachedAlgorithm( + int keySize, + int nonceSize, + int tagSize) + { + Debug.Assert(keySize > 0); + Debug.Assert(nonceSize >= 0 && nonceSize <= 32); + Debug.Assert(tagSize >= 0 && tagSize <= 255); + + _keySize = keySize; + _nonceSize = nonceSize; + _tagSize = tagSize; + } + + public static Aes256GcmDetached Aes256Gcm + { + get + { + Aes256GcmDetached? instance = s_Aes256Gcm; + if (instance == null) + { + Interlocked.CompareExchange(ref s_Aes256Gcm, new Aes256GcmDetached(), null); + instance = s_Aes256Gcm; + } + return instance; + } + } + + public int KeySize => _keySize; + + public int NonceSize => _nonceSize; + + public int TagSize => _tagSize; + + public void Encrypt( + Key key, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag) + { + if (key == null) + { + throw Error.ArgumentNull_Key(nameof(key)); + } + if (key.Algorithm != this) + { + throw Error.Argument_KeyAlgorithmMismatch(nameof(key), nameof(key)); + } + if (nonce.Length != _nonceSize) + { + throw Error.Argument_NonceLength(nameof(nonce), _nonceSize); + } + if (ciphertext.Length != plaintext.Length) + { + throw Error.Argument_CiphertextLength(nameof(ciphertext)); + } + if (ciphertext.Overlaps(plaintext, out int offset) && offset != 0) + { + throw Error.Argument_OverlapCiphertext(nameof(ciphertext)); + } + if (tag.Length != _tagSize) + { + throw Error.Argument_TagLength(nameof(tag), _tagSize); + } + + EncryptCore(key.Handle, nonce, associatedData, plaintext, ciphertext, tag); + } + + public bool Decrypt( + Key key, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext) + { + if (key == null) + { + throw Error.ArgumentNull_Key(nameof(key)); + } + if (key.Algorithm != this) + { + throw Error.Argument_KeyAlgorithmMismatch(nameof(key), nameof(key)); + } + if (nonce.Length != _nonceSize) + { + return false; + } + if (tag.Length != _tagSize) + { + throw Error.Argument_TagLength(nameof(tag), _tagSize); + } + if (plaintext.Length != ciphertext.Length) + { + throw Error.Argument_PlaintextLength(nameof(plaintext)); + } + if (plaintext.Overlaps(ciphertext, out int offset) && offset != 0) + { + throw Error.Argument_OverlapPlaintext(nameof(plaintext)); + } + + return DecryptCore(key.Handle, nonce, associatedData, ciphertext, tag, plaintext); + } + + internal sealed override int GetKeySize() + { + return _keySize; + } + + internal sealed override int GetPublicKeySize() + { + throw Error.InvalidOperation_InternalError(); + } + + internal abstract override int GetSeedSize(); + + private protected abstract void EncryptCore( + SecureMemoryHandle keyHandle, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag); + + private protected abstract bool DecryptCore( + SecureMemoryHandle keyHandle, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext); + } +} diff --git a/src/Cryptography/Aes256GcmDetached.cs b/src/Cryptography/Aes256GcmDetached.cs new file mode 100644 index 00000000..fab95e8b --- /dev/null +++ b/src/Cryptography/Aes256GcmDetached.cs @@ -0,0 +1,196 @@ +using NSec.Cryptography.Formatting; +using System; +using System.Diagnostics; +using System.Threading; +using static Interop.Libsodium; + +namespace NSec.Cryptography +{ + // + // AES256-GCM + // + // Authenticated Encryption with Associated Data (AEAD) algorithm + // based on the Advanced Encryption Standard (AES) in Galois/Counter + // Mode (GCM) with 256-bit keys + // + // References: + // + // FIPS 197 - Advanced Encryption Standard (AES) + // + // NIST SP 800-38D - Recommendation for Block Cipher Modes of + // Operation: Galois/Counter Mode (GCM) and GMAC + // + // RFC 5116 - An Interface and Algorithms for Authenticated Encryption + // + // Parameters: + // + // Key Size - 32 bytes. + // + // Nonce Size - 12 bytes. + // + // Tag Size - 16 bytes. + // + // Plaintext Size - Between 0 and 2^36-31 bytes. (A Span can hold + // only up to 2^31-1 bytes.) + // + // Associated Data Size - Between 0 and 2^61-1 bytes. + // + // Ciphertext Size - The ciphertext always has the size of the + // plaintext plus the tag size. + // + public sealed class Aes256GcmDetached : AeadDetachedAlgorithm + { + private const uint NSecBlobHeader = 0xDE6144DE; + + private static int s_isSupported; + private static int s_selfTest; + + public Aes256GcmDetached() : base( + keySize: crypto_aead_aes256gcm_KEYBYTES, + nonceSize: crypto_aead_aes256gcm_NPUBBYTES, + tagSize: crypto_aead_aes256gcm_ABYTES) + { + if (s_selfTest == 0) + { + SelfTest(); + Interlocked.Exchange(ref s_selfTest, 1); + } + if (s_isSupported == 0) + { + Interlocked.Exchange(ref s_isSupported, crypto_aead_aes256gcm_is_available() != 0 ? 1 : -1); + } + if (s_isSupported < 0) + { + throw Error.PlatformNotSupported_Aes256Gcm(); + } + } + + public static bool IsSupported + { + get + { + if (s_isSupported == 0) + { + Sodium.Initialize(); + Interlocked.Exchange(ref s_isSupported, crypto_aead_aes256gcm_is_available() != 0 ? 1 : -1); + } + return s_isSupported > 0; + } + } + + internal override void CreateKey( + ReadOnlySpan seed, + out SecureMemoryHandle keyHandle, + out PublicKey? publicKey) + { + Debug.Assert(seed.Length == crypto_aead_aes256gcm_KEYBYTES); + + publicKey = null; + keyHandle = SecureMemoryHandle.CreateFrom(seed); + } + + private protected override void EncryptCore( + SecureMemoryHandle keyHandle, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag) + { + Debug.Assert(keyHandle.Size == crypto_aead_aes256gcm_KEYBYTES); + Debug.Assert(nonce.Length == crypto_aead_aes256gcm_NPUBBYTES); + Debug.Assert(ciphertext.Length == plaintext.Length); + Debug.Assert(tag.Length == crypto_aead_aes256gcm_ABYTES); + Debug.Assert(!tag.IsEmpty); + + int error = crypto_aead_aes256gcm_encrypt_detached( + ciphertext, + tag, + out ulong maclen, + plaintext, + (ulong)plaintext.Length, + associatedData, + (ulong)associatedData.Length, + IntPtr.Zero, + nonce, + keyHandle); + + Debug.Assert(error == 0); + Debug.Assert((ulong)tag.Length == maclen); + } + + internal override int GetSeedSize() + { + return crypto_aead_aes256gcm_KEYBYTES; + } + + private protected override bool DecryptCore( + SecureMemoryHandle keyHandle, + ReadOnlySpan nonce, + ReadOnlySpan associatedData, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext) + { + Debug.Assert(keyHandle.Size == crypto_aead_aes256gcm_KEYBYTES); + Debug.Assert(nonce.Length == crypto_aead_aes256gcm_NPUBBYTES); + Debug.Assert(plaintext.Length == ciphertext.Length); + Debug.Assert(tag.Length == crypto_aead_aes256gcm_ABYTES); + Debug.Assert(!tag.IsEmpty); + + int error = crypto_aead_aes256gcm_decrypt_detached( + plaintext, + IntPtr.Zero, + ciphertext, + (ulong)ciphertext.Length, + tag, + associatedData, + (ulong)associatedData.Length, + nonce, + keyHandle); + + return error == 0; + } + + internal override bool TryExportKey( + SecureMemoryHandle keyHandle, + KeyBlobFormat format, + Span blob, + out int blobSize) + { + return format switch + { + KeyBlobFormat.RawSymmetricKey => RawKeyFormatter.TryExport(keyHandle, blob, out blobSize), + KeyBlobFormat.NSecSymmetricKey => NSecKeyFormatter.TryExport(NSecBlobHeader, crypto_aead_aes256gcm_KEYBYTES, crypto_aead_aes256gcm_ABYTES, keyHandle, blob, out blobSize), + _ => throw Error.Argument_FormatNotSupported(nameof(format), format.ToString()), + }; + } + + internal override bool TryImportKey( + ReadOnlySpan blob, + KeyBlobFormat format, + out SecureMemoryHandle? keyHandle, + out PublicKey? publicKey) + { + publicKey = null; + + return format switch + { + KeyBlobFormat.RawSymmetricKey => RawKeyFormatter.TryImport(crypto_aead_aes256gcm_KEYBYTES, blob, out keyHandle), + KeyBlobFormat.NSecSymmetricKey => NSecKeyFormatter.TryImport(NSecBlobHeader, crypto_aead_aes256gcm_KEYBYTES, crypto_aead_aes256gcm_ABYTES, blob, out keyHandle), + _ => throw Error.Argument_FormatNotSupported(nameof(format), format.ToString()), + }; + } + + private static void SelfTest() + { + if ((crypto_aead_aes256gcm_abytes() != crypto_aead_aes256gcm_ABYTES) || + (crypto_aead_aes256gcm_keybytes() != crypto_aead_aes256gcm_KEYBYTES) || + (crypto_aead_aes256gcm_npubbytes() != crypto_aead_aes256gcm_NPUBBYTES) || + (crypto_aead_aes256gcm_nsecbytes() != crypto_aead_aes256gcm_NSECBYTES)) + { + throw Error.InvalidOperation_InitializationFailed(); + } + } + } +} diff --git a/src/Cryptography/Error.cs b/src/Cryptography/Error.cs index 7ede2509..c2844b53 100644 --- a/src/Cryptography/Error.cs +++ b/src/Cryptography/Error.cs @@ -234,6 +234,13 @@ internal static ArgumentException Argument_SignatureLength( return new ArgumentException(string.Format(ResourceManager.GetString(nameof(Argument_SignatureLength))!, arg0), paramName); } + internal static ArgumentException Argument_TagLength( + string paramName, + object? arg0) + { + return new ArgumentException(string.Format(ResourceManager.GetString(nameof(Argument_TagLength))!, arg0), paramName); + } + internal static ArgumentNullException ArgumentNull_Algorithm( string paramName) { diff --git a/src/Interop/Interop.Aead.Aes256Gcm.cs b/src/Interop/Interop.Aead.Aes256Gcm.cs index c0ccb382..89d9f153 100644 --- a/src/Interop/Interop.Aead.Aes256Gcm.cs +++ b/src/Interop/Interop.Aead.Aes256Gcm.cs @@ -41,6 +41,33 @@ internal static partial int crypto_aead_aes256gcm_encrypt( ReadOnlySpan npub, SecureMemoryHandle k); + [LibraryImport(Libraries.Libsodium)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial int crypto_aead_aes256gcm_decrypt_detached( + Span m, + IntPtr nsec, + ReadOnlySpan c, + ulong clen, + ReadOnlySpan mac, + ReadOnlySpan ad, + ulong adlen, + ReadOnlySpan npub, + SecureMemoryHandle k); + + [LibraryImport(Libraries.Libsodium)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial int crypto_aead_aes256gcm_encrypt_detached( + Span c, + Span mac, + out ulong maclen_p, + ReadOnlySpan m, + ulong mlen, + ReadOnlySpan ad, + ulong adlen, + IntPtr nsec, + ReadOnlySpan npub, + SecureMemoryHandle k); + [LibraryImport(Libraries.Libsodium)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial int crypto_aead_aes256gcm_is_available(); diff --git a/src/Interop/Interop.yaml b/src/Interop/Interop.yaml index 69769783..f159fada 100644 --- a/src/Interop/Interop.yaml +++ b/src/Interop/Interop.yaml @@ -47,6 +47,8 @@ Interop.Aead.Aes256Gcm.cs: - crypto_aead_aes256gcm_abytes - crypto_aead_aes256gcm_decrypt (out ulong mlen_p, IntPtr nsec, SecureMemoryHandle k) - crypto_aead_aes256gcm_encrypt (out ulong clen_p, IntPtr nsec, SecureMemoryHandle k) + - crypto_aead_aes256gcm_decrypt_detached (IntPtr nsec, SecureMemoryHandle k) + - crypto_aead_aes256gcm_encrypt_detached (out ulong maclen_p, IntPtr nsec, SecureMemoryHandle k) - crypto_aead_aes256gcm_is_available - crypto_aead_aes256gcm_keybytes - crypto_aead_aes256gcm_npubbytes diff --git a/tests/Algorithms/Aes256GcmDetachedTests.cs b/tests/Algorithms/Aes256GcmDetachedTests.cs new file mode 100644 index 00000000..2e8b5d81 --- /dev/null +++ b/tests/Algorithms/Aes256GcmDetachedTests.cs @@ -0,0 +1,143 @@ +using NSec.Cryptography; +using System; +using System.Text.Json; +using Xunit; +using static Interop.Libsodium; + +namespace NSec.Tests.Algorithms +{ + public static class Aes256GcmDetachedTests + { + public static readonly TheoryData PlaintextLengths = Utilities.Primes; + + #region Properties + + [Fact] + public static void Properties() + { + var a = AeadDetachedAlgorithm.Aes256Gcm; + + Assert.Equal(32, a.KeySize); + Assert.Equal(12, a.NonceSize); + Assert.Equal(16, a.TagSize); + } + + [Fact] + public static void IsSupported() + { + Assert.InRange(Aes256GcmDetached.IsSupported, false, true); + } + + #endregion + + #region Encrypt/Decrypt + + [Theory] + [MemberData(nameof(PlaintextLengths))] + public static void EncryptDecrypt(int length) + { + var a = AeadDetachedAlgorithm.Aes256Gcm; + + using var k = new Key(a); + var n = Utilities.RandomBytes[..a.NonceSize]; + var ad = Utilities.RandomBytes[..100]; + + var expected = Utilities.RandomBytes[..length].ToArray(); + + Span ciphertext = new byte[length]; + Span tag = new byte[a.TagSize]; + Span actual = new byte[length]; + + a.Encrypt(k, n, ad, expected, ciphertext, tag); + + var decryptResult = a.Decrypt(k, n, ad, ciphertext, tag, actual); + Assert.True(decryptResult); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(PlaintextLengths))] + public static void EncryptDetachedDecryptCombined(int length) + { + var detached = AeadDetachedAlgorithm.Aes256Gcm; + var combined = AeadAlgorithm.Aes256Gcm; + + var keyBlobFmt = KeyBlobFormat.NSecSymmetricKey; + + using var detachedK = new Key(detached, new KeyCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport}); + var n = Utilities.RandomBytes[..detached.NonceSize]; + Assert.Equal(combined.NonceSize, n.Length); + var ad = Utilities.RandomBytes[..100]; + + var expected = Utilities.RandomBytes[..length].ToArray(); + + Span ciphertext = new byte[length]; + Span tag = new byte[detached.TagSize]; + Assert.Equal(combined.TagSize, tag.Length); + + detached.Encrypt(detachedK, n, ad, expected, ciphertext, tag); + + Span keyBlob = new byte[detachedK.GetExportBlobSize(keyBlobFmt)]; + + var exportResult = detached.TryExportKey(detachedK.Handle, keyBlobFmt, keyBlob, out int blobsize); + Assert.True(exportResult); + Assert.Equal(keyBlob.Length, blobsize); + + var importResult = Key.TryImport(AeadAlgorithm.Aes256Gcm, keyBlob, keyBlobFmt, out var combinedK); + Assert.True(importResult); + Assert.NotNull(combinedK); + Assert.Equal(combined.KeySize, combinedK?.Size); + + var combinedCiphertext = new byte[length + combined.TagSize]; + Assert.Equal(ciphertext.Length + combined.TagSize, combinedCiphertext.Length); + Array.Copy(ciphertext.ToArray(), combinedCiphertext, ciphertext.Length); + Array.Copy(tag.ToArray(), 0, combinedCiphertext, ciphertext.Length, tag.Length); + + var actual = combined.Decrypt(combinedK, n, ad, combinedCiphertext); + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(PlaintextLengths))] + public static void EncryptCombinedDecryptDetached(int length) + { + var combined = AeadAlgorithm.Aes256Gcm; + var detached = AeadDetachedAlgorithm.Aes256Gcm; + + var keyBlobFmt = KeyBlobFormat.NSecSymmetricKey; + + using var combinedK = new Key(combined, new KeyCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport }); + var n = Utilities.RandomBytes[..combined.NonceSize]; + var ad = Utilities.RandomBytes[..100]; + + var expected = Utilities.RandomBytes[..length].ToArray(); + + var ciphertext = combined.Encrypt(combinedK, n, ad, expected); + Assert.NotNull(ciphertext); + Assert.Equal(length + combined.TagSize, ciphertext.Length); + + Span detachedCiphertext = new Span(ciphertext, 0, length); + Span tag = new Span(ciphertext, length, combined.TagSize); + + Span keyBlob = new byte[combinedK.GetExportBlobSize(keyBlobFmt)]; + + var exportResult = combined.TryExportKey(combinedK.Handle, keyBlobFmt, keyBlob, out var blobSize); + Assert.True(exportResult); + Assert.Equal(keyBlob.Length, blobSize); + + var importResult = Key.TryImport(AeadDetachedAlgorithm.Aes256Gcm, keyBlob, keyBlobFmt, out var detachedK); + Assert.True(importResult); + Assert.NotNull(detachedK); + Assert.Equal(detached.KeySize, detachedK?.Size); + + Span actual = new byte[length]; + + var decryptResult = detached.Decrypt(detachedK, n, ad, detachedCiphertext, tag, actual); + Assert.True(decryptResult); + Assert.Equal(expected, actual); + } + + #endregion + } +}