import type { CreateDeviceRevocationRequest, DeviceTrust, PutDeviceWrappedKeyRequest, RevocationPins } from '@cleocode/contracts/cloud'; import { type KdfParams, type KeyPair } from './crypto.js'; /** What the server stores for a user (PutUserKeysRequest). Nothing here opens without a secret. */ export interface StoredUserKeys { passphraseWrappedMasterKey: string; kdf: KdfParams; recoveryWrappedMasterKey: string; masterKeyVerifier: string; keyVersion: number; } export declare const masterKeyVerifier: (mk: Uint8Array) => string; /** * First-device setup: mint the master key and wrap it by passphrase and by a new recovery key. * Show `recoveryKey` to the user exactly once. */ export declare function createUserKeys(userId: string, passphrase: string, keyVersion?: number): Promise<{ masterKey: Buffer; recoveryKey: string; stored: StoredUserKeys; }>; export declare function unlockWithPassphrase(userId: string, stored: StoredUserKeys, passphrase: string): Promise>; export declare function unlockWithRecoveryKey(userId: string, stored: StoredUserKeys, recoveryKey: string): Buffer; /** Re-wrap the master key under a new passphrase (for example after a recovery-key unlock). */ export declare function rewrapPassphrase(userId: string, mk: Buffer, stored: StoredUserKeys, newPassphrase: string): Promise; /** A device's public identity, as raw key bytes. */ export interface DevicePublicKeys { deviceId: string; encryptionPublicKey: Uint8Array; signingPublicKey: Uint8Array; } /** A grant or certificate that does not come from a trusted master key holder. Never retry it blindly. */ export declare class KeyTrustError extends Error { constructor(message: string); } /** * Certify a device under the master key: HMAC-SHA256 keyed by HKDF(MK, "device-cert"). Only a holder of * the master key can make one, so the server cannot add a device of its own to the set a client trusts. */ export declare function deviceCertificate(mk: Uint8Array, userId: string, device: DevicePublicKeys, keyVersion: number): Buffer; export declare function verifyDeviceCertificate(mk: Uint8Array, userId: string, device: DevicePublicKeys, keyVersion: number, certificate: Uint8Array): boolean; /** * One signing key a client trusts for a device. `pin` is null for a live key. A revoked key keeps a pin: * its signatures are accepted up to the pinned replicaSeq per replica and the pinned checkpoint coversSeq * per stream, and refused past them. `pending` marks a certified key that is no longer live but has no * revocation record yet (only returned with `includePending`, for writing that record). */ export interface SignerKey { publicKey: Uint8Array; pin: RevocationPins | null; pending?: boolean; } /** Trusted signers by device id. A bare key stands for one live key (a device's own key, for example). */ export type TrustedSigners = ReadonlyMap; /** The keys trusted for `deviceId`, normalized. */ export declare function signerKeys(trusted: TrustedSigners, deviceId: string): readonly SignerKey[]; /** Keys that may vouch for new things (grants, endorsements): live and unpinned. */ export declare const liveKeys: (trusted: TrustedSigners, deviceId: string) => SignerKey[]; /** Master keys by key version. Certificates made under older versions verify under their own key. */ export type MasterKeys = ReadonlyMap; /** * What a client has learned about signer trust, persisted with the pull cursor (same local transaction) * and passed back to every certifiedSigners call. Trust only ever tightens through it: the server can * withhold a record or flip its `live` claims, but it cannot make the client forget what it has seen. * Key ids are `${deviceId}/${hex of the Ed25519 public key}`. */ export interface TrustState { /** * The highest master key version this client holds or has proven (by unlocking it). Only such versions are * persisted: a version the server merely declares is used for one call and never stored, so one bad * response cannot brick the device. Live trust needs a certificate at the current version. */ keyVersion: number; /** The last pins seen per revoked key. A key seen pinned is never live again. */ pins: Record; /** Every key seen as the target of a valid revocation record. Never live again. Sorted. */ revoked: string[]; } export declare const initialTrustState: () => TrustState; /** * How far a declared account key version may exceed the highest version this client holds or has proven. * A rotation bumps the version by one, so an honest server is never more than one ahead of a device that * has seen the previous rotation. A device that missed two rotations sees a server error (and fails closed); * it needs a passphrase unlock anyway, to fetch the new master key. */ export declare const MAX_ROTATION_STEP = 1; export interface TrustEvaluation { signers: Map; /** Persist this with the pull cursor, in the same local transaction. */ state: TrustState; /** * The account's key version is ahead of every master key this client holds: nothing is live. Tell the * user "the account key was rotated: unlock with your passphrase to fetch the new master key". */ keyRotated: boolean; /** * The server declared a key version no rotation can explain (more than MAX_ROTATION_STEP above what this * client holds or has proven). Not a rotation: nothing is persisted, and nothing is live in this call. */ serverError?: 'implausible-key-version'; } /** * Update TrustState after a successful passphrase or recovery-key unlock, the only proof of the account's * real key version. It requires proof of possession: the unwrapped master key must match `stored`'s * verifier (and the unwrap itself already checked the AAD, which binds the user and the version). * - `keyVersion` becomes the proven version, if it is higher (an older genuine wrap never lowers it). * - Seen pins are cleared **only** with `confirmedPinReset: true`, after the user explicitly confirms it (for * example, when a thief's record narrowed a pin while the stolen device was still live). The confirmation * prompt must tell the user to revoke the thief's device FIRST: while it is live, its records still count * and would narrow the pins again. The next * certifiedSigners call then takes pins from the records live devices have signed. * - The seen-revoked set is never cleared: revocation is monotonic. */ export declare function resetTrustStateAfterUnlock(state: TrustState, proof: { masterKey: Uint8Array; stored: Pick; }, opts?: { confirmedPinReset?: boolean; }): TrustState; /** * The trusted signer set, built from `GET /v1/devices/trust`, the master keys the client holds and its * persisted TrustState. Build the set for pulls, checkpoints and grants from this, never from the server's * device list alone. Persist the returned state. The rules (docs/security/e2e-keys.md, "Revocation"): * - A key is certified if a certificate for it verifies under the master key of the certificate's own * version (old history stays verifiable after rotation while the client keeps the old master keys). * - **Current version only for liveness.** The current version is the highest the client holds or has * seen. Only a certificate at that version can make a key live, so a thief with a pre-rotation master key * cannot certify a fresh key into live trust. Older certificates carry pinned history only. * - **Any valid record revokes its target.** A record whose signature verifies under a key certified at the * current version, live, pinned or pending, makes its target non-live for good (TrustState.revoked), * whatever the server claims. Records by old-version-only keys are ignored: such a key can never be live. * - **Pins come only from live signers, and only ever narrow.** A record sets pins only if its signer is live * (a server claim at the current version) and not itself revoked. Several such records: the narrowest pins. * The result is always narrowed with the pins this client has seen (TrustState.pins), so neither a withheld * record nor a wider record served later can widen or drop a pin. A revoked key with no pins, now or ever, * is pending (untrusted; `includePending` returns it for the revocation flow). * - **The current version** is the highest of: the versions the client has seen, the account version the * server declares, and the master keys held. When it is above every held master key, nothing is live and * `keyRotated` is true: tell the user "the account key was rotated: unlock with your passphrase to fetch * the new master key". * * **The caller MUST persist the returned `state`** in the same local transaction as the pull cursor, and pass * it back next time. Nothing in core does this yet (the `cleo cloud` commands will); without it the monotonic * guarantees above do not hold. * - A live key: certified at the current version, called live by the server, never revoked. */ export declare function certifiedSigners(masterKeys: MasterKeys, userId: string, trust: DeviceTrust, state: TrustState, /** * The account's key version as the server declares it (`keyVersion` of GET /v1/account/keys), or null * when the account has no keys yet. Used for this call only, never persisted. It may raise the current * version by at most MAX_ROTATION_STEP above the highest held or proven version: a rotation bumps the * version by one. A higher value is a server error (`serverError`), not a rotation. */ accountKeyVersion: number | null, opts?: { includePending?: boolean; }): TrustEvaluation; /** * Revoke one signing key of a device and pin its history (the POST /v1/devices/:id/revocations body). * The signer must hold a certified key: after losing every other device, the device that just unlocked * with the passphrase certifies itself (a self-grant) and signs this. Compute `pins` from segments and * checkpoints this client has itself verified (revocationPins), never from numbers the server supplies. */ export declare function createDeviceRevocation(args: { userId: string; revoked: { deviceId: string; signingPublicKey: Uint8Array; }; pins: RevocationPins; signer: { deviceId: string; signing: KeyPair; }; }): CreateDeviceRevocationRequest; /** * Grant the master key to a device: seal it to the recipient's X25519 key, certify the recipient under * the master key, and sign the whole grant with the signer's Ed25519 key. The signer must hold the * master key legitimately: a device that unlocked by passphrase or recovery key grants to itself, and a * trusted device grants to another (key rotation). The result is the PUT /v1/devices/:id/key body. */ export declare function createDeviceGrant(args: { masterKey: Buffer; userId: string; keyVersion: number; recipient: DevicePublicKeys; signer: { deviceId: string; signing: KeyPair; }; }): PutDeviceWrappedKeyRequest; /** * Open a master key grant made to this device, after checking it came from a trusted signer. The checks, * in order: the signer is in `trustedSigners` (the device's own signing key for a self-grant, or devices * certified under a master key this device already holds); the signature verifies over a message rebuilt * from this device's OWN keys, so a grant made for other keys fails; the sealed box opens; and the key * inside certifies this device, so the key and the signer's intent agree. The server's verifier is not * consulted: it is server-supplied and proves nothing. */ export declare function openDeviceGrant(args: { userId: string; device: { deviceId: string; encryption: KeyPair; signingPublicKey: Uint8Array; }; grant: PutDeviceWrappedKeyRequest; trustedSigners: TrustedSigners; }): Buffer; export declare const newProjectKey: () => Buffer; export declare function wrapProjectKey(mk: Buffer, pdk: Buffer, projectId: string, keyVersion: number): string; /** * Whether a base64 key wrap may be sent: it has content beyond `=` padding * and decodes to at least one byte. Cleo Nexus refuses an empty or * padding-only `wrappedProjectKey` with 400 (cleo-nexus #35), so the client * checks before every send (T13101). * * @param wrapped - The base64 wrap. */ export declare function isSendableWrap(wrapped: string): boolean; export declare function unwrapProjectKey(mk: Buffer, wrapped: string, projectId: string, keyVersion: number): Buffer; /** Key for the personal home stream. It is derived, so it never needs storing or sharing. */ export declare const homeStreamKey: (mk: Buffer) => Buffer; //# sourceMappingURL=keys.d.ts.map