namespace DodoSSH.Crypto; /// /// Named constructors for every AAD descriptor the specification defines. See docs/crypto.md §4. /// /// /// /// The AAD binding is the most valuable structural property in the design: it is what stops a /// server that holds every ciphertext from pasting one row's bytes onto another, rolling a row back /// to a superseded key generation, replaying a revoked grant, or substituting a metadata blob for a /// payload. None of that follows from access control. /// /// /// All of it also depends on callers getting the purpose, resource type and ids right at every call /// site. Hand-constructing makes that a matter of care; going through /// these factories makes it a matter of picking the right method name, which is the difference /// between a property that holds and one that mostly holds. /// /// public static class DshAad { /// /// Binds a wrap of the user's secret bundle to that user and key generation. /// /// /// /// Every wrap kind — passphrase, device, recovery, escrow — protects the same bundle and uses /// this same descriptor. In particular a device wrap is not bound to its device row: /// the client cannot be, since the server assigns the device id after the wrap is built, and it /// need not be, because the wrap is sealed to that device's public key. Relocating the row to /// another device gains an attacker nothing they could open. /// /// /// Including the generation means a server cannot serve back a superseded bundle after a key /// rotation. The client learns the current generation from /me, so a lie there produces /// a tag failure rather than a silent downgrade. /// /// /// /// The user's identifier, as assigned by the server. A client must therefore read /// /api/v1/me — which provisions the account and returns its id even before enrollment — /// before it can build a wrap. /// /// Generation of the identity key pair inside the bundle. public static AadDescriptor UserSecretBundle(Guid userId, uint keyGeneration = 1) => AadDescriptor.Create( CryptoSpec.AadPurpose.UserSecretBundle, CryptoSpec.AadResourceType.User, userId, keyGeneration: keyGeneration); /// /// Binds a vault key grant to its vault and generation. /// /// /// The generation is what makes revocation stick against a malicious server: after a rekey it /// cannot re-serve a previous generation's grant to a removed member, because the AAD no longer /// matches. That is a bound on future reads only — anything already downloaded is already gone, /// which is why offboarding means rotating the SSH credentials themselves. See ADR 0001. /// public static AadDescriptor VaultKeyGrant(Guid vaultId, uint keyGeneration) => AadDescriptor.Create( CryptoSpec.AadPurpose.VaultKeyGrant, CryptoSpec.AadResourceType.Vault, vaultId, keyGeneration: keyGeneration); /// Binds an item's data key, wrapped under the vault key, to that item and version. public static AadDescriptor ItemDataKey( CryptoSpec.AadResourceType resourceType, Guid itemId, uint keyGeneration, uint itemVersion) => AadDescriptor.Create( CryptoSpec.AadPurpose.ItemDataKey, resourceType, itemId, keyGeneration: keyGeneration, itemVersion: itemVersion); /// Binds an item's payload to the item, its data key, generation and version. /// What kind of item this is. /// The item. /// /// The data key the payload is under — the content_key_id column. Reserved so that /// per-item grants can make item-level access cryptographic in M5 without a migration. /// /// Vault key generation in force. /// Item version, so an earlier version cannot be replayed. public static AadDescriptor ItemPayload( CryptoSpec.AadResourceType resourceType, Guid itemId, Guid dataKeyId, uint keyGeneration, uint itemVersion) => AadDescriptor.Create( CryptoSpec.AadPurpose.ItemPayload, resourceType, itemId, dataKeyId, keyGeneration, itemVersion); /// /// Binds an item's encrypted metadata. /// /// /// A distinct purpose from on the same item, which is what stops a /// server swapping one blob for the other — a substitution that would otherwise leave a client /// decrypting a password where it expected a display name. /// public static AadDescriptor ItemMetadata( CryptoSpec.AadResourceType resourceType, Guid itemId, Guid dataKeyId, uint keyGeneration, uint itemVersion) => AadDescriptor.Create( CryptoSpec.AadPurpose.ItemMetadata, resourceType, itemId, dataKeyId, keyGeneration, itemVersion); /// /// Binds a record in the client's own on-disk cache to the row that holds it. /// /// /// /// Separate from every server-side purpose so a cache record can never be accepted as vault /// content, nor the reverse. The threat model differs too: the cache is local, so the adversary /// is a process or a backup with access to the file rather than the server. /// /// /// Changed 2026-07-29 from taking the user id to taking the record's identity. The user /// was already bound by the key — LocalCacheKey is derived from that user's master key, so /// another user's record cannot decrypt at all — which left the AAD binding nothing, and a cache /// record could be moved to a different row of the same user's cache. For plaintext columns like /// a relay address that is not academic: swapping two rows would point one host's connection at /// another host's address. No cache has ever been written, so there is nothing to migrate. /// /// /// What kind of item the record belongs to. /// The row it belongs to — an item id, or a conflict entry's own id. public static AadDescriptor LocalCache( CryptoSpec.AadResourceType resourceType, Guid recordId) => AadDescriptor.Create( CryptoSpec.AadPurpose.LocalCache, resourceType, recordId); }