Skip to main content
Version: 2.x

Encrypting Fields (Preview)

This awesome feature is contributed by Eugen Istoc and inspired by prisma-field-encryption.

ZenStack's field encryption feature helps you add an extra layer of protection to sensitive data stored in your database.

Basic Usage

To use the feature, simply mark the fields you want to encrypt with the @encrypted attribute in ZModel:

model User {
id String @id @default(cuid())
someSecret String @encrypted
}

When calling enhance() to create an enhanced PrismaClient, you need to pass an extra encryption settings to provide the encryption key if you use the default encryption:

function getEncryptionKey(): Uint8Array {
// return a 32-byte key
}

const db = enhance(prisma, { user }, {
encryption: {
encryptionKey: getEncryptionKey()
}
});

Or, if you choose to use custom encryption, pass your custom encrypt and decrypt functions:

async function myEncrypt(model: string, field: FieldInfo, plain: string) {
...
}

async function myDecrypt(model: string, field: FieldInfo, cipher: string) {
...
}

const db = enhance(prisma, { user }, {
encryption: {
encrypt: myEncrypt,
decrypt: myDecrypt
}
});

The encryption feature requires the "encryption" enhancement kind to be enabled. If you manually specify enhancement kinds, make sure it's included:

const db = enhance(prisma, { user }, {
kinds: ['policy', 'encryption'],
...
});

When you use the enhanced PrismaClient, the encryption and decryption process happens transparently:

  • When writing, the field value will be encrypted before being stored in the database.
  • When reading, the field value will be decrypted before being returned to you.

Default Encryption

The default encryption uses the AES-GCM algorithm to encrypt and decrypt the data with a 32-byte symmetric key. A random 12-byte IV is generated and used for each encryption operation, ensuring the encryption result is non-deterministic.

The data stored in the encrypted field consists of the following two parts (both base-64 encoded and joined with a "."):

  1. Metadata object with the following fields:
    • Version byte (v): indicating the encryption version
    • Algorithm (a): AES-GCM
    • Key digest (k): a digest of the encryption key used
  2. Encrypted data: IV bytes concatenated with the encrypted data bytes

Key Rotation

Key rotation is a technique for enhancing security by periodically changing the encryption key. The default encryption supports key rotation by allowing you to specify multiple decryption keys:

const db = enhance(prisma, { user }, {
encryption: {
encryptionKey: encryptionKey,
decryptionKeys: [oldKey1, oldKey2, ...]
}
});

When reading an encrypted field, the decryption key with a digest matching the encryption key digest (extracted from the encrypted data) will be used to decrypt the data. In the rare case when multiple keys have matching digests, each key will be tried in the order they are provided until the data is successfully decrypted.

Please note that the encryptionKey setting value will be automatically used as a decryption key, so you don't need to include it in the decryptionKeys setting.

FIELDS FAIL TO DECRYPT

When a field value fails to decrypt, the default encryption returns the cipher text as is. This allows incremental adoption of encryption, as you can enable the feature and then asynchronously encrypt existing data. During this period, plain and encrypted data will coexist in the same field.

Let us know if you have security concerns about this behavior, and we can consider making it configurable in the future.

Data Migration

When you enable encryption on an existing field with data, you need to migrate the old data into encrypted form. Although ZenStack doesn't have a built-in data migration feature, it provides the infrastructure needed to implement a script.

1. Asynchronously encrypt existing data

As mentioned in the previous section, the way the default encryption handles decryption failures allows you to encrypt existing data asynchronously without taking your service down.

2. Using the default encrypter

The @zenstackhq/runtime package exports an Encrypter class that you can use to encrypt data into the format compatible with what the enhanced PrismaClient expects:


import { Encrypter } from '@zenstackhq/runtime/encryption';

async function main() {
const encrypter = new Encrypter(encryptionKey);

let row = await getNextRow();
while (row) {
row.someSecret = await encrypter.encrypt(row.someSecret);
await saveRow(row);
row = await getNextRow();
}
}

main();

Custom Encryption

You can implement a custom encryption/decryption by providing your own encrypt and decrypt functions. By doing so, you'll be fully responsible for managing keys, encoding and decoding the encrypted data and implementing key rotation.

async function myEncrypt(model: string, field: FieldInfo, plain: string) {
...
}

async function myDecrypt(model: string, field: FieldInfo, cipher: string) {
...
}

const db = enhance(prisma, { user }, {
encryption: {
encrypt: myEncrypt,
decrypt: myDecrypt
}
});

Limitations

  • Only string fields are supported for encryption.
  • Encrypted fields cannot be used in access policies.
  • Although not explicitly disallowed, you should not use an encrypted field as an index, for filtering or sorting.
Comments
Feel free to ask questions, give feedback, or report issues.

Don't Spam


You can edit/delete your comments by going directly to the discussion, clicking on the 'comments' link below