zenstack-encryption 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # zenstack-encryption
2
+
3
+ A [ZenStack](https://zenstack.dev) v3 community plugin that provides transparent field-level encryption and decryption using the `@encrypted` attribute.
4
+
5
+ ## Features
6
+
7
+ - **AES-256-GCM** encryption via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) (no native dependencies)
8
+ - **Transparent** encrypt-on-write, decrypt-on-read through ZenStack's `onQuery` plugin hook
9
+ - **Key rotation** — add previous keys to a fallback list so existing data can still be decrypted while new writes use the latest key
10
+ - **Custom encryption** — bring your own encrypt/decrypt functions for KMS integration, envelope encryption, etc.
11
+ - **Nested writes** — handles `create`, `createMany`, `update`, `updateMany`, `upsert`, and `connectOrCreate` across relations
12
+
13
+ ## How It Works
14
+
15
+ The plugin hooks into the ZenStack ORM's query lifecycle via `onQuery`. When a write operation (`create`, `update`, etc.) is performed, the plugin inspects the schema for fields marked `@encrypted` and encrypts their values before they reach the database. When data is read back, encrypted fields are automatically decrypted before being returned to the caller.
16
+
17
+ ```
18
+ Write path: app → plugin encrypts @encrypted fields → database
19
+ Read path: database → plugin decrypts @encrypted fields → app
20
+ ```
21
+
22
+ Encrypted values are stored as a base64 string with the format `{metadata}.{ciphertext}`, where metadata includes the encryption version, algorithm, and a key digest (used for key rotation lookups). Each encryption uses a random 12-byte IV, so the same plaintext produces different ciphertext every time.
23
+
24
+ > **Note:** Because the plugin operates at the ORM level, direct Kysely query builder calls (`client.$qb`) bypass encryption entirely.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ # npm
30
+ npm install zenstack-encryption
31
+
32
+ # pnpm
33
+ pnpm add zenstack-encryption
34
+ ```
35
+
36
+ ## Setup
37
+
38
+ ### 1. Register the plugin in your ZModel schema
39
+
40
+ Add a `plugin` block to your `.zmodel` file. This makes the `@encrypted` attribute available in your schema:
41
+
42
+ ```zmodel
43
+ plugin encryption {
44
+ provider = 'zenstack-encryption'
45
+ }
46
+ ```
47
+
48
+ ### 2. Mark fields with `@encrypted`
49
+
50
+ Apply `@encrypted` to any `String` field you want to encrypt at rest:
51
+
52
+ ```zmodel
53
+ model User {
54
+ id String @id @default(cuid())
55
+ email String @unique
56
+ name String?
57
+ secretToken String @encrypted
58
+ posts Post[]
59
+ }
60
+
61
+ model Post {
62
+ id String @id @default(cuid())
63
+ title String
64
+ content String? @encrypted
65
+ author User @relation(fields: [authorId], references: [id])
66
+ authorId String
67
+ }
68
+ ```
69
+
70
+ ### 3. Generate your schema
71
+
72
+ ```bash
73
+ npx zenstack generate
74
+ ```
75
+
76
+ ### 4. Configure the plugin at runtime
77
+
78
+ ```typescript
79
+ import { ZenStackClient } from '@zenstackhq/orm';
80
+ import { createEncryptionPlugin, ENCRYPTION_KEY_BYTES } from 'zenstack-encryption';
81
+ import schema from './schema.js';
82
+
83
+ // Load your 32-byte key from environment, KMS, vault, etc.
84
+ const encryptionKey = new Uint8Array(
85
+ Buffer.from(process.env.ENCRYPTION_KEY!, 'base64')
86
+ );
87
+
88
+ const encryptionPlugin = createEncryptionPlugin({ encryptionKey });
89
+
90
+ const client = new ZenStackClient(schema, {
91
+ plugins: [encryptionPlugin],
92
+ });
93
+
94
+ // Fields are encrypted/decrypted transparently
95
+ const user = await client.user.create({
96
+ data: {
97
+ email: 'alice@example.com',
98
+ secretToken: 'super-secret-value',
99
+ },
100
+ });
101
+
102
+ console.log(user.secretToken); // → "super-secret-value" (decrypted)
103
+ ```
104
+
105
+ ## Key Rotation
106
+
107
+ When you need to rotate encryption keys, pass old keys via `decryptionKeys`. The plugin will use the primary `encryptionKey` for new writes, but try all keys (primary + decryption) when decrypting:
108
+
109
+ ```typescript
110
+ const plugin = createEncryptionPlugin({
111
+ encryptionKey: newKey, // used for all new encryptions
112
+ decryptionKeys: [oldKey], // tried during decryption alongside newKey
113
+ });
114
+ ```
115
+
116
+ This enables zero-downtime key rotation:
117
+
118
+ 1. Deploy with both keys configured (new key as primary, old key in `decryptionKeys`)
119
+ 2. Existing data encrypted with the old key is still readable
120
+ 3. New writes use the new key
121
+ 4. Optionally re-encrypt old data by reading and updating records
122
+
123
+ ## Custom Encryption
124
+
125
+ For integration with AWS KMS, HashiCorp Vault, or any other encryption provider, pass custom `encrypt` and `decrypt` functions:
126
+
127
+ ```typescript
128
+ import type { FieldDef } from '@zenstackhq/orm/schema';
129
+
130
+ const plugin = createEncryptionPlugin({
131
+ encrypt: async (model: string, field: FieldDef, plaintext: string) => {
132
+ // Call your encryption service
133
+ return await myKms.encrypt(plaintext);
134
+ },
135
+ decrypt: async (model: string, field: FieldDef, ciphertext: string) => {
136
+ // Call your decryption service
137
+ return await myKms.decrypt(ciphertext);
138
+ },
139
+ });
140
+ ```
141
+
142
+ The `model` and `field` parameters let you use different keys or strategies per model/field.
143
+
144
+ ## Adding to an existing client
145
+
146
+ You can also add the plugin to an existing `ZenStackClient` instance using `$use`:
147
+
148
+ ```typescript
149
+ const baseClient = new ZenStackClient(schema);
150
+ const client = baseClient.$use(createEncryptionPlugin({ encryptionKey }));
151
+ ```
152
+
153
+ ## Limitations
154
+
155
+ - **ORM only** — only applies to ORM CRUD operations, not direct Kysely query builder calls via `client.$qb`
156
+ - **String fields only** — `@encrypted` can only be applied to `String` fields
157
+ - **No encrypted filtering** — encrypted fields cannot be used in `where` clauses since encryption is non-deterministic (each encryption produces different ciphertext due to random IVs)
158
+ - **Storage overhead** — encrypted values are longer than the original plaintext due to base64 encoding + metadata; ensure your database column can accommodate the larger size
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,91 @@
1
+ import * as _zenstackhq_orm0 from "@zenstackhq/orm";
2
+ import { FieldDef, SchemaDef } from "@zenstackhq/orm/schema";
3
+
4
+ //#region src/decrypter.d.ts
5
+ /**
6
+ * Default decrypter with support for key rotation
7
+ */
8
+ declare class Decrypter {
9
+ private readonly decryptionKeys;
10
+ private keys;
11
+ constructor(decryptionKeys: Uint8Array[]);
12
+ /**
13
+ * Decrypts the given data
14
+ */
15
+ decrypt(data: string): Promise<string>;
16
+ }
17
+ //#endregion
18
+ //#region src/encrypter.d.ts
19
+ /**
20
+ * Default encrypter using AES-256-GCM
21
+ */
22
+ declare class Encrypter {
23
+ private readonly encryptionKey;
24
+ private key;
25
+ private keyDigest;
26
+ constructor(encryptionKey: Uint8Array);
27
+ /**
28
+ * Encrypts the given data
29
+ */
30
+ encrypt(data: string): Promise<string>;
31
+ }
32
+ //#endregion
33
+ //#region src/types.d.ts
34
+ /**
35
+ * Simple encryption configuration using built-in AES-256-GCM encryption
36
+ */
37
+ type SimpleEncryption = {
38
+ /**
39
+ * The encryption key (must be 32 bytes / 256 bits)
40
+ */
41
+ encryptionKey: Uint8Array;
42
+ /**
43
+ * Additional decryption keys for key rotation support.
44
+ * When decrypting, all keys (encryptionKey + decryptionKeys) are tried.
45
+ */
46
+ decryptionKeys?: Uint8Array[];
47
+ };
48
+ /**
49
+ * Custom encryption configuration for user-provided encryption handlers
50
+ */
51
+ type CustomEncryption = {
52
+ /**
53
+ * Custom encryption function
54
+ * @param model The model name
55
+ * @param field The field definition
56
+ * @param plain The plaintext value to encrypt
57
+ * @returns The encrypted value
58
+ */
59
+ encrypt: (model: string, field: FieldDef, plain: string) => Promise<string>;
60
+ /**
61
+ * Custom decryption function
62
+ * @param model The model name
63
+ * @param field The field definition
64
+ * @param cipher The encrypted value to decrypt
65
+ * @returns The decrypted value
66
+ */
67
+ decrypt: (model: string, field: FieldDef, cipher: string) => Promise<string>;
68
+ };
69
+ /**
70
+ * Encryption configuration - either simple (built-in) or custom
71
+ */
72
+ type EncryptionConfig = SimpleEncryption | CustomEncryption;
73
+ /**
74
+ * Type guard to check if encryption config is custom
75
+ */
76
+ declare function isCustomEncryption(config: EncryptionConfig): config is CustomEncryption;
77
+ //#endregion
78
+ //#region src/plugin.d.ts
79
+ /**
80
+ * Creates an encryption plugin for ZenStack ORM
81
+ *
82
+ * @param config Encryption configuration (simple or custom)
83
+ * @returns A runtime plugin that handles field encryption/decryption
84
+ */
85
+ declare function createEncryptionPlugin<Schema extends SchemaDef>(config: EncryptionConfig): _zenstackhq_orm0.RuntimePlugin<any, {}, {}>;
86
+ //#endregion
87
+ //#region src/utils.d.ts
88
+ declare const ENCRYPTION_KEY_BYTES = 32;
89
+ //#endregion
90
+ export { type CustomEncryption, Decrypter, ENCRYPTION_KEY_BYTES, Encrypter, type EncryptionConfig, type SimpleEncryption, createEncryptionPlugin, isCustomEncryption };
91
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/decrypter.ts","../src/encrypter.ts","../src/types.ts","../src/plugin.ts","../src/utils.ts"],"mappings":";;;;;;;cAKa,SAAA;EAAA,iBAGoB,cAAA;EAAA,QAFrB,IAAA;cAEqB,cAAA,EAAgB,UAAA;EAeT;;;EAA9B,OAAA,CAAQ,IAAA,WAAe,OAAA;AAAA;;;;;;cClBpB,SAAA;EAAA,iBAIoB,aAAA;EAAA,QAHrB,GAAA;EAAA,QACA,SAAA;cAEqB,aAAA,EAAe,UAAA;EDDf;;;ECUvB,OAAA,CAAQ,IAAA,WAAe,OAAA;AAAA;;;;;;KCbrB,gBAAA;EFAU;;;EEIlB,aAAA,EAAe,UAAA;EFHP;;;;EESR,cAAA,GAAiB,UAAA;AAAA;;;;KAMT,gBAAA;;;ADhBZ;;;;;ECwBI,OAAA,GAAU,KAAA,UAAe,KAAA,EAAO,QAAA,EAAU,KAAA,aAAkB,OAAA;EDtBpD;;;;;;;EC+BR,OAAA,GAAU,KAAA,UAAe,KAAA,EAAO,QAAA,EAAU,MAAA,aAAmB,OAAA;AAAA;;;;KAMrD,gBAAA,GAAmB,gBAAA,GAAmB,gBAAA;;;;iBAKlC,kBAAA,CAAmB,MAAA,EAAQ,gBAAA,GAAmB,MAAA,IAAU,gBAAA;;;;;AF5CxE;;;;iBGwBgB,sBAAA,gBAAsC,SAAA,CAAA,CAAW,MAAA,EAAQ,gBAAA,GAAgB,gBAAA,CAAA,aAAA;;;cC1B5E,oBAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,310 @@
1
+ import { z } from "zod";
2
+ import { definePlugin } from "@zenstackhq/orm";
3
+
4
+ //#region src/utils.ts
5
+ const ENCRYPTER_VERSION = 1;
6
+ const ENCRYPTION_KEY_BYTES = 32;
7
+ const IV_BYTES = 12;
8
+ const ALGORITHM = "AES-GCM";
9
+ const KEY_DIGEST_BYTES = 8;
10
+ const encoder = new TextEncoder();
11
+ const decoder = new TextDecoder();
12
+ const encryptionMetaSchema = z.object({
13
+ v: z.number(),
14
+ a: z.string(),
15
+ k: z.string()
16
+ });
17
+ /**
18
+ * Load a raw encryption key into a CryptoKey object
19
+ */
20
+ async function loadKey(key, keyUsages) {
21
+ const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
22
+ return crypto.subtle.importKey("raw", keyBuffer, ALGORITHM, false, keyUsages);
23
+ }
24
+ /**
25
+ * Get a digest of the encryption key for identification
26
+ */
27
+ async function getKeyDigest(key) {
28
+ const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
29
+ const rawDigest = await crypto.subtle.digest("SHA-256", keyBuffer);
30
+ return new Uint8Array(rawDigest.slice(0, KEY_DIGEST_BYTES)).reduce((acc, byte) => acc + byte.toString(16).padStart(2, "0"), "");
31
+ }
32
+ /**
33
+ * Encrypt data using AES-GCM
34
+ */
35
+ async function _encrypt(data, key, keyDigest) {
36
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
37
+ const encrypted = await crypto.subtle.encrypt({
38
+ name: ALGORITHM,
39
+ iv
40
+ }, key, encoder.encode(data));
41
+ const cipherBytes = [...iv, ...new Uint8Array(encrypted)];
42
+ const meta = {
43
+ v: ENCRYPTER_VERSION,
44
+ a: ALGORITHM,
45
+ k: keyDigest
46
+ };
47
+ return `${btoa(JSON.stringify(meta))}.${btoa(String.fromCharCode(...cipherBytes))}`;
48
+ }
49
+ /**
50
+ * Decrypt data using AES-GCM
51
+ */
52
+ async function _decrypt(data, findKey) {
53
+ const [metaText, cipherText] = data.split(".");
54
+ if (!metaText || !cipherText) throw new Error("Malformed encrypted data");
55
+ let metaObj;
56
+ try {
57
+ metaObj = JSON.parse(atob(metaText));
58
+ } catch {
59
+ throw new Error("Malformed metadata");
60
+ }
61
+ const { a: algorithm, k: keyDigest } = encryptionMetaSchema.parse(metaObj);
62
+ const keys = await findKey(keyDigest);
63
+ if (keys.length === 0) throw new Error("No matching decryption key found");
64
+ const bytes = Uint8Array.from(atob(cipherText), (c) => c.charCodeAt(0));
65
+ const iv = bytes.slice(0, IV_BYTES);
66
+ const cipher = bytes.slice(IV_BYTES);
67
+ let lastError;
68
+ for (const key of keys) {
69
+ let decrypted;
70
+ try {
71
+ decrypted = await crypto.subtle.decrypt({
72
+ name: algorithm,
73
+ iv
74
+ }, key, cipher);
75
+ } catch (err) {
76
+ lastError = err;
77
+ continue;
78
+ }
79
+ return decoder.decode(decrypted);
80
+ }
81
+ throw lastError;
82
+ }
83
+
84
+ //#endregion
85
+ //#region src/decrypter.ts
86
+ /**
87
+ * Default decrypter with support for key rotation
88
+ */
89
+ var Decrypter = class {
90
+ constructor(decryptionKeys) {
91
+ this.decryptionKeys = decryptionKeys;
92
+ this.keys = [];
93
+ if (decryptionKeys.length === 0) throw new Error("At least one decryption key must be provided");
94
+ for (const key of decryptionKeys) if (key.length !== ENCRYPTION_KEY_BYTES) throw new Error(`Decryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);
95
+ }
96
+ /**
97
+ * Decrypts the given data
98
+ */
99
+ async decrypt(data) {
100
+ if (this.keys.length === 0) this.keys = await Promise.all(this.decryptionKeys.map(async (key) => ({
101
+ key: await loadKey(key, ["decrypt"]),
102
+ digest: await getKeyDigest(key)
103
+ })));
104
+ return _decrypt(data, async (digest) => this.keys.filter((entry) => entry.digest === digest).map((entry) => entry.key));
105
+ }
106
+ };
107
+
108
+ //#endregion
109
+ //#region src/encrypter.ts
110
+ /**
111
+ * Default encrypter using AES-256-GCM
112
+ */
113
+ var Encrypter = class {
114
+ constructor(encryptionKey) {
115
+ this.encryptionKey = encryptionKey;
116
+ if (encryptionKey.length !== ENCRYPTION_KEY_BYTES) throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);
117
+ }
118
+ /**
119
+ * Encrypts the given data
120
+ */
121
+ async encrypt(data) {
122
+ if (!this.key) this.key = await loadKey(this.encryptionKey, ["encrypt"]);
123
+ if (!this.keyDigest) this.keyDigest = await getKeyDigest(this.encryptionKey);
124
+ return _encrypt(data, this.key, this.keyDigest);
125
+ }
126
+ };
127
+
128
+ //#endregion
129
+ //#region src/types.ts
130
+ /**
131
+ * Type guard to check if encryption config is custom
132
+ */
133
+ function isCustomEncryption(config) {
134
+ return "encrypt" in config && "decrypt" in config;
135
+ }
136
+
137
+ //#endregion
138
+ //#region src/plugin.ts
139
+ const ENCRYPTED_ATTRIBUTE = "@encrypted";
140
+ /**
141
+ * Check if a field has the @encrypted attribute
142
+ */
143
+ function isEncryptedField(field) {
144
+ return field.attributes?.some((attr) => attr.name === ENCRYPTED_ATTRIBUTE) ?? false;
145
+ }
146
+ /**
147
+ * Check if a model has any encrypted fields
148
+ */
149
+ function hasEncryptedFields(model) {
150
+ return Object.values(model.fields).some(isEncryptedField);
151
+ }
152
+ /**
153
+ * Creates an encryption plugin for ZenStack ORM
154
+ *
155
+ * @param config Encryption configuration (simple or custom)
156
+ * @returns A runtime plugin that handles field encryption/decryption
157
+ */
158
+ function createEncryptionPlugin(config) {
159
+ let encrypter;
160
+ let decrypter;
161
+ let customEncryption;
162
+ if (isCustomEncryption(config)) customEncryption = config;
163
+ else {
164
+ const simpleConfig = config;
165
+ encrypter = new Encrypter(simpleConfig.encryptionKey);
166
+ decrypter = new Decrypter([simpleConfig.encryptionKey, ...simpleConfig.decryptionKeys ?? []]);
167
+ }
168
+ async function encryptValue(model, field, value) {
169
+ if (customEncryption) return customEncryption.encrypt(model, field, value);
170
+ return encrypter.encrypt(value);
171
+ }
172
+ async function decryptValue(model, field, value) {
173
+ if (customEncryption) return customEncryption.decrypt(model, field, value);
174
+ return decrypter.decrypt(value);
175
+ }
176
+ /**
177
+ * Recursively encrypt fields in write data
178
+ */
179
+ async function encryptWriteData(schema, modelName, data) {
180
+ const model = schema.models[modelName];
181
+ if (!model) return;
182
+ for (const [fieldName, value] of Object.entries(data)) {
183
+ if (value === null || value === void 0 || value === "") continue;
184
+ const field = model.fields[fieldName];
185
+ if (!field) continue;
186
+ if (isEncryptedField(field) && typeof value === "string") {
187
+ data[fieldName] = await encryptValue(modelName, field, value);
188
+ continue;
189
+ }
190
+ if (field.relation && typeof value === "object") {
191
+ const relatedModel = field.type;
192
+ await encryptNestedWrites(schema, relatedModel, value);
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Handle nested write operations (create, update, connect, etc.)
198
+ */
199
+ async function encryptNestedWrites(schema, modelName, data) {
200
+ const createData = data["create"];
201
+ if (createData) if (Array.isArray(createData)) for (const item of createData) await encryptWriteData(schema, modelName, item);
202
+ else await encryptWriteData(schema, modelName, createData);
203
+ const createManyData = data["createMany"];
204
+ if (createManyData && typeof createManyData === "object") {
205
+ const createManyItems = createManyData["data"];
206
+ if (Array.isArray(createManyItems)) for (const item of createManyItems) await encryptWriteData(schema, modelName, item);
207
+ }
208
+ const updateData = data["update"];
209
+ if (updateData) if (Array.isArray(updateData)) for (const item of updateData) {
210
+ const itemData = item["data"];
211
+ if (itemData) await encryptWriteData(schema, modelName, itemData);
212
+ }
213
+ else {
214
+ const updateObj = updateData;
215
+ const nestedData = updateObj["data"];
216
+ if (nestedData) await encryptWriteData(schema, modelName, nestedData);
217
+ else await encryptWriteData(schema, modelName, updateObj);
218
+ }
219
+ const updateManyData = data["updateMany"];
220
+ if (updateManyData) if (Array.isArray(updateManyData)) for (const item of updateManyData) {
221
+ const itemData = item["data"];
222
+ if (itemData) await encryptWriteData(schema, modelName, itemData);
223
+ }
224
+ else {
225
+ const nestedData = updateManyData["data"];
226
+ if (nestedData) await encryptWriteData(schema, modelName, nestedData);
227
+ }
228
+ const upsertData = data["upsert"];
229
+ if (upsertData) if (Array.isArray(upsertData)) for (const item of upsertData) {
230
+ const upsertItem = item;
231
+ const createPart = upsertItem["create"];
232
+ const updatePart = upsertItem["update"];
233
+ if (createPart) await encryptWriteData(schema, modelName, createPart);
234
+ if (updatePart) await encryptWriteData(schema, modelName, updatePart);
235
+ }
236
+ else {
237
+ const upsertObj = upsertData;
238
+ const createPart = upsertObj["create"];
239
+ const updatePart = upsertObj["update"];
240
+ if (createPart) await encryptWriteData(schema, modelName, createPart);
241
+ if (updatePart) await encryptWriteData(schema, modelName, updatePart);
242
+ }
243
+ const connectOrCreateData = data["connectOrCreate"];
244
+ if (connectOrCreateData) if (Array.isArray(connectOrCreateData)) for (const item of connectOrCreateData) {
245
+ const createPart = item["create"];
246
+ if (createPart) await encryptWriteData(schema, modelName, createPart);
247
+ }
248
+ else {
249
+ const createPart = connectOrCreateData["create"];
250
+ if (createPart) await encryptWriteData(schema, modelName, createPart);
251
+ }
252
+ }
253
+ /**
254
+ * Recursively decrypt fields in result data
255
+ */
256
+ async function decryptResultData(schema, modelName, data) {
257
+ const model = schema.models[modelName];
258
+ if (!model) return;
259
+ for (const [fieldName, value] of Object.entries(data)) {
260
+ if (value === null || value === void 0 || value === "") continue;
261
+ const field = model.fields[fieldName];
262
+ if (!field) continue;
263
+ if (isEncryptedField(field) && typeof value === "string") {
264
+ try {
265
+ data[fieldName] = await decryptValue(modelName, field, value);
266
+ } catch (error) {
267
+ console.warn(`Failed to decrypt field ${modelName}.${fieldName}:`, error);
268
+ }
269
+ continue;
270
+ }
271
+ if (field.relation && value !== null) {
272
+ const relatedModel = field.type;
273
+ if (Array.isArray(value)) {
274
+ for (const item of value) if (typeof item === "object" && item !== null) await decryptResultData(schema, relatedModel, item);
275
+ } else if (typeof value === "object") await decryptResultData(schema, relatedModel, value);
276
+ }
277
+ }
278
+ }
279
+ return definePlugin({
280
+ id: "encryption",
281
+ name: "Encryption Plugin",
282
+ description: "Automatically encrypts and decrypts fields marked with @encrypted",
283
+ onQuery: async (ctx) => {
284
+ const { model, operation, args, proceed, client } = ctx;
285
+ const schema = client.schema;
286
+ const modelDef = schema.models[model];
287
+ if (!modelDef || !hasEncryptedFields(modelDef)) return proceed(args);
288
+ const processedArgs = args ? JSON.parse(JSON.stringify(args)) : void 0;
289
+ if (operation === "create" || operation === "update" || operation === "upsert" || operation === "createMany" || operation === "updateMany" || operation === "createManyAndReturn") {
290
+ if (processedArgs?.data) if (Array.isArray(processedArgs.data)) for (const item of processedArgs.data) await encryptWriteData(schema, model, item);
291
+ else await encryptWriteData(schema, model, processedArgs.data);
292
+ if (operation === "upsert") {
293
+ if (processedArgs?.create) await encryptWriteData(schema, model, processedArgs.create);
294
+ if (processedArgs?.update) await encryptWriteData(schema, model, processedArgs.update);
295
+ }
296
+ }
297
+ const result = await proceed(processedArgs);
298
+ if (result !== null && result !== void 0) {
299
+ if (Array.isArray(result)) {
300
+ for (const item of result) if (typeof item === "object" && item !== null) await decryptResultData(schema, model, item);
301
+ } else if (typeof result === "object") await decryptResultData(schema, model, result);
302
+ }
303
+ return result;
304
+ }
305
+ });
306
+ }
307
+
308
+ //#endregion
309
+ export { Decrypter, ENCRYPTION_KEY_BYTES, Encrypter, createEncryptionPlugin, isCustomEncryption };
310
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/utils.ts","../src/decrypter.ts","../src/encrypter.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const ENCRYPTER_VERSION = 1;\nexport const ENCRYPTION_KEY_BYTES = 32;\nexport const IV_BYTES = 12;\nexport const ALGORITHM = 'AES-GCM';\nexport const KEY_DIGEST_BYTES = 8;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nconst encryptionMetaSchema = z.object({\n // version\n v: z.number(),\n // algorithm\n a: z.string(),\n // key digest\n k: z.string(),\n});\n\n/**\n * Load a raw encryption key into a CryptoKey object\n */\nexport async function loadKey(key: Uint8Array, keyUsages: KeyUsage[]): Promise<CryptoKey> {\n // Convert to ArrayBuffer for crypto.subtle compatibility\n const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength) as ArrayBuffer;\n return crypto.subtle.importKey('raw', keyBuffer, ALGORITHM, false, keyUsages);\n}\n\n/**\n * Get a digest of the encryption key for identification\n */\nexport async function getKeyDigest(key: Uint8Array): Promise<string> {\n // Convert to ArrayBuffer for crypto.subtle compatibility\n const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength) as ArrayBuffer;\n const rawDigest = await crypto.subtle.digest('SHA-256', keyBuffer);\n return new Uint8Array(rawDigest.slice(0, KEY_DIGEST_BYTES)).reduce(\n (acc, byte) => acc + byte.toString(16).padStart(2, '0'),\n '',\n );\n}\n\n/**\n * Encrypt data using AES-GCM\n */\nexport async function _encrypt(data: string, key: CryptoKey, keyDigest: string): Promise<string> {\n const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n const encrypted = await crypto.subtle.encrypt(\n {\n name: ALGORITHM,\n iv,\n },\n key,\n encoder.encode(data),\n );\n\n // combine IV and encrypted data into a single array of bytes\n const cipherBytes = [...iv, ...new Uint8Array(encrypted)];\n\n // encryption metadata\n const meta = { v: ENCRYPTER_VERSION, a: ALGORITHM, k: keyDigest };\n\n // convert concatenated result to base64 string\n return `${btoa(JSON.stringify(meta))}.${btoa(String.fromCharCode(...cipherBytes))}`;\n}\n\n/**\n * Decrypt data using AES-GCM\n */\nexport async function _decrypt(data: string, findKey: (digest: string) => Promise<CryptoKey[]>): Promise<string> {\n const [metaText, cipherText] = data.split('.');\n if (!metaText || !cipherText) {\n throw new Error('Malformed encrypted data');\n }\n\n let metaObj: unknown;\n try {\n metaObj = JSON.parse(atob(metaText));\n } catch {\n throw new Error('Malformed metadata');\n }\n\n // parse meta\n const { a: algorithm, k: keyDigest } = encryptionMetaSchema.parse(metaObj);\n\n // find a matching decryption key\n const keys = await findKey(keyDigest);\n if (keys.length === 0) {\n throw new Error('No matching decryption key found');\n }\n\n // convert base64 back to bytes\n const bytes = Uint8Array.from(atob(cipherText), (c) => c.charCodeAt(0));\n\n // extract IV from the head\n const iv = bytes.slice(0, IV_BYTES);\n const cipher = bytes.slice(IV_BYTES);\n let lastError: unknown;\n\n for (const key of keys) {\n let decrypted: ArrayBuffer;\n try {\n decrypted = await crypto.subtle.decrypt({ name: algorithm, iv }, key, cipher);\n } catch (err) {\n lastError = err;\n continue;\n }\n return decoder.decode(decrypted);\n }\n\n throw lastError;\n}\n","import { _decrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';\n\n/**\n * Default decrypter with support for key rotation\n */\nexport class Decrypter {\n private keys: Array<{ key: CryptoKey; digest: string }> = [];\n\n constructor(private readonly decryptionKeys: Uint8Array[]) {\n if (decryptionKeys.length === 0) {\n throw new Error('At least one decryption key must be provided');\n }\n\n for (const key of decryptionKeys) {\n if (key.length !== ENCRYPTION_KEY_BYTES) {\n throw new Error(`Decryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);\n }\n }\n }\n\n /**\n * Decrypts the given data\n */\n async decrypt(data: string): Promise<string> {\n if (this.keys.length === 0) {\n this.keys = await Promise.all(\n this.decryptionKeys.map(async (key) => ({\n key: await loadKey(key, ['decrypt']),\n digest: await getKeyDigest(key),\n })),\n );\n }\n\n return _decrypt(data, async (digest) =>\n this.keys.filter((entry) => entry.digest === digest).map((entry) => entry.key),\n );\n }\n}\n","import { _encrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';\n\n/**\n * Default encrypter using AES-256-GCM\n */\nexport class Encrypter {\n private key: CryptoKey | undefined;\n private keyDigest: string | undefined;\n\n constructor(private readonly encryptionKey: Uint8Array) {\n if (encryptionKey.length !== ENCRYPTION_KEY_BYTES) {\n throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);\n }\n }\n\n /**\n * Encrypts the given data\n */\n async encrypt(data: string): Promise<string> {\n if (!this.key) {\n this.key = await loadKey(this.encryptionKey, ['encrypt']);\n }\n\n if (!this.keyDigest) {\n this.keyDigest = await getKeyDigest(this.encryptionKey);\n }\n\n return _encrypt(data, this.key, this.keyDigest);\n }\n}\n","import type { FieldDef } from '@zenstackhq/orm/schema';\n\n/**\n * Simple encryption configuration using built-in AES-256-GCM encryption\n */\nexport type SimpleEncryption = {\n /**\n * The encryption key (must be 32 bytes / 256 bits)\n */\n encryptionKey: Uint8Array;\n\n /**\n * Additional decryption keys for key rotation support.\n * When decrypting, all keys (encryptionKey + decryptionKeys) are tried.\n */\n decryptionKeys?: Uint8Array[];\n};\n\n/**\n * Custom encryption configuration for user-provided encryption handlers\n */\nexport type CustomEncryption = {\n /**\n * Custom encryption function\n * @param model The model name\n * @param field The field definition\n * @param plain The plaintext value to encrypt\n * @returns The encrypted value\n */\n encrypt: (model: string, field: FieldDef, plain: string) => Promise<string>;\n\n /**\n * Custom decryption function\n * @param model The model name\n * @param field The field definition\n * @param cipher The encrypted value to decrypt\n * @returns The decrypted value\n */\n decrypt: (model: string, field: FieldDef, cipher: string) => Promise<string>;\n};\n\n/**\n * Encryption configuration - either simple (built-in) or custom\n */\nexport type EncryptionConfig = SimpleEncryption | CustomEncryption;\n\n/**\n * Type guard to check if encryption config is custom\n */\nexport function isCustomEncryption(config: EncryptionConfig): config is CustomEncryption {\n return 'encrypt' in config && 'decrypt' in config;\n}\n","import { definePlugin } from '@zenstackhq/orm';\nimport type { FieldDef, ModelDef, SchemaDef } from '@zenstackhq/orm/schema';\nimport { Decrypter } from './decrypter.js';\nimport { Encrypter } from './encrypter.js';\nimport type { CustomEncryption, EncryptionConfig, SimpleEncryption } from './types.js';\nimport { isCustomEncryption } from './types.js';\n\nconst ENCRYPTED_ATTRIBUTE = '@encrypted';\n\n/**\n * Check if a field has the @encrypted attribute\n */\nfunction isEncryptedField(field: FieldDef): boolean {\n return field.attributes?.some((attr) => attr.name === ENCRYPTED_ATTRIBUTE) ?? false;\n}\n\n/**\n * Check if a model has any encrypted fields\n */\nfunction hasEncryptedFields(model: ModelDef): boolean {\n return Object.values(model.fields).some(isEncryptedField);\n}\n\n/**\n * Creates an encryption plugin for ZenStack ORM\n *\n * @param config Encryption configuration (simple or custom)\n * @returns A runtime plugin that handles field encryption/decryption\n */\nexport function createEncryptionPlugin<Schema extends SchemaDef>(config: EncryptionConfig) {\n let encrypter: Encrypter | undefined;\n let decrypter: Decrypter | undefined;\n let customEncryption: CustomEncryption | undefined;\n\n if (isCustomEncryption(config)) {\n customEncryption = config;\n } else {\n const simpleConfig = config as SimpleEncryption;\n encrypter = new Encrypter(simpleConfig.encryptionKey);\n const allDecryptionKeys = [simpleConfig.encryptionKey, ...(simpleConfig.decryptionKeys ?? [])];\n decrypter = new Decrypter(allDecryptionKeys);\n }\n\n async function encryptValue(model: string, field: FieldDef, value: string): Promise<string> {\n if (customEncryption) {\n return customEncryption.encrypt(model, field, value);\n }\n return encrypter!.encrypt(value);\n }\n\n async function decryptValue(model: string, field: FieldDef, value: string): Promise<string> {\n if (customEncryption) {\n return customEncryption.decrypt(model, field, value);\n }\n return decrypter!.decrypt(value);\n }\n\n /**\n * Recursively encrypt fields in write data\n */\n async function encryptWriteData(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n const model = schema.models[modelName];\n if (!model) return;\n\n for (const [fieldName, value] of Object.entries(data)) {\n if (value === null || value === undefined || value === '') {\n continue;\n }\n\n const field = model.fields[fieldName];\n if (!field) continue;\n\n // Handle encrypted string fields\n if (isEncryptedField(field) && typeof value === 'string') {\n data[fieldName] = await encryptValue(modelName, field, value);\n continue;\n }\n\n // Handle relation fields (nested writes)\n if (field.relation && typeof value === 'object') {\n const relatedModel = field.type;\n await encryptNestedWrites(schema, relatedModel, value as Record<string, unknown>);\n }\n }\n }\n\n /**\n * Handle nested write operations (create, update, connect, etc.)\n */\n async function encryptNestedWrites(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n // Handle create\n const createData = data['create'];\n if (createData) {\n if (Array.isArray(createData)) {\n for (const item of createData) {\n await encryptWriteData(schema, modelName, item as Record<string, unknown>);\n }\n } else {\n await encryptWriteData(schema, modelName, createData as Record<string, unknown>);\n }\n }\n\n // Handle createMany\n const createManyData = data['createMany'];\n if (createManyData && typeof createManyData === 'object') {\n const createManyItems = (createManyData as Record<string, unknown>)['data'];\n if (Array.isArray(createManyItems)) {\n for (const item of createManyItems) {\n await encryptWriteData(schema, modelName, item as Record<string, unknown>);\n }\n }\n }\n\n // Handle update\n const updateData = data['update'];\n if (updateData) {\n if (Array.isArray(updateData)) {\n for (const item of updateData) {\n const updateItem = item as Record<string, unknown>;\n const itemData = updateItem['data'];\n if (itemData) {\n await encryptWriteData(schema, modelName, itemData as Record<string, unknown>);\n }\n }\n } else {\n const updateObj = updateData as Record<string, unknown>;\n const nestedData = updateObj['data'];\n if (nestedData) {\n await encryptWriteData(schema, modelName, nestedData as Record<string, unknown>);\n } else {\n await encryptWriteData(schema, modelName, updateObj);\n }\n }\n }\n\n // Handle updateMany\n const updateManyData = data['updateMany'];\n if (updateManyData) {\n if (Array.isArray(updateManyData)) {\n for (const item of updateManyData) {\n const updateItem = item as Record<string, unknown>;\n const itemData = updateItem['data'];\n if (itemData) {\n await encryptWriteData(schema, modelName, itemData as Record<string, unknown>);\n }\n }\n } else {\n const updateObj = updateManyData as Record<string, unknown>;\n const nestedData = updateObj['data'];\n if (nestedData) {\n await encryptWriteData(schema, modelName, nestedData as Record<string, unknown>);\n }\n }\n }\n\n // Handle upsert\n const upsertData = data['upsert'];\n if (upsertData) {\n if (Array.isArray(upsertData)) {\n for (const item of upsertData) {\n const upsertItem = item as Record<string, unknown>;\n const createPart = upsertItem['create'];\n const updatePart = upsertItem['update'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n if (updatePart) {\n await encryptWriteData(schema, modelName, updatePart as Record<string, unknown>);\n }\n }\n } else {\n const upsertObj = upsertData as Record<string, unknown>;\n const createPart = upsertObj['create'];\n const updatePart = upsertObj['update'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n if (updatePart) {\n await encryptWriteData(schema, modelName, updatePart as Record<string, unknown>);\n }\n }\n }\n\n // Handle connectOrCreate\n const connectOrCreateData = data['connectOrCreate'];\n if (connectOrCreateData) {\n if (Array.isArray(connectOrCreateData)) {\n for (const item of connectOrCreateData) {\n const cocItem = item as Record<string, unknown>;\n const createPart = cocItem['create'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n }\n } else {\n const cocObj = connectOrCreateData as Record<string, unknown>;\n const createPart = cocObj['create'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n }\n }\n }\n\n /**\n * Recursively decrypt fields in result data\n */\n async function decryptResultData(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n const model = schema.models[modelName];\n if (!model) return;\n\n for (const [fieldName, value] of Object.entries(data)) {\n if (value === null || value === undefined || value === '') {\n continue;\n }\n\n const field = model.fields[fieldName];\n if (!field) continue;\n\n // Handle encrypted string fields\n if (isEncryptedField(field) && typeof value === 'string') {\n try {\n data[fieldName] = await decryptValue(modelName, field, value);\n } catch (error) {\n // If decryption fails, log warning and keep original value\n console.warn(`Failed to decrypt field ${modelName}.${fieldName}:`, error);\n }\n continue;\n }\n\n // Handle relation fields (nested data)\n if (field.relation && value !== null) {\n const relatedModel = field.type;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'object' && item !== null) {\n await decryptResultData(schema, relatedModel, item as Record<string, unknown>);\n }\n }\n } else if (typeof value === 'object') {\n await decryptResultData(schema, relatedModel, value as Record<string, unknown>);\n }\n }\n }\n }\n\n return definePlugin<Schema, {}, {}>({\n id: 'encryption',\n name: 'Encryption Plugin',\n description: 'Automatically encrypts and decrypts fields marked with @encrypted',\n\n onQuery: async (ctx) => {\n const { model, operation, args, proceed, client } = ctx;\n const schema = (client as any).schema as SchemaDef;\n const modelDef = schema.models[model];\n\n // Check if this model has any encrypted fields\n if (!modelDef || !hasEncryptedFields(modelDef)) {\n return proceed(args);\n }\n\n // Clone args to avoid mutating original\n const processedArgs = args ? JSON.parse(JSON.stringify(args)) : undefined;\n\n // Handle write operations - encrypt data before writing\n if (\n operation === 'create' ||\n operation === 'update' ||\n operation === 'upsert' ||\n operation === 'createMany' ||\n operation === 'updateMany' ||\n operation === 'createManyAndReturn'\n ) {\n if (processedArgs?.data) {\n if (Array.isArray(processedArgs.data)) {\n for (const item of processedArgs.data) {\n await encryptWriteData(schema, model, item);\n }\n } else {\n await encryptWriteData(schema, model, processedArgs.data);\n }\n }\n\n // Handle upsert create/update\n if (operation === 'upsert') {\n if (processedArgs?.create) {\n await encryptWriteData(schema, model, processedArgs.create);\n }\n if (processedArgs?.update) {\n await encryptWriteData(schema, model, processedArgs.update);\n }\n }\n }\n\n // Execute the query\n const result = await proceed(processedArgs);\n\n // Handle read operations - decrypt data after reading\n if (result !== null && result !== undefined) {\n if (Array.isArray(result)) {\n for (const item of result) {\n if (typeof item === 'object' && item !== null) {\n await decryptResultData(schema, model, item as Record<string, unknown>);\n }\n }\n } else if (typeof result === 'object') {\n await decryptResultData(schema, model, result as Record<string, unknown>);\n }\n }\n\n return result;\n },\n });\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "zenstack-encryption",
3
+ "version": "0.1.0",
4
+ "description": "ZenStack Encryption Plugin - Automatic field encryption/decryption for @encrypted fields",
5
+ "keywords": [
6
+ "zenstack",
7
+ "encryption",
8
+ "aes",
9
+ "crypto",
10
+ "plugin"
11
+ ],
12
+ "homepage": "https://github.com/genu/zenstack-encryption#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/genu/zenstack-encryption/issues"
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/genu/zenstack-encryption.git"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "plugin.zmodel",
24
+ "LICENSE",
25
+ "README.md"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.mts",
32
+ "default": "./dist/index.mjs"
33
+ },
34
+ "./plugin.zmodel": "./plugin.zmodel",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "peerDependencies": {
41
+ "@zenstackhq/orm": ">=3.3.0"
42
+ },
43
+ "dependencies": {
44
+ "zod": "4.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@zenstackhq/orm": "3.3.2",
48
+ "eslint": "9.0.0",
49
+ "tsdown": "0.20.0",
50
+ "typescript": "5.9.0",
51
+ "typescript-eslint": "8.0.0",
52
+ "vitest": "3.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsdown",
56
+ "watch": "tsdown --watch",
57
+ "lint": "eslint .",
58
+ "test": "vitest run"
59
+ }
60
+ }
package/plugin.zmodel ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Indicates that the field should be encrypted when storing in the database and decrypted when read.
3
+ * Only applicable to String fields. The encryption uses AES-256-GCM via the Web Crypto API.
4
+ *
5
+ * To use this attribute, you must configure encryption options when creating the ZenStackClient.
6
+ */
7
+ attribute @encrypted() @@@targetField([StringField])