@syncular/client-plugin-encryption 0.0.1 → 0.0.2-127
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 +36 -0
- package/dist/crypto-utils.d.ts +7 -0
- package/dist/crypto-utils.d.ts.map +1 -0
- package/dist/crypto-utils.js +110 -0
- package/dist/crypto-utils.js.map +1 -0
- package/dist/index.d.ts +6 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -137
- package/dist/index.js.map +1 -1
- package/dist/key-sharing.d.ts +5 -6
- package/dist/key-sharing.d.ts.map +1 -1
- package/dist/key-sharing.js +36 -112
- package/dist/key-sharing.js.map +1 -1
- package/package.json +28 -5
- package/src/__tests__/field-encryption-keys.test.ts +68 -0
- package/src/__tests__/key-sharing.test.ts +32 -0
- package/src/__tests__/scope-resolution.test.ts +202 -0
- package/src/crypto-utils.test.ts +84 -0
- package/src/crypto-utils.ts +125 -0
- package/src/index.ts +78 -152
- package/src/key-sharing.ts +41 -127
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @syncular/client-plugin-encryption
|
|
2
|
+
|
|
3
|
+
End-to-end encryption plugin for the Syncular client. Supports field-level encryption with key sharing between devices.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @syncular/client-plugin-encryption
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
createFieldEncryptionPlugin,
|
|
16
|
+
createStaticFieldEncryptionKeys,
|
|
17
|
+
} from '@syncular/client-plugin-encryption';
|
|
18
|
+
|
|
19
|
+
const encryption = createFieldEncryptionPlugin({
|
|
20
|
+
rules: [{ scope: 'user', table: 'notes', fields: ['body'] }],
|
|
21
|
+
keys: createStaticFieldEncryptionKeys({
|
|
22
|
+
keys: { default: 'base64url:...' },
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
- Encryption guide: https://syncular.dev/docs/build/encryption
|
|
30
|
+
|
|
31
|
+
## Links
|
|
32
|
+
|
|
33
|
+
- GitHub: https://github.com/syncular/syncular
|
|
34
|
+
- Issues: https://github.com/syncular/syncular/issues
|
|
35
|
+
|
|
36
|
+
> Status: Alpha. APIs and storage layouts may change between releases.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function randomBytes(length: number): Uint8Array;
|
|
2
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
3
|
+
export declare function base64ToBytes(base64: string): Uint8Array;
|
|
4
|
+
export declare function bytesToBase64Url(bytes: Uint8Array): string;
|
|
5
|
+
export declare function base64UrlToBytes(base64url: string): Uint8Array;
|
|
6
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
|
7
|
+
//# sourceMappingURL=crypto-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-utils.d.ts","sourceRoot":"","sources":["../src/crypto-utils.ts"],"names":[],"mappings":"AAYA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAWtD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAqCvD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CA8BxD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAK1D;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAO9D;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAYlD"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
2
|
+
const BASE64_LOOKUP = new Uint8Array(256);
|
|
3
|
+
for (let i = 0; i < BASE64_CHARS.length; i++) {
|
|
4
|
+
BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
|
|
5
|
+
}
|
|
6
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
7
|
+
const BASE64_URL_PATTERN = /^[A-Za-z0-9_-]*$/;
|
|
8
|
+
export function randomBytes(length) {
|
|
9
|
+
const cryptoObj = globalThis.crypto;
|
|
10
|
+
if (!cryptoObj?.getRandomValues) {
|
|
11
|
+
throw new Error('Secure random generator is not available (crypto.getRandomValues). ' +
|
|
12
|
+
'Ensure you are running in a secure context or polyfill crypto.');
|
|
13
|
+
}
|
|
14
|
+
const out = new Uint8Array(length);
|
|
15
|
+
cryptoObj.getRandomValues(out);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
export function bytesToBase64(bytes) {
|
|
19
|
+
if (typeof Buffer !== 'undefined') {
|
|
20
|
+
return Buffer.from(bytes).toString('base64');
|
|
21
|
+
}
|
|
22
|
+
let result = '';
|
|
23
|
+
const len = bytes.length;
|
|
24
|
+
const remainder = len % 3;
|
|
25
|
+
for (let i = 0; i < len - remainder; i += 3) {
|
|
26
|
+
const a = bytes[i];
|
|
27
|
+
const b = bytes[i + 1];
|
|
28
|
+
const c = bytes[i + 2];
|
|
29
|
+
result +=
|
|
30
|
+
BASE64_CHARS.charAt((a >> 2) & 0x3f) +
|
|
31
|
+
BASE64_CHARS.charAt(((a << 4) | (b >> 4)) & 0x3f) +
|
|
32
|
+
BASE64_CHARS.charAt(((b << 2) | (c >> 6)) & 0x3f) +
|
|
33
|
+
BASE64_CHARS.charAt(c & 0x3f);
|
|
34
|
+
}
|
|
35
|
+
if (remainder === 1) {
|
|
36
|
+
const a = bytes[len - 1];
|
|
37
|
+
result +=
|
|
38
|
+
BASE64_CHARS.charAt((a >> 2) & 0x3f) +
|
|
39
|
+
BASE64_CHARS.charAt((a << 4) & 0x3f) +
|
|
40
|
+
'==';
|
|
41
|
+
}
|
|
42
|
+
else if (remainder === 2) {
|
|
43
|
+
const a = bytes[len - 2];
|
|
44
|
+
const b = bytes[len - 1];
|
|
45
|
+
result +=
|
|
46
|
+
BASE64_CHARS.charAt((a >> 2) & 0x3f) +
|
|
47
|
+
BASE64_CHARS.charAt(((a << 4) | (b >> 4)) & 0x3f) +
|
|
48
|
+
BASE64_CHARS.charAt((b << 2) & 0x3f) +
|
|
49
|
+
'=';
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
export function base64ToBytes(base64) {
|
|
54
|
+
if (!BASE64_PATTERN.test(base64)) {
|
|
55
|
+
throw new Error('Invalid base64 string');
|
|
56
|
+
}
|
|
57
|
+
if (typeof Buffer !== 'undefined') {
|
|
58
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
59
|
+
}
|
|
60
|
+
const len = base64.length;
|
|
61
|
+
let padding = 0;
|
|
62
|
+
if (base64[len - 1] === '=')
|
|
63
|
+
padding++;
|
|
64
|
+
if (base64[len - 2] === '=')
|
|
65
|
+
padding++;
|
|
66
|
+
const outputLen = (len * 3) / 4 - padding;
|
|
67
|
+
const out = new Uint8Array(outputLen);
|
|
68
|
+
let outIdx = 0;
|
|
69
|
+
for (let i = 0; i < len; i += 4) {
|
|
70
|
+
const a = BASE64_LOOKUP[base64.charCodeAt(i)];
|
|
71
|
+
const b = BASE64_LOOKUP[base64.charCodeAt(i + 1)];
|
|
72
|
+
const c = BASE64_LOOKUP[base64.charCodeAt(i + 2)];
|
|
73
|
+
const d = BASE64_LOOKUP[base64.charCodeAt(i + 3)];
|
|
74
|
+
out[outIdx++] = (a << 2) | (b >> 4);
|
|
75
|
+
if (outIdx < outputLen)
|
|
76
|
+
out[outIdx++] = ((b << 4) | (c >> 2)) & 0xff;
|
|
77
|
+
if (outIdx < outputLen)
|
|
78
|
+
out[outIdx++] = ((c << 6) | d) & 0xff;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
export function bytesToBase64Url(bytes) {
|
|
83
|
+
return bytesToBase64(bytes)
|
|
84
|
+
.replace(/\+/g, '-')
|
|
85
|
+
.replace(/\//g, '_')
|
|
86
|
+
.replace(/=+$/g, '');
|
|
87
|
+
}
|
|
88
|
+
export function base64UrlToBytes(base64url) {
|
|
89
|
+
if (!BASE64_URL_PATTERN.test(base64url)) {
|
|
90
|
+
throw new Error('Invalid base64url string');
|
|
91
|
+
}
|
|
92
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
93
|
+
const padded = base64 + '==='.slice((base64.length + 3) % 4);
|
|
94
|
+
return base64ToBytes(padded);
|
|
95
|
+
}
|
|
96
|
+
export function hexToBytes(hex) {
|
|
97
|
+
const normalized = hex.trim().toLowerCase();
|
|
98
|
+
if (normalized.length % 2 !== 0) {
|
|
99
|
+
throw new Error('Invalid hex string (length must be even)');
|
|
100
|
+
}
|
|
101
|
+
const out = new Uint8Array(normalized.length / 2);
|
|
102
|
+
for (let i = 0; i < out.length; i++) {
|
|
103
|
+
const byte = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
104
|
+
if (!Number.isFinite(byte))
|
|
105
|
+
throw new Error('Invalid hex string');
|
|
106
|
+
out[i] = byte;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=crypto-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-utils.js","sourceRoot":"","sources":["../src/crypto-utils.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { SyncClientDb, SyncClientPlugin, SyncClientPluginContext } from '@syncular/client';
|
|
1
|
+
import type { SyncClientDb, SyncClientPlugin, SyncClientPluginContext, SyncEngine } from '@syncular/client';
|
|
2
2
|
import { type Kysely } from 'kysely';
|
|
3
3
|
export * from './key-sharing';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
type FieldDecryptionErrorMode = 'throw' | 'keepCiphertext';
|
|
5
|
+
interface FieldEncryptionRule {
|
|
6
6
|
scope: string;
|
|
7
7
|
/**
|
|
8
8
|
* Optional table selector. Strongly recommended for correctness:
|
|
@@ -35,7 +35,7 @@ export interface FieldEncryptionKeys {
|
|
|
35
35
|
field: string;
|
|
36
36
|
}) => string | Promise<string>;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
interface FieldEncryptionPluginOptions {
|
|
39
39
|
name?: string;
|
|
40
40
|
rules: FieldEncryptionRule[];
|
|
41
41
|
keys: FieldEncryptionKeys;
|
|
@@ -50,7 +50,7 @@ export interface FieldEncryptionPluginOptions {
|
|
|
50
50
|
*/
|
|
51
51
|
envelopePrefix?: string;
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
interface RefreshEncryptedFieldsTarget {
|
|
54
54
|
scope: string;
|
|
55
55
|
table: string;
|
|
56
56
|
fields?: string[];
|
|
@@ -61,17 +61,9 @@ export interface RefreshEncryptedFieldsResult {
|
|
|
61
61
|
rowsUpdated: number;
|
|
62
62
|
fieldsUpdated: number;
|
|
63
63
|
}
|
|
64
|
-
export interface RefreshEncryptedFieldsOptions<DB extends SyncClientDb = SyncClientDb> {
|
|
65
|
-
db: Kysely<DB>;
|
|
66
|
-
rules: FieldEncryptionRule[];
|
|
67
|
-
keys: FieldEncryptionKeys;
|
|
68
|
-
envelopePrefix?: string;
|
|
69
|
-
decryptionErrorMode?: FieldDecryptionErrorMode;
|
|
70
|
-
targets?: RefreshEncryptedFieldsTarget[];
|
|
71
|
-
ctx?: Partial<SyncClientPluginContext>;
|
|
72
|
-
}
|
|
73
64
|
export interface FieldEncryptionPluginRefreshRequest<DB extends SyncClientDb = SyncClientDb> {
|
|
74
65
|
db: Kysely<DB>;
|
|
66
|
+
engine?: Pick<SyncEngine<DB>, 'recordLocalMutations'>;
|
|
75
67
|
targets?: RefreshEncryptedFieldsTarget[];
|
|
76
68
|
ctx?: Partial<SyncClientPluginContext>;
|
|
77
69
|
}
|
|
@@ -82,6 +74,5 @@ export declare function createStaticFieldEncryptionKeys(args: {
|
|
|
82
74
|
keys: Record<string, Uint8Array | string>;
|
|
83
75
|
encryptionKid?: string;
|
|
84
76
|
}): FieldEncryptionKeys;
|
|
85
|
-
export declare function refreshEncryptedFields<DB extends SyncClientDb = SyncClientDb>(options: RefreshEncryptedFieldsOptions<DB>): Promise<RefreshEncryptedFieldsResult>;
|
|
86
77
|
export declare function createFieldEncryptionPlugin(pluginOptions: FieldEncryptionPluginOptions): FieldEncryptionPlugin;
|
|
87
78
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,EACvB,UAAU,EACX,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAU1C,cAAc,eAAe,CAAC;AAI9B,KAAK,wBAAwB,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAE3D,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CACjB,GAAG,EAAE,uBAAuB,EAC5B,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KACjE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,UAAU,4BAA4B;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,IAAI,EAAE,mBAAmB,CAAC;IAC1B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,wBAAwB,CAAC;IAC/C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,4BAA4B;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB;AAeD,MAAM,WAAW,mCAAmC,CAClD,EAAE,SAAS,YAAY,GAAG,YAAY;IAEtC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,MAAM,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,sBAAsB,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,4BAA4B,EAAE,CAAC;IACzC,GAAG,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D,sBAAsB,EAAE,CAAC,EAAE,SAAS,YAAY,GAAG,YAAY,EAC7D,OAAO,EAAE,mCAAmC,CAAC,EAAE,CAAC,KAC7C,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC5C;AA0BD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE;IACpD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,CAAC;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,mBAAmB,CAsBtB;AAqjBD,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,4BAA4B,GAC1C,qBAAqB,CA0MvB"}
|
package/dist/index.js
CHANGED
|
@@ -1,130 +1,12 @@
|
|
|
1
1
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
2
|
+
import { isRecord } from '@syncular/core';
|
|
2
3
|
import { sql } from 'kysely';
|
|
4
|
+
import { base64ToBytes, base64UrlToBytes, bytesToBase64Url, hexToBytes, randomBytes, } from './crypto-utils.js';
|
|
3
5
|
// Re-export key sharing utilities
|
|
4
|
-
export * from './key-sharing';
|
|
6
|
+
export * from './key-sharing.js';
|
|
5
7
|
const DEFAULT_PREFIX = 'dgsync:e2ee:1:';
|
|
6
8
|
const encoder = new TextEncoder();
|
|
7
9
|
const decoder = new TextDecoder();
|
|
8
|
-
// Base64 lookup tables for universal encoding/decoding (works in all runtimes)
|
|
9
|
-
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
10
|
-
const BASE64_LOOKUP = new Uint8Array(256);
|
|
11
|
-
for (let i = 0; i < BASE64_CHARS.length; i++) {
|
|
12
|
-
BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
|
|
13
|
-
}
|
|
14
|
-
function isRecord(value) {
|
|
15
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
-
}
|
|
17
|
-
function randomBytes(length) {
|
|
18
|
-
const cryptoObj = globalThis.crypto;
|
|
19
|
-
if (!cryptoObj?.getRandomValues) {
|
|
20
|
-
throw new Error('Secure random generator is not available (crypto.getRandomValues). ' +
|
|
21
|
-
'Ensure you are running in a secure context or polyfill crypto.');
|
|
22
|
-
}
|
|
23
|
-
const out = new Uint8Array(length);
|
|
24
|
-
cryptoObj.getRandomValues(out);
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Universal base64 encoding that works in all JavaScript runtimes.
|
|
29
|
-
* Uses Buffer for Node/Bun (fast), lookup table for others (RN-compatible).
|
|
30
|
-
*/
|
|
31
|
-
function bytesToBase64(bytes) {
|
|
32
|
-
// Node/Bun fast path
|
|
33
|
-
if (typeof Buffer !== 'undefined') {
|
|
34
|
-
return Buffer.from(bytes).toString('base64');
|
|
35
|
-
}
|
|
36
|
-
// Universal fallback using lookup table (works in RN, browsers, etc.)
|
|
37
|
-
let result = '';
|
|
38
|
-
const len = bytes.length;
|
|
39
|
-
const remainder = len % 3;
|
|
40
|
-
// Process 3 bytes at a time
|
|
41
|
-
for (let i = 0; i < len - remainder; i += 3) {
|
|
42
|
-
const a = bytes[i];
|
|
43
|
-
const b = bytes[i + 1];
|
|
44
|
-
const c = bytes[i + 2];
|
|
45
|
-
result +=
|
|
46
|
-
BASE64_CHARS.charAt((a >> 2) & 0x3f) +
|
|
47
|
-
BASE64_CHARS.charAt(((a << 4) | (b >> 4)) & 0x3f) +
|
|
48
|
-
BASE64_CHARS.charAt(((b << 2) | (c >> 6)) & 0x3f) +
|
|
49
|
-
BASE64_CHARS.charAt(c & 0x3f);
|
|
50
|
-
}
|
|
51
|
-
// Handle remaining bytes
|
|
52
|
-
if (remainder === 1) {
|
|
53
|
-
const a = bytes[len - 1];
|
|
54
|
-
result +=
|
|
55
|
-
BASE64_CHARS.charAt((a >> 2) & 0x3f) +
|
|
56
|
-
BASE64_CHARS.charAt((a << 4) & 0x3f) +
|
|
57
|
-
'==';
|
|
58
|
-
}
|
|
59
|
-
else if (remainder === 2) {
|
|
60
|
-
const a = bytes[len - 2];
|
|
61
|
-
const b = bytes[len - 1];
|
|
62
|
-
result +=
|
|
63
|
-
BASE64_CHARS.charAt((a >> 2) & 0x3f) +
|
|
64
|
-
BASE64_CHARS.charAt(((a << 4) | (b >> 4)) & 0x3f) +
|
|
65
|
-
BASE64_CHARS.charAt((b << 2) & 0x3f) +
|
|
66
|
-
'=';
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Universal base64 decoding that works in all JavaScript runtimes.
|
|
72
|
-
* Uses Buffer for Node/Bun (fast), lookup table for others (RN-compatible).
|
|
73
|
-
*/
|
|
74
|
-
function base64ToBytes(base64) {
|
|
75
|
-
// Node/Bun fast path
|
|
76
|
-
if (typeof Buffer !== 'undefined') {
|
|
77
|
-
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
78
|
-
}
|
|
79
|
-
// Universal fallback using lookup table (works in RN, browsers, etc.)
|
|
80
|
-
// Remove padding and calculate output length
|
|
81
|
-
const len = base64.length;
|
|
82
|
-
let padding = 0;
|
|
83
|
-
if (base64[len - 1] === '=')
|
|
84
|
-
padding++;
|
|
85
|
-
if (base64[len - 2] === '=')
|
|
86
|
-
padding++;
|
|
87
|
-
const outputLen = (len * 3) / 4 - padding;
|
|
88
|
-
const out = new Uint8Array(outputLen);
|
|
89
|
-
let outIdx = 0;
|
|
90
|
-
for (let i = 0; i < len; i += 4) {
|
|
91
|
-
const a = BASE64_LOOKUP[base64.charCodeAt(i)];
|
|
92
|
-
const b = BASE64_LOOKUP[base64.charCodeAt(i + 1)];
|
|
93
|
-
const c = BASE64_LOOKUP[base64.charCodeAt(i + 2)];
|
|
94
|
-
const d = BASE64_LOOKUP[base64.charCodeAt(i + 3)];
|
|
95
|
-
out[outIdx++] = (a << 2) | (b >> 4);
|
|
96
|
-
if (outIdx < outputLen)
|
|
97
|
-
out[outIdx++] = ((b << 4) | (c >> 2)) & 0xff;
|
|
98
|
-
if (outIdx < outputLen)
|
|
99
|
-
out[outIdx++] = ((c << 6) | d) & 0xff;
|
|
100
|
-
}
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
function bytesToBase64Url(bytes) {
|
|
104
|
-
return bytesToBase64(bytes)
|
|
105
|
-
.replace(/\+/g, '-')
|
|
106
|
-
.replace(/\//g, '_')
|
|
107
|
-
.replace(/=+$/g, '');
|
|
108
|
-
}
|
|
109
|
-
function base64UrlToBytes(base64url) {
|
|
110
|
-
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
111
|
-
const padded = base64 + '==='.slice((base64.length + 3) % 4);
|
|
112
|
-
return base64ToBytes(padded);
|
|
113
|
-
}
|
|
114
|
-
function hexToBytes(hex) {
|
|
115
|
-
const normalized = hex.trim().toLowerCase();
|
|
116
|
-
if (normalized.length % 2 !== 0) {
|
|
117
|
-
throw new Error('Invalid hex string (length must be even)');
|
|
118
|
-
}
|
|
119
|
-
const out = new Uint8Array(normalized.length / 2);
|
|
120
|
-
for (let i = 0; i < out.length; i++) {
|
|
121
|
-
const byte = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
122
|
-
if (!Number.isFinite(byte))
|
|
123
|
-
throw new Error('Invalid hex string');
|
|
124
|
-
out[i] = byte;
|
|
125
|
-
}
|
|
126
|
-
return out;
|
|
127
|
-
}
|
|
128
10
|
function decodeKeyMaterial(key) {
|
|
129
11
|
if (key instanceof Uint8Array)
|
|
130
12
|
return key;
|
|
@@ -151,6 +33,9 @@ export function createStaticFieldEncryptionKeys(args) {
|
|
|
151
33
|
if (!raw)
|
|
152
34
|
throw new Error(`Missing encryption key for kid "${kid}"`);
|
|
153
35
|
const decoded = decodeKeyMaterial(raw);
|
|
36
|
+
if (decoded.length !== 32) {
|
|
37
|
+
throw new Error(`Encryption key for kid "${kid}" must be 32 bytes (got ${decoded.length})`);
|
|
38
|
+
}
|
|
154
39
|
cache.set(kid, decoded);
|
|
155
40
|
return decoded;
|
|
156
41
|
},
|
|
@@ -177,11 +62,16 @@ function decodeEnvelope(prefix, value) {
|
|
|
177
62
|
const [kid, nonceB64, ctB64] = parts;
|
|
178
63
|
if (!kid || !nonceB64 || !ctB64)
|
|
179
64
|
return null;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
65
|
+
try {
|
|
66
|
+
return {
|
|
67
|
+
kid,
|
|
68
|
+
nonce: base64UrlToBytes(nonceB64),
|
|
69
|
+
ciphertext: base64UrlToBytes(ctB64),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
185
75
|
}
|
|
186
76
|
async function getKeyOrThrow(keys, kid) {
|
|
187
77
|
const key = await keys.getKey(kid);
|
|
@@ -255,6 +145,7 @@ async function decryptValue(args) {
|
|
|
255
145
|
function buildRuleIndex(rules) {
|
|
256
146
|
const byScopeTable = new Map();
|
|
257
147
|
const tablesByScope = new Map();
|
|
148
|
+
const scopesByTable = new Map();
|
|
258
149
|
for (const rule of rules) {
|
|
259
150
|
const scope = rule.scope;
|
|
260
151
|
const table = rule.table ?? '*';
|
|
@@ -274,9 +165,12 @@ function buildRuleIndex(rules) {
|
|
|
274
165
|
byScopeTable.set(key, { fields: new Set(rule.fields), rowIdField });
|
|
275
166
|
}
|
|
276
167
|
if (table !== '*') {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
tablesByScope.set(scope,
|
|
168
|
+
const tables = tablesByScope.get(scope) ?? new Set();
|
|
169
|
+
tables.add(table);
|
|
170
|
+
tablesByScope.set(scope, tables);
|
|
171
|
+
const scopes = scopesByTable.get(table) ?? new Set();
|
|
172
|
+
scopes.add(scope);
|
|
173
|
+
scopesByTable.set(table, scopes);
|
|
280
174
|
}
|
|
281
175
|
}
|
|
282
176
|
// Freeze sets to make accidental mutation harder.
|
|
@@ -286,7 +180,7 @@ function buildRuleIndex(rules) {
|
|
|
286
180
|
rowIdField: v.rowIdField,
|
|
287
181
|
});
|
|
288
182
|
}
|
|
289
|
-
return { byScopeTable, tablesByScope };
|
|
183
|
+
return { byScopeTable, tablesByScope, scopesByTable };
|
|
290
184
|
}
|
|
291
185
|
function getRuleConfig(index, args) {
|
|
292
186
|
const exact = index.byScopeTable.get(`${args.scope}\u001f${args.table}`);
|
|
@@ -295,6 +189,26 @@ function getRuleConfig(index, args) {
|
|
|
295
189
|
const wildcard = index.byScopeTable.get(`${args.scope}\u001f*`);
|
|
296
190
|
return wildcard ?? null;
|
|
297
191
|
}
|
|
192
|
+
function resolveScopeAndTable(args) {
|
|
193
|
+
const direct = getRuleConfig(args.index, {
|
|
194
|
+
scope: args.identifier,
|
|
195
|
+
table: args.identifier,
|
|
196
|
+
});
|
|
197
|
+
if (direct) {
|
|
198
|
+
return { scope: args.identifier, table: args.identifier };
|
|
199
|
+
}
|
|
200
|
+
const tablesForScope = args.index.tablesByScope.get(args.identifier);
|
|
201
|
+
if (tablesForScope && tablesForScope.size === 1) {
|
|
202
|
+
const table = Array.from(tablesForScope)[0];
|
|
203
|
+
return { scope: args.identifier, table };
|
|
204
|
+
}
|
|
205
|
+
const scopesForTable = args.index.scopesByTable.get(args.identifier);
|
|
206
|
+
if (scopesForTable && scopesForTable.size === 1) {
|
|
207
|
+
const scope = Array.from(scopesForTable)[0];
|
|
208
|
+
return { scope, table: args.identifier };
|
|
209
|
+
}
|
|
210
|
+
return { scope: args.identifier, table: args.identifier };
|
|
211
|
+
}
|
|
298
212
|
function inferSnapshotTable(args) {
|
|
299
213
|
if (isRecord(args.row)) {
|
|
300
214
|
const tn = args.row.table_name;
|
|
@@ -442,7 +356,7 @@ function resolveRefreshTargets(args) {
|
|
|
442
356
|
fields: Array.from(target.fields),
|
|
443
357
|
}));
|
|
444
358
|
}
|
|
445
|
-
|
|
359
|
+
async function refreshEncryptedFields(options) {
|
|
446
360
|
const prefix = options.envelopePrefix ?? DEFAULT_PREFIX;
|
|
447
361
|
if (!prefix.endsWith(':')) {
|
|
448
362
|
throw new Error('RefreshEncryptedFieldsOptions.envelopePrefix must end with ":"');
|
|
@@ -468,6 +382,7 @@ export async function refreshEncryptedFields(options) {
|
|
|
468
382
|
let rowsScanned = 0;
|
|
469
383
|
let rowsUpdated = 0;
|
|
470
384
|
let fieldsUpdated = 0;
|
|
385
|
+
const updatedRows = [];
|
|
471
386
|
await options.db.transaction().execute(async (trx) => {
|
|
472
387
|
for (const target of targets) {
|
|
473
388
|
const columns = [target.rowIdField, ...target.fields];
|
|
@@ -530,9 +445,21 @@ export async function refreshEncryptedFields(options) {
|
|
|
530
445
|
`.execute(trx);
|
|
531
446
|
rowsUpdated += 1;
|
|
532
447
|
fieldsUpdated += changedFields;
|
|
448
|
+
updatedRows.push({ table: target.table, rowId });
|
|
533
449
|
}
|
|
534
450
|
}
|
|
535
451
|
});
|
|
452
|
+
if (updatedRows.length > 0 && options.engine) {
|
|
453
|
+
const deduped = new Map();
|
|
454
|
+
for (const row of updatedRows) {
|
|
455
|
+
deduped.set(`${row.table}\u001f${row.rowId}`, row);
|
|
456
|
+
}
|
|
457
|
+
options.engine.recordLocalMutations(Array.from(deduped.values()).map((row) => ({
|
|
458
|
+
table: row.table,
|
|
459
|
+
rowId: row.rowId,
|
|
460
|
+
op: 'upsert',
|
|
461
|
+
})));
|
|
462
|
+
}
|
|
536
463
|
return {
|
|
537
464
|
tablesProcessed: targets.length,
|
|
538
465
|
rowsScanned,
|
|
@@ -552,6 +479,7 @@ export function createFieldEncryptionPlugin(pluginOptions) {
|
|
|
552
479
|
name,
|
|
553
480
|
refreshEncryptedFields: (options) => refreshEncryptedFields({
|
|
554
481
|
db: options.db,
|
|
482
|
+
engine: options.engine,
|
|
555
483
|
rules: pluginOptions.rules,
|
|
556
484
|
keys: pluginOptions.keys,
|
|
557
485
|
envelopePrefix: prefix,
|
|
@@ -570,6 +498,10 @@ export function createFieldEncryptionPlugin(pluginOptions) {
|
|
|
570
498
|
if (!op.payload)
|
|
571
499
|
return op;
|
|
572
500
|
const payload = op.payload;
|
|
501
|
+
const target = resolveScopeAndTable({
|
|
502
|
+
index,
|
|
503
|
+
identifier: op.table,
|
|
504
|
+
});
|
|
573
505
|
const nextPayload = await transformRecordFields({
|
|
574
506
|
ctx,
|
|
575
507
|
index,
|
|
@@ -577,8 +509,8 @@ export function createFieldEncryptionPlugin(pluginOptions) {
|
|
|
577
509
|
prefix,
|
|
578
510
|
decryptionErrorMode,
|
|
579
511
|
mode: 'encrypt',
|
|
580
|
-
scope:
|
|
581
|
-
table:
|
|
512
|
+
scope: target.scope,
|
|
513
|
+
table: target.table,
|
|
582
514
|
rowId: op.row_id,
|
|
583
515
|
record: payload,
|
|
584
516
|
});
|
|
@@ -604,6 +536,10 @@ export function createFieldEncryptionPlugin(pluginOptions) {
|
|
|
604
536
|
return r;
|
|
605
537
|
if (!isRecord(r.server_row))
|
|
606
538
|
return r;
|
|
539
|
+
const target = resolveScopeAndTable({
|
|
540
|
+
index,
|
|
541
|
+
identifier: op.table,
|
|
542
|
+
});
|
|
607
543
|
const nextRow = await transformRecordFields({
|
|
608
544
|
ctx,
|
|
609
545
|
index,
|
|
@@ -611,8 +547,8 @@ export function createFieldEncryptionPlugin(pluginOptions) {
|
|
|
611
547
|
prefix,
|
|
612
548
|
decryptionErrorMode,
|
|
613
549
|
mode: 'decrypt',
|
|
614
|
-
scope:
|
|
615
|
-
table:
|
|
550
|
+
scope: target.scope,
|
|
551
|
+
table: target.table,
|
|
616
552
|
rowId: op.row_id,
|
|
617
553
|
record: r.server_row,
|
|
618
554
|
});
|
|
@@ -672,6 +608,10 @@ export function createFieldEncryptionPlugin(pluginOptions) {
|
|
|
672
608
|
return change;
|
|
673
609
|
if (!isRecord(change.row_json))
|
|
674
610
|
return change;
|
|
611
|
+
const target = resolveScopeAndTable({
|
|
612
|
+
index,
|
|
613
|
+
identifier: change.table,
|
|
614
|
+
});
|
|
675
615
|
const nextRow = await transformRecordFields({
|
|
676
616
|
ctx,
|
|
677
617
|
index,
|
|
@@ -679,8 +619,8 @@ export function createFieldEncryptionPlugin(pluginOptions) {
|
|
|
679
619
|
prefix,
|
|
680
620
|
decryptionErrorMode,
|
|
681
621
|
mode: 'decrypt',
|
|
682
|
-
scope:
|
|
683
|
-
table:
|
|
622
|
+
scope: target.scope,
|
|
623
|
+
table: target.table,
|
|
684
624
|
rowId: change.row_id,
|
|
685
625
|
record: change.row_json,
|
|
686
626
|
});
|