Skip to main content

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.

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'),
},
},
})
info

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'),
},
},
})
info

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:

  1. Generate a new key (see generating keys).

  2. Set the new key as current and the old one as legacy, and deploy. New writes use current; existing values still read through the legacy fallback.

  3. Once every server in an environment encrypts with the new current, run reencryptEncryptedColumns() 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.

  4. Drop legacy and 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.