using System.Security.Cryptography; using NSec.Cryptography; namespace DodoSSH.Crypto; /// /// The DSH1 operations: symmetric content encryption and anonymous-sender key wrapping. /// /// /// /// docs/crypto.md is normative. Every operation takes an rather /// than a raw AAD, so a caller cannot omit the binding that stops a server relocating /// ciphertext between rows or key generations. /// /// /// Decryption returns on authentication failure rather than throwing. /// Ciphertext arrives from a server that is explicitly not trusted, so a failed tag is an /// expected outcome to be handled, not an exceptional one. /// /// public static class DshCrypto { private static AeadAlgorithm Aead => AeadAlgorithm.XChaCha20Poly1305; private static KeyAgreementAlgorithm Agreement => KeyAgreementAlgorithm.X25519; /// /// Encrypts under a known symmetric key, producing a complete DSH1 envelope /// (). /// /// 32-byte content key. /// Data to protect. /// Identifies the row this ciphertext belongs to. public static byte[] Seal(ReadOnlySpan key, ReadOnlySpan plaintext, in AadDescriptor descriptor) { RequireSymmetricKey(key); Span aad = stackalloc byte[CryptoSpec.DigestSize]; descriptor.ComputeAad(aad); // A 192-bit nonce is why a random nonce per message is safe with no counter to track. Span nonce = stackalloc byte[DshEnvelope.XChaChaNonceSize]; RandomNumberGenerator.Fill(nonce); using var aeadKey = Key.Import(Aead, key, KeyBlobFormat.RawSymmetricKey); var ciphertext = Aead.Encrypt(aeadKey, nonce, aad, plaintext); return DshEnvelope.Write(CryptoSpec.AlgorithmId.XChaCha20Poly1305, nonce, ciphertext); } /// /// Opens an envelope produced by . /// /// The plaintext, or if the envelope is malformed, uses /// another construction, or fails authentication under this descriptor. public static byte[]? Open(ReadOnlySpan key, ReadOnlySpan envelope, in AadDescriptor descriptor) { RequireSymmetricKey(key); if (!DshEnvelope.TryRead(envelope, out var view)) { return null; } if (view.Algorithm != CryptoSpec.AlgorithmId.XChaCha20Poly1305) { return null; } Span aad = stackalloc byte[CryptoSpec.DigestSize]; descriptor.ComputeAad(aad); using var aeadKey = Key.Import(Aead, key, KeyBlobFormat.RawSymmetricKey); return Aead.Decrypt(aeadKey, view.Nonce, aad, view.Ciphertext); } /// /// Seals to a recipient's X25519 public key, producing a /// envelope. See docs/crypto.md §6. /// /// /// Anonymous-sender by construction: this proves nothing about who created the envelope. /// Callers that need attribution — grants, in particular — must additionally attach a /// detached Ed25519 signature, or a server could fabricate a grant undetectably. /// public static byte[] SealTo( ReadOnlySpan recipientPublicKey, ReadOnlySpan plaintext, in AadDescriptor descriptor) { RequirePublicKey(recipientPublicKey); Span aad = stackalloc byte[CryptoSpec.DigestSize]; descriptor.ComputeAad(aad); var recipient = PublicKey.Import(Agreement, recipientPublicKey, KeyBlobFormat.RawPublicKey); // The ephemeral key must be exportable: its public half goes into the envelope. using var ephemeral = Key.Create( Agreement, new KeyCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport }); var ephemeralPublic = ephemeral.PublicKey.Export(KeyBlobFormat.RawPublicKey); using var shared = Agreement.Agree(ephemeral, recipient) ?? throw new CryptographicException("X25519 agreement failed; the recipient key is invalid."); Span derived = stackalloc byte[CryptoSpec.SymmetricKeySize]; DeriveSealKey(shared, ephemeralPublic, recipientPublicKey, aad, derived); Span nonce = stackalloc byte[DshEnvelope.XChaChaNonceSize]; RandomNumberGenerator.Fill(nonce); using var contentKey = Key.Import(Aead, derived, KeyBlobFormat.RawSymmetricKey); var ciphertext = Aead.Encrypt(contentKey, nonce, aad, plaintext); CryptographicOperations.ZeroMemory(derived); return DshEnvelope.Write( CryptoSpec.AlgorithmId.SealToX25519, nonce, ciphertext, ephemeralPublic); } /// /// Opens an envelope produced by using the recipient's private key. /// /// The plaintext, or on any failure. public static byte[]? OpenSealed(Key recipientKey, ReadOnlySpan envelope, in AadDescriptor descriptor) { ArgumentNullException.ThrowIfNull(recipientKey); if (!DshEnvelope.TryRead(envelope, out var view)) { return null; } if (view.Algorithm != CryptoSpec.AlgorithmId.SealToX25519) { return null; } Span aad = stackalloc byte[CryptoSpec.DigestSize]; descriptor.ComputeAad(aad); PublicKey ephemeralPublic; try { ephemeralPublic = PublicKey.Import(Agreement, view.EphemeralPublicKey, KeyBlobFormat.RawPublicKey); } catch (FormatException) { return null; } using var shared = Agreement.Agree(recipientKey, ephemeralPublic); if (shared is null) { // All-zero agreement: a low-order or otherwise invalid ephemeral key. return null; } var recipientPublic = recipientKey.PublicKey.Export(KeyBlobFormat.RawPublicKey); Span derived = stackalloc byte[CryptoSpec.SymmetricKeySize]; DeriveSealKey(shared, view.EphemeralPublicKey, recipientPublic, aad, derived); using var contentKey = Key.Import(Aead, derived, KeyBlobFormat.RawSymmetricKey); var plaintext = Aead.Decrypt(contentKey, view.Nonce, aad, view.Ciphertext); CryptographicOperations.ZeroMemory(derived); return plaintext; } /// /// Computes an identity fingerprint over a user's two public keys. See docs/crypto.md §8. /// public static byte[] ComputeFingerprint( ReadOnlySpan x25519PublicKey, ReadOnlySpan ed25519PublicKey) { RequirePublicKey(x25519PublicKey); RequirePublicKey(ed25519PublicKey); var label = CryptoSpec.DerivationLabels.Fingerprint; Span input = stackalloc byte[label.Length + (CryptoSpec.PublicKeySize * 2)]; label.CopyTo(input); x25519PublicKey.CopyTo(input[label.Length..]); ed25519PublicKey.CopyTo(input[(label.Length + CryptoSpec.PublicKeySize)..]); var fingerprint = new byte[CryptoSpec.DigestSize]; SHA256.HashData(input, fingerprint); return fingerprint; } /// /// HKDF-SHA256 over the X25519 shared secret, salted with both public keys and bound to /// the AAD. Specified rather than reusing libsodium's sealed box, whose KDF covers only the /// key pair and would not carry the AAD binding. /// private static void DeriveSealKey( SharedSecret shared, ReadOnlySpan ephemeralPublicKey, ReadOnlySpan recipientPublicKey, ReadOnlySpan aad, Span destination) { Span salt = stackalloc byte[CryptoSpec.PublicKeySize * 2]; ephemeralPublicKey.CopyTo(salt); recipientPublicKey.CopyTo(salt[CryptoSpec.PublicKeySize..]); var prefix = CryptoSpec.DerivationLabels.SealTo; Span info = stackalloc byte[prefix.Length + aad.Length]; prefix.CopyTo(info); aad.CopyTo(info[prefix.Length..]); KeyDerivationAlgorithm.HkdfSha256.DeriveBytes(shared, salt, info, destination); } private static void RequireSymmetricKey(ReadOnlySpan key) { if (key.Length != CryptoSpec.SymmetricKeySize) { throw new ArgumentException( $"Key must be {CryptoSpec.SymmetricKeySize} bytes, got {key.Length}.", nameof(key)); } } private static void RequirePublicKey(ReadOnlySpan publicKey) { if (publicKey.Length != CryptoSpec.PublicKeySize) { throw new ArgumentException( $"Public key must be {CryptoSpec.PublicKeySize} bytes, got {publicKey.Length}.", nameof(publicKey)); } } }