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);
}