namespace DodoSSH.Contracts;
///
/// Argon2id parameters as stored and transmitted.
///
///
/// Stored in plaintext per wrap row. Salts are not secrets, and keeping the parameters with the
/// wrap makes raising them later a per-user, unlock-time migration rather than a breaking
/// change — an older client can still open its own wrap.
///
/// is kibibytes, matching both the storage column and libsodium.
/// Client code should go through Argon2Profile rather than handling the number directly;
/// see docs/crypto.md §2 for why that unit needs guarding.
///
///
/// KDF identifier. Currently always argon2id.
/// 16-byte salt.
/// Memory cost in KiB.
/// Number of passes.
/// Lanes. Always 1; libsodium supports no other value.
public sealed record KdfParameters(
string Algorithm,
byte[] Salt,
int MemoryKibibytes,
int Passes,
int Parallelism);
///
/// The self-describing, signed statement binding a user to their public keys.
///
///
/// Its SHA-256 is used as the nonce of a fresh OIDC authorization, so the resulting ID
/// token is signed by the identity provider over exactly these keys. That is what stops the
/// DodoSSH server fabricating a key for a user who never enrolled. See ADR 0001.
///
/// Statement format version.
/// OIDC issuer.
/// OIDC subject.
/// Email at enrollment time, for display only.
/// X25519 public key, 32 bytes.
/// Ed25519 public key, 32 bytes.
/// Generation of this key pair, starting at 1.
/// When the client generated the keys.
/// Human-readable name of the enrolling device.
public sealed record KeyStatement(
int Version,
string Issuer,
string Subject,
string? Email,
byte[] EncryptionPublicKey,
byte[] SigningPublicKey,
int KeyGeneration,
DateTimeOffset CreatedAt,
string DeviceName);
///
/// The personal vault a client creates as part of enrolling.
///
///
///
/// The vault key is generated on the client and sealed to the user's own X25519 key, so the
/// server cannot produce this and cannot verify that contains
/// anything in particular. It stores the bytes and the signature that attributes them.
///
///
/// is chosen by the client, not the server. That is what makes enrollment
/// safely retryable: a client whose request timed out re-sends the identical body and gets the
/// identical result, instead of accumulating a second vault it never learns about. It is also
/// required by the grant signature, which covers the vault id — see docs/crypto.md §7.
///
///
/// Client-generated UUIDv7 for the new vault.
/// Display name. Plaintext, as all vault names are.
///
/// The vault key sealed to the enrolling user's own encryption key. Opaque to the server.
///
///
/// Ed25519 signature over the canonical grant tuple, by the key being enrolled. A self-grant
/// carries no key log head, because there is no third party whose key could have been
/// substituted.
///
/// Signing timestamp, part of the signed tuple.
public sealed record PersonalVaultRequest(
Guid VaultId,
string Name,
byte[] WrappedVaultKey,
byte[] GrantSignature,
DateTimeOffset GrantedAt);
/// A request to enroll a user's first identity key pair.
/// The key statement.
/// Ed25519 self-signature over the statement.
///
/// An ID token whose nonce equals the SHA-256 of the statement. The server verifies it
/// against the provider's JWKS.
///
///
/// The user's secret bundle sealed under the passphrase-derived key. Opaque to the server.
///
/// Parameters needed to re-derive the wrapping key.
///
/// X25519 public key of this device, so the bundle can also be wrapped to the device and
/// unlocked without re-entering the passphrase.
///
///
/// The same bundle sealed to . Required whenever a device key
/// is supplied: only the holder of the bundle can produce this, so a device key without its wrap
/// registers a device that can never unlock anything.
///
///
/// The same bundle sealed under a recovery-code-derived key.
///
/// Parameters for the recovery wrap.
///
/// The personal vault to create, with its key already wrapped to the enrolling user.
///
public sealed record EnrollmentRequest(
KeyStatement Statement,
byte[] StatementSignature,
string IdentityProviderToken,
byte[] WrappedPrivateKey,
KdfParameters KdfParameters,
byte[]? DevicePublicKey,
byte[]? DeviceWrappedPrivateKey,
byte[]? RecoveryWrappedPrivateKey,
KdfParameters? RecoveryKdfParameters,
PersonalVaultRequest PersonalVault);
/// Result of a successful enrollment.
/// The user's identifier.
/// The generation now current.
/// Identity fingerprint over both public keys.
/// The automatically created personal vault.
/// The enrolled device, when a device key was supplied.
///
/// Position of this statement in the append-only key log. Clients cache the log head and
/// include it in signed operations, which is what makes a forked view detectable.
///
public sealed record EnrollmentResponse(
Guid UserId,
int KeyGeneration,
byte[] Fingerprint,
Guid PersonalVaultId,
Guid? DeviceId,
long KeyLogSequence);
/// A public-key directory entry.
///
/// Before wrapping a vault key to one of these, a client must verify it: check the identity
/// provider binding, compare against its pinned fingerprint, and confirm the key log head is
/// consistent. Wrapping to an unverified key is the one mistake that undoes end-to-end
/// encryption entirely.
///
/// The user.
/// Email, for display.
/// Display name.
/// X25519 public key.
/// Ed25519 public key.
/// Identity fingerprint.
/// Generation of this key pair.
/// Key log position of the statement that introduced it.
public sealed record DirectoryEntry(
Guid UserId,
string? Email,
string? DisplayName,
byte[] EncryptionPublicKey,
byte[] SigningPublicKey,
byte[] Fingerprint,
int KeyGeneration,
long KeyLogSequence);
/// The caller's own profile and unlock state.
/// The user.
/// OIDC issuer.
/// OIDC subject.
/// Email.
/// Display name.
///
/// True when no identity key exists yet, so the client must run enrollment before anything else.
///
/// Current key generation, when enrolled.
///
/// The passphrase wrap of the secret bundle, for unlocking on this device.
///
/// Parameters to re-derive the wrapping key.
/// Vaults the caller can reach.
public sealed record MeResponse(
Guid UserId,
string Issuer,
string Subject,
string? Email,
string? DisplayName,
bool EnrollmentRequired,
int? KeyGeneration,
byte[]? WrappedPrivateKey,
KdfParameters? KdfParameters,
IReadOnlyList Vaults);
/// A vault the caller can reach, with the wrapped key needed to open it.
/// The vault.
/// Display name. Plaintext, and only for vaults, not for items.
/// Whether this is the caller's personal vault.
/// Owning team, for a team vault.
/// Current key generation.
/// The caller's effective permissions, as a flags value.
///
/// The vault key sealed to the caller's X25519 key. Absent when a grant is awaiting re-wrap
/// after a rekey, in which case the vault is temporarily unreadable and a member holding Share
/// must complete it.
///
/// Whether a membership change has left this vault needing a rekey.
public sealed record VaultSummary(
Guid VaultId,
string Name,
bool IsPersonal,
Guid? TeamId,
uint KeyGeneration,
int Permissions,
byte[]? WrappedVaultKey,
bool RekeyRequired);