export default class Encrypt { static encrypt(data: any, { algorithm, key }: EncryptOptions): string; /** * Decrypts a value previously produced by {@link Encrypt.encrypt}. * * Behavior depends on whether `legacyOpts` is provided: * * **Two-arg form** (no rotation): * - `null`/`undefined` input returns `null`. * - Cipher op / auth tag / payload-shape failure throws `DecryptionError`. * - Successful decrypt with non-JSON plaintext throws `DecryptionParseError`. * * **Three-arg form** (rotation): tries the current key first; on * `DecryptionError` falls back to the legacy key. If both fail, throws * `DecryptionRotationError` carrying both per-key errors. A * `DecryptionParseError` from the current key is **not** retried — the * cipher already matched, so a parse failure means the encrypted format * is wrong (an app bug), not a wrong key. * * `MissingEncryptionKey` propagates from either form when a key is missing. * * **`onLegacyKeyUsed`** (optional fourth argument): during a key rotation, * tells you which values still need the legacy key. It is called with no * arguments, synchronously, before a three-arg call returns a value that * the legacy key opened. It is not called when the current key opens the * value, when decryption throws, or in the two-arg form. An error it * throws propagates from `decrypt` as is. * * ```ts * const value = Encrypt.decrypt( * encrypted, * { algorithm: 'aes-256-gcm', key: newKey }, * { algorithm: 'aes-256-gcm', key: oldKey }, * { onLegacyKeyUsed: () => console.warn('this value is still encrypted with the old key') } * ) * ``` * * @throws MissingEncryptionKey * @throws DecryptionError * @throws DecryptionParseError * @throws DecryptionRotationError */ static decrypt(encrypted: string, { algorithm, key }: DecryptOptions, legacyOpts?: DecryptOptions, { onLegacyKeyUsed }?: DecryptCallbacks): RetType | null; /** * Opens `encrypted` as {@link Encrypt.decrypt} does, with `currentOpts` and, * when they do not open it, `legacyOpts`, and encrypts the value again with * `currentOpts`, so that reading it no longer needs the legacy key. Use it * during a key rotation for a value stored outside an `@deco.Encrypted` * column, such as a cookie; `Model.reencryptEncryptedColumns()` does this * for every value of a model's encrypted properties. * * ```ts * const rewritten = Encrypt.reencrypt( * storedValue, * { algorithm: 'aes-256-gcm', key: newKey }, * { algorithm: 'aes-256-gcm', key: oldKey } * ) * ``` * * @throws MissingEncryptionKey * @throws DecryptionError * @throws DecryptionParseError * @throws DecryptionRotationError */ static reencrypt(encrypted: string, currentOpts: EncryptOptions, legacyOpts?: DecryptOptions): string; private static attemptDecryptionWithLegacyKeys; /** * Generates a base64-encoded random key suitable for the given algorithm. * * ## Rotation workflow * * 1. Generate a new key: `const newKey = Encrypt.generateKey('aes-256-gcm')`. * 2. Configure rotation by setting both `current` and `legacy`. For * encrypted columns: `dreamApp.set('encryption', { columns: { current: * { algorithm: 'aes-256-gcm', key: newKey }, legacy: { algorithm: * 'aes-256-gcm', key: oldKey } } })`. For cookies, use the equivalent * shape under `psychicApp.set('encryption', { cookies: { current, * legacy } })`. * 3. Deploy. New encryptions use `current`; existing ciphertext continues * to decrypt via `legacy` fallback. * 4. For cookies, wait at least the cookie `maxAge` so all in-flight * cookies have either expired or been re-issued under the new key. For * `@Encrypted` columns, re-encrypt every existing row under the new key * by calling `reencryptEncryptedColumns()` on each model that has * encrypted properties (`await User.reencryptEncryptedColumns()`), in * each environment, from a console or a script rather than a migration. * Run it only once every server in that environment runs with the new * `current`: a server still on the old `current` keeps writing old-key * values that its single pass over the table can miss. Writing old-key * ciphertext straight into a backing column (`setAttribute`, * `setAttributes` or `updateAttributes` given the backing column) after * it has run also puts an old-key value back. For any other value * encrypted with these keys, {@link Encrypt.reencrypt} rewrites it. * 5. Drop `legacy` from config and deploy again. * * To see which values still need the old key while `legacy` is * configured, register `dreamApp.on('encryption:legacy-key-used', ...)`, * which fires each time reading an `@Encrypted` property needed `legacy` * to open the value, or pass `onLegacyKeyUsed` to {@link Encrypt.decrypt} * for values you decrypt yourself. A value nobody reads never fires * either, so silence alone does not show that `legacy` is safe to drop. * * ## When to rotate * * - On a scheduled cadence (90–180 days is a reasonable policy default). * - Incident response: leaked env file, departing employee with key * access, or any suspected key compromise. * * ## How long to keep `legacy` * * - For cookies: at least the cookie `maxAge`, so in-flight sessions are * not forced to re-authenticate. * - For `@Encrypted` columns: until every existing row has been * re-encrypted under the new key. Once `legacy` is dropped, reading the * `@Encrypted` property of a row that is still under the old key throws * `DecryptionError`, the same error a tampered or corrupted value * throws, so the error class alone cannot show that `legacy` was * dropped too early. `DecryptionRotationError` is thrown only while * `legacy` is configured and neither key opens the value. Neither class * extends the other, so code that catches `DecryptionRotationError` * during a rotation must also catch `DecryptionError` to handle such a * read once `legacy` is dropped. */ static generateKey(algorithm: EncryptAlgorithm): string; static validateKey(base64EncodedKey: string, algorithm: EncryptAlgorithm): boolean; } export interface EncryptOptions extends BaseOptions { } export interface DecryptOptions extends BaseOptions { } export interface DecryptCallbacks { /** * Called with no arguments, synchronously, before a three-arg * {@link Encrypt.decrypt} returns a value that the legacy key opened. * Never called when the current key opens the value, when decryption * throws, or in the two-arg form. */ onLegacyKeyUsed?: () => void; } interface BaseOptions { algorithm: EncryptAlgorithm; key: string; } export type EncryptAESAlgorithm = 'aes-256-gcm' | 'aes-192-gcm' | 'aes-128-gcm'; export type EncryptAlgorithm = EncryptAESAlgorithm; export type EncryptAESBitSize = 256 | 192 | 128; export interface PsychicEncryptionPayload { ciphertext: string; tag: string; iv: string; } export {};