/** * Credential encryption — AES-256-GCM with device-bound key derivation. * * Every key derives from the 32-byte `machine-key` in the CLEO home. That key * NEVER leaves the device; credentials cross devices only through the * passphrase-sealed path in `store/credential-transfer.ts`. * * Two KDFs: * * - **Project** (T12326): `HMAC-SHA256(machine-key, "cleo:project-credential:v2\0" * ‖ projectId)`, ciphertext version `0x02`. Keyed by project IDENTITY, so a * moved or renamed project still decrypts. The pre-T12326 path-bound KDF * (`HMAC-SHA256(machine-key, projectPath)`, version `0x01`) is read-only and * migrated on first successful decrypt ({@link decryptProjectSecret}). * - **Global** (ADR-037 §5): `HMAC-SHA256(machine-key ‖ global-salt, id)`, * version `0x01`, for project-independent credentials. * * @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 3.6 * @module crypto/credentials */ /** * Absolute path to the machine key file: `/machine-key`. * * Resolved through {@link getCleoHome} — the same root as `global-salt` and * the agent registry's key reader — so `CLEO_HOME` relocates all of them * together. (Before T12326 this function re-derived the XDG path itself and * ignored `CLEO_HOME`, so a relocated home read the salt from one directory * and the key from another.) On every platform the default resolves to the * same file as before. * * The machine key is DEVICE-LOCAL by design: it is never exported in any * backup bundle. Credentials move between devices through * `store/credential-transfer.ts` instead. * * @param cleoHome - CLEO home to resolve against (defaults to the ambient home). * @returns Absolute path to the machine key. * @task T12326 */ export declare function getMachineKeyPath(cleoHome?: string): string; /** * Options shared by every credential encrypt/decrypt call. * * @task T12326 */ export interface CredentialKeyOptions { /** * CLEO home whose `machine-key` (and `global-salt`, for the global KDF) keys * the operation. Defaults to the ambient home. A backup import passes the * home it is restoring INTO, which need not be the process's own; an explicit * home also reads the salt fresh instead of from the process memo. */ readonly cleoHome?: string; } /** * Ciphertext version written by the pre-T12326 project KDF, * `HMAC-SHA256(machine-key, projectPath)`. Read-only: decrypted during * migration, never written. */ export declare const PROJECT_CIPHERTEXT_VERSION_LEGACY_PATH = 1; /** * Ciphertext version written by the project-identity KDF, * `HMAC-SHA256(machine-key, "cleo:project-credential:v2\0" ‖ projectId)`. */ export declare const PROJECT_CIPHERTEXT_VERSION_PROJECT_ID = 2; /** * Which KDF decrypted a project credential. * * @task T12326 */ export type ProjectCredentialKdf = 'project-id' | 'legacy-path'; /** * Key context for {@link decryptProjectSecret}. * * @task T12326 */ export interface ProjectSecretContext extends CredentialKeyOptions { /** * Stable project identity — the declared id (`.cleo/project.json`, the * legacy `.cleo/project-id`, else the `project-info.json` cache). Travels * with the project directory, so a moved or renamed checkout derives the * same key. */ readonly projectId: string; /** * Ids this project was keyed by before (T12716): the `project-info.json` * cache when it disagrees with the tracked id, and its * `previousProjectIds` receipts. A project-id ciphertext sealed under one * of them is opened and re-wrapped under {@link ProjectSecretContext.projectId}, * so re-keying a project never strands its credentials. */ readonly previousProjectIds?: readonly string[]; /** * Candidate project paths for ciphertexts still under the legacy path KDF * (typically the current root plus the path the registry last recorded). * Tried in order; unused for project-id ciphertexts. */ readonly legacyProjectPaths?: readonly string[]; } /** * Result of {@link decryptProjectSecret}. * * @task T12326 */ export interface ProjectSecretReadResult { /** The decrypted secret. */ readonly plaintext: string; /** Which KDF opened it. */ readonly kdf: ProjectCredentialKdf; /** * When the ciphertext was legacy (path-bound): the same secret re-encrypted * under the project-id KDF. The caller persists it in place of the old * ciphertext. `null` when the ciphertext was already current. */ readonly rewrapped: string | null; } /** * Encrypt a project-scoped secret under the project-identity KDF. * * The key is `HMAC-SHA256(machine-key, "cleo:project-credential:v2\0" ‖ * projectId)`: bound to this device (machine-key) and to the project's * identity, but NOT to where the project directory lives. Moving or renaming * the checkout keeps the key; a different device does not (see * `store/credential-transfer.ts` for the portable path). * * @param plaintext - The secret (e.g. an agent API key). * @param projectId - Stable project identity. * @param options - Key options (which CLEO home's machine-key). * @returns Base64 ciphertext, version byte {@link PROJECT_CIPHERTEXT_VERSION_PROJECT_ID}. * @task T12326 */ export declare function encryptProjectSecret(plaintext: string, projectId: string, options?: CredentialKeyOptions): Promise; /** * Decrypt a project-scoped secret, migrating legacy ciphertexts on the way. * * - Version `0x02` ciphertexts open with the project-identity key; one sealed * under a `previousProjectIds` entry is opened and re-wrapped (T12716). * - Version `0x01` (legacy, path-bound) ciphertexts are tried against each of * `legacyProjectPaths`; the first that authenticates wins and the secret is * re-encrypted under the project-identity KDF and returned as `rewrapped` * for the caller to persist. The legacy ciphertext is never needed again. * * @param ciphertext - Base64 ciphertext from either KDF. * @param context - Project identity plus legacy path candidates. * @returns The plaintext, which KDF opened it, and any re-wrapped ciphertext. * @throws {Error} If no key authenticates. The message names the paths tried * so the caller can report which credential to re-enter. * @task T12326 */ export declare function decryptProjectSecret(ciphertext: string, context: ProjectSecretContext): Promise; /** * The machine key and global salt of one CLEO home: the inputs of the global * KDF. Held only in memory, only as long as one operation needs it. * * @task T12867 */ export interface GlobalKeyMaterial { /** The 32-byte `machine-key`. */ readonly machineKey: Buffer; /** The 32-byte `global-salt`. */ readonly globalSalt: Buffer; } /** * Load the global KDF inputs of a CLEO home. * * With `create: false` nothing is ever written: a missing `machine-key` or * `global-salt` returns `null`, so a read path never mints key material * (T12867 review N1). With `create: true` both are created on first use, * exclusively (N2). * * @param options - The CLEO home, and whether missing material may be created. * @returns The material, or `null` when it is missing and `create` is false. * @throws {Error} When a key file exists but is unsafe (permissions, length) or unreadable. * @task T12867 */ export declare function loadGlobalKeyMaterial(options: CredentialKeyOptions & { readonly create: boolean; }): Promise; /** * Derive the global KDF key for `id`: `HMAC-SHA256(machine-key || global-salt, id)` * (ADR-037 §5), the same key {@link encryptGlobal} uses. * * @param material - The home's key material. * @param id - The identity binding the ciphertext. * @returns A 32-byte AES-256 key. * @task T12867 */ export declare function deriveGlobalKeyFromMaterial(material: GlobalKeyMaterial, id: string): Buffer; /** * Encrypt with an already-derived global key, binding `associatedData` * (authenticated, not encrypted) into the GCM tag. Same framing as * {@link encryptGlobal}. * * @param plaintext - The secret. * @param key - From {@link deriveGlobalKeyFromMaterial}. * @param associatedData - Context the ciphertext is bound to (for example a field path). * @returns Base64 ciphertext. * @task T12867 */ export declare function sealWithGlobalKey(plaintext: string, key: Buffer, associatedData: string): string; /** * Decrypt a {@link sealWithGlobalKey} ciphertext. * * @param ciphertext - Base64 ciphertext. * @param key - From {@link deriveGlobalKeyFromMaterial}. * @param associatedData - The same context it was sealed with. * @returns The plaintext, or `null` when the ciphertext is malformed, of an * unknown version, or fails authentication (wrong key, id or context). * @task T12867 */ export declare function openWithGlobalKey(ciphertext: string, key: Buffer, associatedData: string): string | null; /** * Encrypt a plaintext string using AES-256-GCM with a global, machine-bound key. * * Output format is byte-identical to {@link encrypt}: * base64(version + iv + ciphertext + authTag) * - version: 1 byte (0x01 = AES-256-GCM) * - iv: 12 bytes * - ciphertext: variable length * - authTag: 16 bytes * * The encryption key is derived via {@link deriveGlobalKey} (machine-key + * global-salt + `id`), so the resulting ciphertext decrypts from any project * on the same machine — unlike the project-bound {@link encrypt}. * * @param plaintext - The string to encrypt (e.g. an LLM API key). * @param id - The stable identity used for key derivation (e.g. an agentId). * @param options - Key options (which CLEO home's machine-key + salt). * @returns Base64-encoded ciphertext. * * @see decryptGlobal — the inverse operation. * @task T11710 */ export declare function encryptGlobal(plaintext: string, id: string, options?: CredentialKeyOptions): Promise; /** * Decrypt a base64-encoded ciphertext produced by {@link encryptGlobal} using * the global, machine-bound key derived from `id`. * * @param ciphertext - Base64-encoded string from {@link encryptGlobal}. * @param id - The identity used at encryption time. A mismatched `id` derives a * different key and fails the GCM auth tag (throws). * @param options - Key options (which CLEO home's machine-key + salt). * @returns The original plaintext string. * @throws If decryption fails (wrong id, corrupted data, or machine-key/ * global-salt mismatch). * * @see encryptGlobal — the inverse operation. * @task T11710 */ export declare function decryptGlobal(ciphertext: string, id: string, options?: CredentialKeyOptions): Promise; /** * Synchronous {@link decryptGlobal}, for read paths that cannot await (the * LLM credential resolver, T13715). Never creates key material: a missing * `machine-key` throws `ENOENT`, as no ciphertext can exist without it. * * @param ciphertext - Base64-encoded string from {@link encryptGlobal}. * @param id - The identity used at encryption time. * @param options - Key options (which CLEO home's machine-key + salt). * @returns The original plaintext string. * @throws If the key is missing or unsafe, or decryption fails. * @task T13715 */ export declare function decryptGlobalSync(ciphertext: string, id: string, options?: CredentialKeyOptions): string; //# sourceMappingURL=credentials.d.ts.map