Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,11 @@ attribute @meta(_ name: String, _ value: Any)
* Marks an attribute as deprecated.
*/
attribute @@@deprecated(_ message: String)

/**
* Indicates that the field should be encrypted when storing in the database and decrypted when read.
* Only applicable to String fields. The encryption uses AES-256-GCM via the Web Crypto API.
*
* To use this attribute, you must configure encryption options when creating the ZenStackClient.
*/
attribute @encrypted() @@@targetField([StringField])
4 changes: 4 additions & 0 deletions packages/plugins/encryption/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import base from '@zenstackhq/eslint-config';

/** @type {import('eslint').Linter.Config[]} */
export default [...base];
48 changes: 48 additions & 0 deletions packages/plugins/encryption/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@zenstackhq/plugin-encryption",
"version": "3.3.2",
"description": "ZenStack Encryption Plugin - Automatic field encryption/decryption for @encrypted fields",
"type": "module",
"scripts": {
"build": "tsc --noEmit && tsup-node",
"watch": "tsup-node --watch",
"lint": "eslint src --ext ts",
"pack": "pnpm pack"
},
"keywords": [
"zenstack",
"encryption",
"aes",
"crypto"
],
"author": "ZenStack Team",
"license": "MIT",
"files": [
"dist"
],
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": {
"import": "./package.json",
"require": "./package.json"
}
},
"dependencies": {
"@zenstackhq/orm": "workspace:*",
"zod": "catalog:"
},
"devDependencies": {
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*"
}
}
38 changes: 38 additions & 0 deletions packages/plugins/encryption/src/decrypter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { _decrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';

/**
* Default decrypter with support for key rotation
*/
export class Decrypter {
private keys: Array<{ key: CryptoKey; digest: string }> = [];

constructor(private readonly decryptionKeys: Uint8Array[]) {
if (decryptionKeys.length === 0) {
throw new Error('At least one decryption key must be provided');
}

for (const key of decryptionKeys) {
if (key.length !== ENCRYPTION_KEY_BYTES) {
throw new Error(`Decryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);
}
}
}

/**
* Decrypts the given data
*/
async decrypt(data: string): Promise<string> {
if (this.keys.length === 0) {
this.keys = await Promise.all(
this.decryptionKeys.map(async (key) => ({
key: await loadKey(key, ['decrypt']),
digest: await getKeyDigest(key),
})),
);
}

return _decrypt(data, async (digest) =>
this.keys.filter((entry) => entry.digest === digest).map((entry) => entry.key),
);
}
}
30 changes: 30 additions & 0 deletions packages/plugins/encryption/src/encrypter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { _encrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';

/**
* Default encrypter using AES-256-GCM
*/
export class Encrypter {
private key: CryptoKey | undefined;
private keyDigest: string | undefined;

constructor(private readonly encryptionKey: Uint8Array) {
if (encryptionKey.length !== ENCRYPTION_KEY_BYTES) {
throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);
}
}

/**
* Encrypts the given data
*/
async encrypt(data: string): Promise<string> {
if (!this.key) {
this.key = await loadKey(this.encryptionKey, ['encrypt']);
}

if (!this.keyDigest) {
this.keyDigest = await getKeyDigest(this.encryptionKey);
}

return _encrypt(data, this.key, this.keyDigest);
}
}
6 changes: 6 additions & 0 deletions packages/plugins/encryption/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { Decrypter } from './decrypter.js';
export { Encrypter } from './encrypter.js';
export { createEncryptionPlugin } from './plugin.js';
export type { CustomEncryption, EncryptionConfig, SimpleEncryption } from './types.js';
export { isCustomEncryption } from './types.js';
export { ENCRYPTION_KEY_BYTES } from './utils.js';
Loading
Loading