Skip to content

Encryption ​

@routier/encryption provides AES-GCM encryption as a schema transform. It is not a database plugin and has no relationship to file storage: it transforms selected property values before any IDbPlugin stores them.

Setup ​

bash
npm install @routier/encryption
ts
import { createKeyring, encryption } from "@routier/encryption";

const keyring = await createKeyring({
  activeKeyId: "k2",
  keys: { k1: oldSecret, k2: currentSecret },
});

const userSchema = s.define("users", {
  id: s.string().key(),
  email: s.string(),
  notes: s.string(),
}).modify(x => ({
  email: x.transform(encryption(keyring, { searchable: true })),
  notes: x.transform(encryption(keyring)),
})).compile();

Use the resulting schema with memory, SQLite, PostgreSQL, MySQL, MongoDB, Dexie, replication, or another plugin as usual.

Randomized versus searchable ​

ModeStored equality leaks?Database equality filters?
encryption(keyring)No; a fresh IV produces different ciphertext for equal valuesNo
encryption(keyring, { searchable: true })Yes; equal values produce equal ciphertextYes, equality only

Deterministic searchable encryption reveals which rows share a value and how often it occurs. Use it only for high-cardinality lookup values such as email. Do not use it for salary, diagnosis, status, or other low-cardinality/sensitive values where frequency is revealing.

Keys and rotation ​

Secrets must contain at least 32 bytes of entropy. Every envelope records the key ID. Rotate by adding a new active key while retaining old keys until all old rows have been rewritten:

ts
const keyring = await createKeyring({
  activeKeyId: "k3",
  keys: { k1: oldest, k2: previous, k3: current },
});

Removing a key that stored rows still reference makes those rows fail on read. isEnvelope(value) identifies this package's stored format.

Supported property values ​

Strings, numbers, booleans, dates, and objects are encoded with their type and restored after decryption. The database sees ciphertext text; your application receives the original type.

Custom transforms ​

Encryption is one use of the general transform API. Compression, redaction, and custom codecs use the same boundary:

ts
.modify(x => ({
  payload: x.transform({
    stores: SchemaTypes.String,
    to: value => compress(value),
    from: stored => decompress(stored),
  }),
}))

Transforms may be async. They replace an existing property, preserve its application type, and declare the storage type.

Released under the MIT License.