/** * Encrypted keystore for DKG node private keys. * * Encrypts key material at rest using AES-256-GCM with a key derived from a * user passphrase via scrypt. Compatible with the Ethereum keystore V3 pattern * but simplified for our use case. * * Usage: * const ks = await encryptKeystore(privateKeyHex, passphrase); * await writeFile('keystore.json', JSON.stringify(ks)); * ... * const key = await decryptKeystore(ks, passphrase); */ export interface EncryptedKeystore { version: 1; crypto: { cipher: 'aes-256-gcm'; ciphertext: string; iv: string; tag: string; kdf: 'scrypt'; kdfparams: { n: number; r: number; p: number; dklen: number; salt: string; }; }; /** Hex-encoded SHA-256 of the derived key, used for quick passphrase validation. */ id: string; } /** * Single owner of the scrypt KDF policy: production parameters, lower and upper * bounds, the derived execution budget, and the cost validation that guards it. * Encryption, derivation, validation, and the tests all consume THIS object — * there is deliberately no second representation to drift from it. * * Lower bounds (CLI-1): what we MUST enforce on the (untrusted) `kdfparams` * block before deriving a key. Without these, an attacker who can write a * keystore file can advertise toy scrypt parameters (e.g. N=256, r=1) and force * the loader to brute-force in O(1). Production scrypt minimums per draft RFC * and OWASP cheat-sheet: N ≥ 2^15, r ≥ 8, p ≥ 1, dklen == 32 (AES-256-GCM), * salt ≥ 16 bytes. * * Upper bounds: the counterpart. The minimums stop a keystore advertising a * cost cheap enough to attack; the maximums stop one advertising a cost too * expensive to service, which would let a file exhaust the process before any * passphrase is checked. * * The acceptance ceiling and the execution budget are ONE policy. Splitting * them is what made two prior revisions wrong: * - `deriveKey` passed a hardcoded `maxmem` of 256 MiB while production writes * N=2^18, r=8 — a working set of *exactly* 256 MiB. OpenSSL enforces * `workingSet + overhead <= maxmem`, so the module's own production * parameters failed with `ERR_CRYPTO_INVALID_SCRYPT_PARAMS` ("memory limit * exceeded"). The suite never caught it because it lowers N to 2^15. * - An acceptance ceiling above the execution budget would admit parameters * that then fail inside OpenSSL rather than at our diagnosable boundary. * So `executionMaxmemBytes` is derived from `maxWorkingSetBytes` plus an * allowance for OpenSSL's own overhead (measured at ~0.3% of the working set; * 8 MiB is generous at the ceiling). */ export interface ScryptKdfPolicy { /** Parameters this module writes when creating a keystore. */ readonly production: { readonly n: number; readonly r: number; readonly p: number; }; readonly minN: number; readonly minR: number; readonly minP: number; readonly maxP: number; readonly requiredDklen: number; readonly minSaltBytes: number; /** Largest scrypt working set (128 * N * r bytes) we accept. */ readonly maxWorkingSetBytes: number; /** `maxmem` handed to OpenSSL: the ceiling plus its bounded overhead. */ readonly executionMaxmemBytes: number; /** scrypt's working set — the quantity both the ceiling and OpenSSL bound. */ workingSetBytes(n: number, r: number): number; /** * Throws a diagnosable error for any parameter set we will not service, so * nothing reaches OpenSSL that could only fail there. (Lower bounds live in * `decryptKeystore` beside their established "weak keystore" wording.) */ assertCostWithinLimits(n: number, r: number, p: number): void; } export declare const SCRYPT_KDF_POLICY: ScryptKdfPolicy; /** @internal Allow tests to use lighter scrypt params to avoid memory limits */ export declare function _setScryptN(n: number): void; export declare function encryptKeystore(privateKeyHex: string, passphrase: string): Promise; export declare function decryptKeystore(keystore: EncryptedKeystore, passphrase: string): Promise; export declare function isEncryptedKeystore(obj: unknown): obj is EncryptedKeystore; //# sourceMappingURL=keystore.d.ts.map