Internal
Psychic and Dream both use the Encrypt class under the hood for various operations. Configurations for these internal uses are provided in the conf/app.ts and conf/dream.ts files.
Cookie encryption
By default, Psychic encrypts any cookies that are created through its cookie api. The encryption algorithm and key for this can be set in conf/app.ts, like so:
// conf/app.ts
psy.set('encryption', {
cookies: {
current: {
algorithm: 'aes-256-gcm',
key: AppEnv.string('APP_ENCRYPTION_KEY'),
},
},
})
If you are in the middle of key rotation and need to temporarily provide multiple keys for decryption, you can do that as well:
// conf/app.ts
psy.set('encryption', {
cookies: {
current: {
algorithm: 'aes-256-gcm',
key: AppEnv.string('APP_ENCRYPTION_KEY'),
},
legacy: {
algorithm: 'aes-128-gcm',
key: AppEnv.string('APP_LEGACY_ENCRYPTION_KEY'),
},
},
})
For more information on utilizing cookies, see the Cookie guides.
Encrypted decorator
Within Dream, an Encrypted decorator is provided, which will automatically encrypt any values that are bound to its call. It encrypts with the column keys set in conf/dream.ts; a new app's conf/dream.ts sets them for you from COLUMN_ENCRYPTION_KEY:
// conf/dream.ts
app.set('encryption', {
columns: {
current: {
algorithm: 'aes-256-gcm',
key: AppEnv.string('COLUMN_ENCRYPTION_KEY'),
},
},
})
If you are in the middle of key rotation and need to temporarily provide multiple keys for decryption, you can do that as well:
// conf/dream.ts
app.set('encryption', {
columns: {
current: {
algorithm: 'aes-256-gcm',
key: AppEnv.string('COLUMN_ENCRYPTION_KEY'),
},
legacy: {
algorithm: 'aes-128-gcm',
key: AppEnv.string('LEGACY_COLUMN_ENCRYPTION_KEY'),
},
},
})
For more information on the @deco.Encrypted decorator, see the Encrypted guides
Rotating column encryption keys
Values in @deco.Encrypted columns stay encrypted under whichever key wrote them, so before dropping legacy, re-encrypt the existing rows under the new current key. Rotate in this order:
-
Generate a new key (see generating keys).
-
Set the new key as
currentand the old one aslegacy, and deploy. New writes usecurrent; existing values still read through thelegacyfallback. -
Once every server in an environment encrypts with the new
current, runreencryptEncryptedColumns()there for every model with encrypted properties, from the console or a script:await User.reencryptEncryptedColumns()It walks every row of the model's table (including rows default scopes such as soft delete hide, and STI siblings), and rewrites each value under the current key, without running model hooks or changing
updatedAt. Run it in each environment separately rather than from a migration: each environment rotates its keys on its own schedule, while a migration runs everywhere with the code. -
Drop
legacyand deploy again.
Once legacy is dropped, reading a row still under the old key throws DecryptionError.
While legacy is configured, the encryption:legacy-key-used hook fires each time reading an @deco.Encrypted property needed legacy to open the value. A value nobody reads never fires it, so a quiet hook alone doesn't show that legacy is safe to drop; run reencryptEncryptedColumns() regardless.
For cookies, keep legacy for at least the cookie maxAge, so in-flight sessions aren't forced to sign in again.