namespace DodoSSH.Crypto; /// /// Constants of the DodoSSH cryptographic specification, version 1. /// /// /// docs/crypto.md is normative; this type must agree with it exactly. The values here are /// written into stored data, so changing one reinterprets or orphans existing ciphertext. /// Only clients can re-encrypt, so the server cannot migrate a change here. /// public static class CryptoSpec { /// Magic prefix identifying a DSH1 envelope. public static ReadOnlySpan EnvelopeMagic => "DSH1"u8; /// Magic prefix of the canonical AAD encoding. public static ReadOnlySpan AadMagic => "dsh1\n"u8; /// Length of the canonical AAD encoding, before hashing. public const int AadEncodedLength = 64; /// Version of the AAD derivation rule that payloads are bound to. /// /// Stored per row as payload_aad_version so a future change can be applied lazily, /// re-encrypting on next write rather than in a migration. /// public const byte CurrentAadVersion = 1; /// Version of the relational schema that AAD is bound to. public const ushort CurrentSchemaVersion = 1; /// Size of a symmetric content or wrapping key. public const int SymmetricKeySize = 32; /// /// Size of the passphrase-derived master key, which subkeys are expanded from. /// /// /// 64 bytes, not 32, because it is the PRK of an HKDF-SHA512 expansion and .NET's /// HKDF.Expand rejects a PRK shorter than the hash output. See docs/crypto.md §3. /// public const int MasterKeySize = 64; /// Size of an X25519 or Ed25519 public key. public const int PublicKeySize = 32; /// Size of an Ed25519 signature. public const int SignatureSize = 64; /// Size of a SHA-256 output, used for AAD, fingerprints and digests. public const int DigestSize = 32; /// Size of an AEAD authentication tag. public const int TagSize = 16; /// Recommended salt length for password-based derivation. public const int SaltSize = 16; /// Identifies the construction used by a DSH1 envelope. public enum AlgorithmId : byte { /// Reserved; never written, and rejected on read. Unspecified = 0, /// Symmetric content encryption under a known key. XChaCha20Poly1305 = 1, /// /// Symmetric fallback for environments without XChaCha20-Poly1305. Accepted on read, /// not currently emitted. /// Aes256Gcm = 2, /// Anonymous-sender seal to an X25519 public key. See docs/crypto.md §6. SealToX25519 = 3, // 4 is reserved for a hybrid X25519 + ML-KEM-768 seal. Claimed now so it cannot be // reused: store-now-decrypt-later is a real threat for long-lived SSH keys. } /// /// What a given ciphertext is, so that one kind of blob can never be substituted for /// another. Part of the AAD; see docs/crypto.md §4.2. /// public enum AadPurpose : byte { /// Not a legal value. Unspecified = 0, /// A wrap of the user's secret bundle. UserSecretBundle = 1, /// A vault key sealed to a member's public key. VaultKeyGrant = 2, /// An item data key wrapped under a vault key. ItemDataKey = 3, /// Item plaintext under its data key. ItemPayload = 4, /// Encrypted item metadata under its data key. ItemMetadata = 5, /// A record in a client's on-disk cache. LocalCache = 6, } /// /// The kind of entity a ciphertext belongs to. Part of the AAD; see docs/crypto.md §4.3. /// Append only. /// public enum AadResourceType : byte { /// No resource. Legal only where the purpose implies none. None = 0, /// A user account. User = 1, /// An enrolled device. Device = 2, /// A vault. Vault = 3, /// An SSH host. Host = 4, /// A credential. Credential = 5, /// An SSH key pair. SshKey = 6, /// A host group. HostGroup = 7, /// A tag. Tag = 8, /// A snippet. Snippet = 9, /// A port forward. PortForward = 10, /// A known SSH host key. KnownHostKey = 11, // 12 and 13 close a hole rather than adding a feature. Contracts.SyncEntityType has carried // HostTag and HostCredential since it was frozen, so those items are syncable — but with no // resource type here, their payloads had nothing to bind an AAD to. Added 2026-07-29, while // the enum is still append-only and no such item has been stored. /// A host-to-tag association. HostTag = 12, /// A host-to-credential association. HostCredential = 13, /// One connection that was made, and how long it lasted. /// /// 14 and 15 are the first members here that are not one-behind their SyncEntityType /// counterparts by the same constant offset — the two enums drifted apart when 12 and 13 closed a /// hole. That is why nothing casts between them and why AadResourceTypeTests pins each pairing /// by name. /// ConnectionLogEntry = 14, /// One create, edit or delete of a vault item. ActivityLogEntry = 15, /// An S3-compatible bucket, and the credentials that reach it. ObjectStore = 16, // 17 was SpelunkKey, removed with its item type (SyncEntityType 14). Not to be reused, so no payload // sealed under it can ever open as something else. /// This device's sign-in to one Spelunk. Local cache only; never synced. SpelunkSignIn = 18, } /// HKDF info labels. Domain-separated so one subkey cannot stand in for another. public static class DerivationLabels { /// Derives the key-encryption key that wraps the secret bundle. public static ReadOnlySpan PassphraseKek => "dsh1/kek/passphrase/v1"u8; /// /// Derives the key that encrypts the client's on-disk cache. /// /// /// v2 derives from the secret bundle, where v1 derived from the passphrase master key. The /// cache key has to be reachable through every door that opens the vault — passphrase, device, /// recovery code, escrow — and only the bundle is common to all of them. Under v1 a device unlock /// could open the identity and still not read the cache it had written, and a recovery-code unlock /// would silently derive a different key and orphan the whole cache. Bumping the label rather than /// reusing it is what makes an old cache fail to open instead of decrypting to nonsense. /// public static ReadOnlySpan LocalCache => "dsh1/localcache/v2"u8; /// Prefix of the SealTo key-derivation info, concatenated with the AAD. public static ReadOnlySpan SealTo => "dsh1/sealto/v1|"u8; /// Prefix of the identity key fingerprint input. public static ReadOnlySpan Fingerprint => "dsh1/fp/v1"u8; } /// Ed25519 signing contexts. Prevents a signature being replayed in another role. public static class SigningContexts { /// Signs an enrollment key statement. public static ReadOnlySpan KeyStatement => "dsh1/sig/keystatement/v1"u8; /// Signs a vault key grant tuple. public static ReadOnlySpan Grant => "dsh1/sig/grant/v1"u8; /// Signs an admin attestation of another user's key statement. public static ReadOnlySpan Attestation => "dsh1/sig/attestation/v1"u8; } }