/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" /** * Scriptable wrapper over the Lockstore keystore. A single instance per * process is opened against the current profile's keystore file * (lockstore.keys.sqlite). * * KEK references are opaque strings of the form * `lockstore::kek:::`, minted by * `createKek`. A collection is an arbitrary namespace under which a * single DEK (data encryption key) is wrapped by one or more KEKs; * see `createDek`. * * Methods that touch SQLite or run PBKDF2 return a `Promise` and * execute on a private background queue so they do not block the main * thread. The cheap in-memory state check (`isKekUnlocked`) remains * synchronous; `lockKek` and `lock` are also cheap but return a Promise * so the API shape is uniform. */ [scriptable, uuid(a83f5d62-7b1c-4d2e-9f0a-3c5e8b6a1d4e)] interface nsILockstore : nsISupports { /* --- Unified KEK lock / unlock --------------------------------------- */ // // These work for any KEK type that might require user interaction: // // Password → `secret` is the user's password, fed into PBKDF2 // to derive the wrapping key that AES-GCM-unwraps the // stored KEK; the unwrapped KEK is cached in memory // for `timeoutMs`. Required — must not be empty. // Pkcs11Token → `secret` is the PIN. When supplied, Lockstore // authenticates the slot via // `PK11_CheckUserPassword` (direct `C_Login`), // bypassing the NSS password callback. When empty, // Lockstore falls back to `slot.authenticate()`, // which delegates to whatever password callback // the embedding application registered (e.g. PSM // in Firefox). // LocalKey → no-op; `isKekUnlocked` always returns true, // `lockKek`/`unlockKek` succeed without side effects. // // The copy of `secret` handed to the FFI is zeroised once the FFI has // consumed it; callers should still follow their own hygiene rules for // the string they passed in. /** * Unlock `kekRef` so subsequent DEK accesses under it succeed for at * most `timeoutMs` milliseconds. * * The returned Promise resolves with `undefined` on success and rejects * with: * - NS_ERROR_ABORT on wrong secret / PIN. * - NS_ERROR_NOT_INITIALIZED if the KEK requires initialisation that * hasn't been performed yet (e.g. no Password kek_ref minted). * - NS_ERROR_INVALID_ARG on an unrecognised `kekRef`. */ [implicit_jscontext] Promise unlockKek(in AUTF8String kekRef, in ACString secret, in uint32_t timeoutMs); /// Drop any cached authentication for `kekRef`. Subsequent DEK accesses /// will throw NS_ERROR_NOT_AVAILABLE until the caller re-unlocks. For /// PKCS#11 this also calls `PK11_Logout` so NSS's own /// authenticated-slot state is cleared alongside the Lockstore cache. /// The operation is cheap (in-memory cache eviction + best-effort /// `PK11_Logout`), but the Promise shape matches the rest of the API /// so JS callers don't have to special-case it. [implicit_jscontext] Promise lockKek(in AUTF8String kekRef); /// True iff `kekRef` is currently unlocked. A valid-but-unrecognised /// `kekRef` (one that doesn't correspond to any KEK type Lockstore /// knows about) returns `false` rather than throwing. An empty /// `kekRef` throws `NS_ERROR_INVALID_ARG`. boolean isKekUnlocked(in AUTF8String kekRef); /// Lock every KEK that holds cached authentication: zeroises every /// cached Password KEK in memory, clears every PKCS#11 unlock entry, /// and calls `PK11_Logout` on each previously-unlocked slot. Intended /// for shutdown / logout paths. The operation is cheap; the Promise /// shape matches the rest of the API. [implicit_jscontext] Promise lock(); /* --- DEK / collection management ------------------------------------- */ /** * Create a new DEK for `collection`, wrapped under `kekRef`. The * returned Promise rejects with NS_ERROR_FAILURE if the collection * already has a DEK. * * `extractable` controls whether the raw DEK bytes can later be * exported via `getDek`. Use the default (`false`) unless an * external cipher demands raw key material (e.g. mozStorage's * ObfuscatingVFS, which needs a 32-byte key for SQLite page * encryption); `encrypt`/`decrypt` work regardless of `extractable`. */ [implicit_jscontext] Promise createDek(in AUTF8String collection, in AUTF8String kekRef, in boolean extractable); /** * Install caller-supplied `dekBytes` as the DEK for `collection`, * wrapped under the existing `kekRef`. Used for migrating data * already encrypted under a known external DEK into the keystore- * managed model without re-encrypting at rest. * * `dekBytes` must be 32 bytes (AES-256-GCM default cipher suite). * The Promise rejects with: * - NS_ERROR_INVALID_ARG on wrong length, or empty `collection` * / `kekRef` * - NS_ERROR_NOT_AVAILABLE if `kekRef` doesn't exist or is locked * - NS_ERROR_FAILURE if `collection` already has a DEK * * Note: imported DEKs are inherently extractable by the caller (the * bytes are already in their hands). The `extractable` flag controls * only whether future `getDek` calls succeed on this DEK. */ [implicit_jscontext] Promise importDek(in AUTF8String collection, in AUTF8String kekRef, in Array dekBytes, in boolean extractable); /** * Resolves with `true` iff the DEK for `collection` was created * with `extractable = true`. Touches SQLite (loads the DEK metadata * row), hence async — distinct from `isKekUnlocked`, which is an * in-memory cache check. * * Rejects with NS_ERROR_NOT_AVAILABLE if `collection` doesn't exist. */ [implicit_jscontext] Promise isDekExtractable(in AUTF8String collection); /** * Delete the DEK for `collection`. Rejects with * NS_ERROR_NOT_AVAILABLE if no DEK exists. The keystore does not * track any associated datastore; callers are responsible for * disposing of ciphertext under this collection by other means. */ [implicit_jscontext] Promise deleteDek(in AUTF8String collection); /// Resolves with an Array of every collection currently /// managed by this keystore. [implicit_jscontext] Promise listDeks(); /// Resolves with an Array of every kekRef that /// currently wraps the DEK named `dekName`. The array is non-empty /// for any DEK that exists (the keystore enforces at least one KEK /// wrapping); the Promise rejects with NS_ERROR_NOT_AVAILABLE if no /// DEK by that name exists (including the empty string). Returns /// only the kekRef strings, never the wrapped key bytes themselves /// — useful for callers that need to discover the wrapping state /// (e.g. login crypto deciding whether to encrypt under LocalKey or /// Password for a given DEK). [implicit_jscontext] Promise listKeks(in AUTF8String dekName); /// Wrap an existing collection's DEK under an additional `kekRef`. [implicit_jscontext] Promise addKek(in AUTF8String collection, in AUTF8String fromKekRef, in AUTF8String toKekRef); /// Remove a `kekRef` wrapping from a collection. The last remaining /// wrapping cannot be removed. [implicit_jscontext] Promise removeKek(in AUTF8String collection, in AUTF8String kekRef); /** * Atomically rewrap the DEK for `collection` from `oldKekRef` to * `newKekRef`. The DEK bytes are unchanged, so ciphertexts at rest * under this collection remain valid. Equivalent in effect to * `addKek(collection, oldKekRef, newKekRef)` followed by * `removeKek(collection, oldKekRef)` but atomic at the kvstore-row * level — a crash mid-operation leaves the keystore in the old * state or the new state, never a transient half-state. * * `oldKekRef` must currently wrap the collection and be unlocked. * The Promise rejects with: * - NS_ERROR_INVALID_ARG if `oldKekRef` == `newKekRef`, or either * is empty * - NS_ERROR_NOT_AVAILABLE if `collection` or its `oldKekRef` * wrapping doesn't exist * - NS_ERROR_FAILURE if `newKekRef` already wraps this collection * - NS_ERROR_NOT_AVAILABLE (`Locked`) if `oldKekRef` is locked */ [implicit_jscontext] Promise switchKek(in AUTF8String collection, in AUTF8String oldKekRef, in AUTF8String newKekRef); /* --- Crypto (async, byte-oriented) ----------------------------------- */ /** * Encrypt `plaintext` with the DEK for `(collection, kekRef)`. The work * runs off the main thread. The returned Promise resolves with a byte * array (Array) containing a self-describing blob: * [cipher_suite_id(1)] || [nonce] || [ciphertext+tag] * and rejects with an nsresult on failure. The DEK need not be * extractable. */ [implicit_jscontext] Promise encrypt(in AUTF8String collection, in AUTF8String kekRef, in Array plaintext); /** * Decrypt a blob produced by `encrypt`. Cipher suite is inferred from * the blob's leading byte. Runs off the main thread; the Promise * resolves with the plaintext bytes. */ [implicit_jscontext] Promise decrypt(in AUTF8String collection, in AUTF8String kekRef, in Array ciphertext); /** * Return the raw DEK bytes for `(collection, kekRef)` as an * `Array` (32 bytes for the default AES-256-GCM / * ChaCha20-Poly1305 suites). The DEK must have been created with * `extractable = true`; otherwise the Promise rejects with * `NS_ERROR_NOT_AVAILABLE`. * * The returned bytes are sensitive: any caller is now in possession * of the symmetric key that can decrypt every ciphertext stored * under this DEK. Prefer `encrypt`/`decrypt` unless an external * cipher (e.g. mozStorage's ObfuscatingVFS, which needs a 32-byte * key for SQLite page encryption) requires raw key material. * * The Promise also rejects with `NS_ERROR_NOT_AVAILABLE` if no DEK * exists for `(collection, kekRef)`, and with `NS_ERROR_INVALID_ARG` * if either argument is empty. */ [implicit_jscontext] Promise getDek(in AUTF8String collection, in AUTF8String kekRef); /* --- KEK creation --------------------------------------------------- */ /** * Generic KEK-creation entry point. Always mints a fresh kek_ref of * the form `lockstore::kek:::`; a profile * can host any number of KEKs per `KekType`. Dispatches on `kekType`: * * "local" * Generates a fresh AES-256 KEK and persists it (verbatim) in * a `LocalKekRecord`. LocalKey has no unlock ceremony — its * confidentiality at rest is provided by the underlying SQLite * encryption layer. `secret` and `cacheTimeoutMs` are ignored. * * "password" * Wraps a fresh AES-256 KEK under PBKDF2-HMAC-SHA256 of * `secret` (must be non-empty). Each kek_ref carries an * independent salt + iteration count + ciphertext, so multiple * password KEKs coexist without shared state. Rotation is * `createKek("password", new)` + `switchKek(oldKekRef, newKekRef)` * + `removeKek(oldKekRef)`. If `cacheTimeoutMs` is non-zero the * just-derived KEK is also inserted into the auth cache with * that expiry, so callers can use the returned kek_ref without * an immediate `unlockKek`. * * "pkcs11" * Provisions a fresh PKCS#11-backed KEK against the slot * named by the PKCS#11 URI in `secret`. The slot is * authenticated via NSS's registered password callback (PSM * in Firefox); Lockstore then finds-or-creates a long-lived * AES wrapping key on the slot, generates a fresh software * KEK, wraps it under the wrapping key, and persists a * record. `cacheTimeoutMs` is ignored — PKCS#11 unlock is * mediated by NSS, not by the Lockstore cache. * * `identifier` selects the kek_ref `` suffix: empty (the usual * case) mints a fresh random id; a non-empty base64url * (`[A-Za-z0-9_-]`) identifier is used verbatim, making the call a * deterministic get-or-create -- a later `createKek` with the same * `kekType` + `identifier` returns the existing KEK untouched. * * `cacheTimeoutMs` is the duration (in milliseconds) that the * just-derived KEK is kept in the in-memory auth cache. Only * meaningful for the `"password"` tier; ignored elsewhere. * * Lockstore copies the secret bytes into its own buffer, consumes * them, and zeroises the buffer before resolving the Promise; the * caller's `ACString` is never mutated. * * The returned Promise resolves with the freshly-minted `kek_ref` * the caller should hand to subsequent `createDek` / `encrypt` * calls, and rejects with: * - NS_ERROR_INVALID_ARG on an unknown `kekType`, a non-base64url * `identifier`, an empty Password `secret`, or a malformed * PKCS#11 URI. * - NS_ERROR_ABORT if a PKCS#11 token unlock prompt is cancelled. * - NS_ERROR_FAILURE on any other failure (SQLite write error, * crypto-layer error). */ [implicit_jscontext] Promise createKek(in ACString kekType, in ACString identifier, in ACString secret, in uint32_t cacheTimeoutMs); /** * Destroy the KEK referenced by `kekRef`. The persisted row is * dropped along with the per-`kekRef` unlock-cache entry — the * authenticated KEK bytes derived from a password / PIN that * `unlockKek` cached and that `lockKek` would otherwise clear. No * DEK material is touched: every DEK wrapping under `kekRef` must * already be gone (via `removeKek` / `switchKek`) before this call * succeeds. For a PKCS#11 KEK the slot is logged out via the same * `PK11_Logout` path used by `lockKek`. * * Deletion is always explicit. `removeKek` and `deleteDek` drop * wrappings only; the per-`kekRef` KEK record stays on disk until * `deleteKek` is called for it. * * The returned Promise resolves with `undefined` on success and * rejects with: * - NS_ERROR_INVALID_ARG on an empty `kekRef`. * - NS_ERROR_NOT_AVAILABLE if no record exists at `kekRef`. * - NS_ERROR_FAILURE if any DEK still wraps under `kekRef`, or * on SQLite / crypto-layer errors. */ [implicit_jscontext] Promise deleteKek(in AUTF8String kekRef); };