@syncular/client-plugin-encryption 0.0.1-100

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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Key sharing utilities for human-friendly key exchange.
3
+ *
4
+ * Supports:
5
+ * - BIP39 mnemonic phrases (24 words for 32-byte keys)
6
+ * - URL-safe encoding for QR codes
7
+ * - X25519 keypairs for asymmetric key wrapping
8
+ */
9
+ /**
10
+ * Generate a cryptographically secure 32-byte symmetric key.
11
+ */
12
+ export declare function generateSymmetricKey(): Uint8Array;
13
+ /**
14
+ * Convert a 32-byte key to a 24-word BIP39 mnemonic phrase.
15
+ *
16
+ * The same key always produces the same words (deterministic).
17
+ */
18
+ export declare function keyToMnemonic(key: Uint8Array): string;
19
+ /**
20
+ * Parse a BIP39 mnemonic phrase back to key bytes.
21
+ *
22
+ * @throws If the mnemonic is invalid or has wrong word count
23
+ */
24
+ export declare function normalizeMnemonicInput(phrase: string): string;
25
+ export declare function mnemonicToKey(phrase: string): Uint8Array;
26
+ /**
27
+ * Encode a key as URL-safe base64 (for QR codes).
28
+ */
29
+ export declare function keyToBase64Url(key: Uint8Array): string;
30
+ /**
31
+ * Decode URL-safe base64 back to key bytes.
32
+ */
33
+ export declare function base64UrlToKey(encoded: string): Uint8Array;
34
+ interface KeyPair {
35
+ publicKey: Uint8Array;
36
+ privateKey: Uint8Array;
37
+ }
38
+ /**
39
+ * Generate an X25519 keypair for key exchange.
40
+ *
41
+ * Store privateKey securely. Share publicKey freely.
42
+ */
43
+ export declare function generateKeypair(): KeyPair;
44
+ /**
45
+ * Convert a 32-byte public key to a 24-word mnemonic.
46
+ */
47
+ export declare function publicKeyToMnemonic(publicKey: Uint8Array): string;
48
+ /**
49
+ * Parse a mnemonic back to a public key.
50
+ */
51
+ export declare function mnemonicToPublicKey(phrase: string): Uint8Array;
52
+ interface WrappedKey {
53
+ /** Sender's ephemeral public key (32 bytes) */
54
+ ephemeralPublic: Uint8Array;
55
+ /** Encrypted symmetric key + auth tag (48 bytes) */
56
+ ciphertext: Uint8Array;
57
+ }
58
+ /**
59
+ * Wrap a symmetric key for a recipient using their public key.
60
+ *
61
+ * Uses X25519 ECDH + HKDF + XChaCha20-Poly1305.
62
+ */
63
+ export declare function wrapKeyForRecipient(recipientPublicKey: Uint8Array, symmetricKey: Uint8Array): WrappedKey;
64
+ /**
65
+ * Unwrap a key using your private key.
66
+ */
67
+ export declare function unwrapKey(myPrivateKey: Uint8Array, wrapped: WrappedKey): Uint8Array;
68
+ /**
69
+ * Serialize a wrapped key for storage/transmission.
70
+ */
71
+ export declare function encodeWrappedKey(wrapped: WrappedKey): string;
72
+ /**
73
+ * Deserialize a wrapped key.
74
+ */
75
+ export declare function decodeWrappedKey(encoded: string): WrappedKey;
76
+ interface SymmetricKeyShare {
77
+ type: 'symmetric';
78
+ key: Uint8Array;
79
+ kid?: string;
80
+ }
81
+ interface PublicKeyShare {
82
+ type: 'publicKey';
83
+ publicKey: Uint8Array;
84
+ }
85
+ type ParsedShare = SymmetricKeyShare | PublicKeyShare;
86
+ /**
87
+ * Encode a symmetric key as a shareable URL.
88
+ *
89
+ * Format: sync://k/1/<base64url>[/<kid>]
90
+ */
91
+ export declare function keyToShareUrl(key: Uint8Array, kid?: string): string;
92
+ /**
93
+ * Encode a public key as a shareable URL.
94
+ *
95
+ * Format: sync://pk/1/<base64url>
96
+ */
97
+ export declare function publicKeyToShareUrl(publicKey: Uint8Array): string;
98
+ /**
99
+ * Parse a share URL back to typed result.
100
+ */
101
+ export declare function parseShareUrl(url: string): ParsedShare;
102
+ interface SymmetricKeyJson {
103
+ type: 'symmetric';
104
+ kid?: string;
105
+ k: string;
106
+ }
107
+ interface PublicKeyJson {
108
+ type: 'publicKey';
109
+ pk: string;
110
+ }
111
+ /**
112
+ * Encode a symmetric key as JSON.
113
+ */
114
+ export declare function keyToJson(key: Uint8Array, kid?: string): SymmetricKeyJson;
115
+ /**
116
+ * Encode a public key as JSON.
117
+ */
118
+ export declare function publicKeyToJson(publicKey: Uint8Array): PublicKeyJson;
119
+ /**
120
+ * Parse a JSON key share string.
121
+ */
122
+ export declare function parseKeyShareJson(json: string): ParsedShare;
123
+ export {};
124
+ //# sourceMappingURL=key-sharing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-sharing.d.ts","sourceRoot":"","sources":["../src/key-sharing.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAqDH;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAKrD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwB7D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CASxD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAM1D;AAMD,UAAU,OAAO;IACf,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAIzC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAKjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAE9D;AAMD,UAAU,UAAU;IAClB,+CAA+C;IAC/C,eAAe,EAAE,UAAU,CAAC;IAC5B,oDAAoD;IACpD,UAAU,EAAE,UAAU,CAAC;CACxB;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,kBAAkB,EAAE,UAAU,EAC9B,YAAY,EAAE,UAAU,GACvB,UAAU,CAuCZ;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,YAAY,EAAE,UAAU,EACxB,OAAO,EAAE,UAAU,GAClB,UAAU,CAsCZ;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAG5D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAW5D;AAQD,UAAU,iBAAiB;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,UAAU,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,KAAK,WAAW,GAAG,iBAAiB,GAAG,cAAc,CAAC;AAEtD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAInE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAGjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAkCtD;AAMD,UAAU,gBAAgB;IACxB,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAMzE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,UAAU,GAAG,aAAa,CAKpE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CA0C3D"}
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Key sharing utilities for human-friendly key exchange.
3
+ *
4
+ * Supports:
5
+ * - BIP39 mnemonic phrases (24 words for 32-byte keys)
6
+ * - URL-safe encoding for QR codes
7
+ * - X25519 keypairs for asymmetric key wrapping
8
+ */
9
+ import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
10
+ import { x25519 } from '@noble/curves/ed25519.js';
11
+ import { hkdf } from '@noble/hashes/hkdf.js';
12
+ import { sha256 } from '@noble/hashes/sha2.js';
13
+ import { entropyToMnemonic, mnemonicToEntropy } from '@scure/bip39';
14
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
15
+ import { isRecord } from '@syncular/core';
16
+ import { base64UrlToBytes, bytesToBase64Url, randomBytes, } from './crypto-utils.js';
17
+ const WORD_SET = new Set(wordlist);
18
+ // ============================================================================
19
+ // Utility Functions
20
+ // ============================================================================
21
+ function concatBytes(...arrays) {
22
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
23
+ const result = new Uint8Array(totalLength);
24
+ let offset = 0;
25
+ for (const arr of arrays) {
26
+ result.set(arr, offset);
27
+ offset += arr.length;
28
+ }
29
+ return result;
30
+ }
31
+ function isAllZero(bytes) {
32
+ for (let i = 0; i < bytes.length; i++) {
33
+ if (bytes[i] !== 0)
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ function validateSharedSecret(sharedSecret) {
39
+ // Reject all-zero shared secrets which indicate a low-order point attack.
40
+ // This can happen if a malicious party provides a small-order public key.
41
+ if (isAllZero(sharedSecret)) {
42
+ throw new Error('X25519 shared secret is all zeros - possible low-order point attack');
43
+ }
44
+ }
45
+ // ============================================================================
46
+ // Symmetric Key Utilities
47
+ // ============================================================================
48
+ /**
49
+ * Generate a cryptographically secure 32-byte symmetric key.
50
+ */
51
+ export function generateSymmetricKey() {
52
+ return randomBytes(32);
53
+ }
54
+ /**
55
+ * Convert a 32-byte key to a 24-word BIP39 mnemonic phrase.
56
+ *
57
+ * The same key always produces the same words (deterministic).
58
+ */
59
+ export function keyToMnemonic(key) {
60
+ if (key.length !== 32) {
61
+ throw new Error(`Key must be 32 bytes, got ${key.length}`);
62
+ }
63
+ return entropyToMnemonic(key, wordlist);
64
+ }
65
+ /**
66
+ * Parse a BIP39 mnemonic phrase back to key bytes.
67
+ *
68
+ * @throws If the mnemonic is invalid or has wrong word count
69
+ */
70
+ export function normalizeMnemonicInput(phrase) {
71
+ const normalized = phrase.trim().toLowerCase().replace(/\s+/g, ' ');
72
+ if (!normalized)
73
+ return normalized;
74
+ // Keep valid word tokens only so pasted numbered lists like
75
+ // "1 word 2 word ..." still recover cleanly.
76
+ const tokenizedWords = (normalized.match(/[a-z]+/g) ?? []).filter((word) => WORD_SET.has(word));
77
+ if (tokenizedWords.length === 24)
78
+ return tokenizedWords.join(' ');
79
+ if (tokenizedWords.length > 24) {
80
+ for (let i = 0; i <= tokenizedWords.length - 24; i++) {
81
+ const candidate = tokenizedWords.slice(i, i + 24).join(' ');
82
+ try {
83
+ mnemonicToEntropy(candidate, wordlist);
84
+ return candidate;
85
+ }
86
+ catch {
87
+ // Continue scanning for a valid 24-word window.
88
+ }
89
+ }
90
+ }
91
+ return normalized;
92
+ }
93
+ export function mnemonicToKey(phrase) {
94
+ const normalized = normalizeMnemonicInput(phrase);
95
+ const entropy = mnemonicToEntropy(normalized, wordlist);
96
+ if (entropy.length !== 32) {
97
+ throw new Error(`Expected 24-word mnemonic (32 bytes), got ${entropy.length} bytes`);
98
+ }
99
+ return entropy;
100
+ }
101
+ /**
102
+ * Encode a key as URL-safe base64 (for QR codes).
103
+ */
104
+ export function keyToBase64Url(key) {
105
+ return bytesToBase64Url(key);
106
+ }
107
+ /**
108
+ * Decode URL-safe base64 back to key bytes.
109
+ */
110
+ export function base64UrlToKey(encoded) {
111
+ const key = base64UrlToBytes(encoded);
112
+ if (key.length !== 32) {
113
+ throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
114
+ }
115
+ return key;
116
+ }
117
+ /**
118
+ * Generate an X25519 keypair for key exchange.
119
+ *
120
+ * Store privateKey securely. Share publicKey freely.
121
+ */
122
+ export function generateKeypair() {
123
+ const privateKey = randomBytes(32);
124
+ const publicKey = x25519.getPublicKey(privateKey);
125
+ return { publicKey, privateKey };
126
+ }
127
+ /**
128
+ * Convert a 32-byte public key to a 24-word mnemonic.
129
+ */
130
+ export function publicKeyToMnemonic(publicKey) {
131
+ if (publicKey.length !== 32) {
132
+ throw new Error(`Public key must be 32 bytes, got ${publicKey.length}`);
133
+ }
134
+ return entropyToMnemonic(publicKey, wordlist);
135
+ }
136
+ /**
137
+ * Parse a mnemonic back to a public key.
138
+ */
139
+ export function mnemonicToPublicKey(phrase) {
140
+ return mnemonicToKey(phrase);
141
+ }
142
+ const HKDF_INFO = new TextEncoder().encode('syncular-key-wrap-v1');
143
+ /**
144
+ * Wrap a symmetric key for a recipient using their public key.
145
+ *
146
+ * Uses X25519 ECDH + HKDF + XChaCha20-Poly1305.
147
+ */
148
+ export function wrapKeyForRecipient(recipientPublicKey, symmetricKey) {
149
+ if (recipientPublicKey.length !== 32) {
150
+ throw new Error('Recipient public key must be 32 bytes');
151
+ }
152
+ if (symmetricKey.length !== 32) {
153
+ throw new Error('Symmetric key must be 32 bytes');
154
+ }
155
+ // Generate ephemeral keypair
156
+ const ephemeralPrivate = randomBytes(32);
157
+ const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate);
158
+ // ECDH shared secret
159
+ const sharedSecret = x25519.getSharedSecret(ephemeralPrivate, recipientPublicKey);
160
+ // Reject low-order points that would result in all-zero shared secret
161
+ validateSharedSecret(sharedSecret);
162
+ // Derive wrapping key using HKDF
163
+ const wrappingKey = hkdf(sha256, sharedSecret, ephemeralPublic, HKDF_INFO, 32);
164
+ // Encrypt the symmetric key
165
+ const nonce = randomBytes(24);
166
+ const aead = xchacha20poly1305(wrappingKey, nonce);
167
+ const encrypted = aead.encrypt(symmetricKey);
168
+ // Ciphertext = nonce + encrypted (24 + 32 + 16 = 72 bytes)
169
+ const ciphertext = concatBytes(nonce, encrypted);
170
+ return { ephemeralPublic, ciphertext };
171
+ }
172
+ /**
173
+ * Unwrap a key using your private key.
174
+ */
175
+ export function unwrapKey(myPrivateKey, wrapped) {
176
+ if (myPrivateKey.length !== 32) {
177
+ throw new Error('Private key must be 32 bytes');
178
+ }
179
+ if (wrapped.ephemeralPublic.length !== 32) {
180
+ throw new Error('Ephemeral public key must be 32 bytes');
181
+ }
182
+ if (wrapped.ciphertext.length !== 72) {
183
+ throw new Error('Ciphertext must be 72 bytes (nonce + encrypted key + tag)');
184
+ }
185
+ // ECDH shared secret
186
+ const sharedSecret = x25519.getSharedSecret(myPrivateKey, wrapped.ephemeralPublic);
187
+ // Reject low-order points that would result in all-zero shared secret
188
+ validateSharedSecret(sharedSecret);
189
+ // Derive wrapping key using HKDF
190
+ const wrappingKey = hkdf(sha256, sharedSecret, wrapped.ephemeralPublic, HKDF_INFO, 32);
191
+ // Extract nonce and encrypted data
192
+ const nonce = wrapped.ciphertext.slice(0, 24);
193
+ const encrypted = wrapped.ciphertext.slice(24);
194
+ // Decrypt
195
+ const aead = xchacha20poly1305(wrappingKey, nonce);
196
+ return aead.decrypt(encrypted);
197
+ }
198
+ /**
199
+ * Serialize a wrapped key for storage/transmission.
200
+ */
201
+ export function encodeWrappedKey(wrapped) {
202
+ const combined = concatBytes(wrapped.ephemeralPublic, wrapped.ciphertext);
203
+ return bytesToBase64Url(combined);
204
+ }
205
+ /**
206
+ * Deserialize a wrapped key.
207
+ */
208
+ export function decodeWrappedKey(encoded) {
209
+ const combined = base64UrlToBytes(encoded);
210
+ if (combined.length !== 104) {
211
+ throw new Error(`Invalid wrapped key length: expected 104 bytes, got ${combined.length}`);
212
+ }
213
+ return {
214
+ ephemeralPublic: combined.slice(0, 32),
215
+ ciphertext: combined.slice(32),
216
+ };
217
+ }
218
+ // ============================================================================
219
+ // Share URL Format
220
+ // ============================================================================
221
+ const SHARE_URL_PREFIX = 'sync://';
222
+ /**
223
+ * Encode a symmetric key as a shareable URL.
224
+ *
225
+ * Format: sync://k/1/<base64url>[/<kid>]
226
+ */
227
+ export function keyToShareUrl(key, kid) {
228
+ const encoded = bytesToBase64Url(key);
229
+ const kidPart = kid ? `/${encodeURIComponent(kid)}` : '';
230
+ return `${SHARE_URL_PREFIX}k/1/${encoded}${kidPart}`;
231
+ }
232
+ /**
233
+ * Encode a public key as a shareable URL.
234
+ *
235
+ * Format: sync://pk/1/<base64url>
236
+ */
237
+ export function publicKeyToShareUrl(publicKey) {
238
+ const encoded = bytesToBase64Url(publicKey);
239
+ return `${SHARE_URL_PREFIX}pk/1/${encoded}`;
240
+ }
241
+ /**
242
+ * Parse a share URL back to typed result.
243
+ */
244
+ export function parseShareUrl(url) {
245
+ if (!url.startsWith(SHARE_URL_PREFIX)) {
246
+ throw new Error(`Invalid share URL: must start with ${SHARE_URL_PREFIX}`);
247
+ }
248
+ const rest = url.slice(SHARE_URL_PREFIX.length);
249
+ const parts = rest.split('/');
250
+ if (parts.length < 3) {
251
+ throw new Error('Invalid share URL format');
252
+ }
253
+ const [type, version, encoded, kidEncoded] = parts;
254
+ if (version !== '1') {
255
+ throw new Error(`Unsupported share URL version: ${version}`);
256
+ }
257
+ if (!encoded) {
258
+ throw new Error('Invalid share URL: missing encoded key data');
259
+ }
260
+ if (type === 'k') {
261
+ const key = base64UrlToKey(encoded);
262
+ const kid = kidEncoded ? decodeURIComponent(kidEncoded) : undefined;
263
+ return { type: 'symmetric', key, kid };
264
+ }
265
+ if (type === 'pk') {
266
+ const publicKey = base64UrlToKey(encoded);
267
+ return { type: 'publicKey', publicKey };
268
+ }
269
+ throw new Error(`Unknown share URL type: ${type}`);
270
+ }
271
+ /**
272
+ * Encode a symmetric key as JSON.
273
+ */
274
+ export function keyToJson(key, kid) {
275
+ return {
276
+ type: 'symmetric',
277
+ ...(kid && { kid }),
278
+ k: bytesToBase64Url(key),
279
+ };
280
+ }
281
+ /**
282
+ * Encode a public key as JSON.
283
+ */
284
+ export function publicKeyToJson(publicKey) {
285
+ return {
286
+ type: 'publicKey',
287
+ pk: bytesToBase64Url(publicKey),
288
+ };
289
+ }
290
+ /**
291
+ * Parse a JSON key share string.
292
+ */
293
+ export function parseKeyShareJson(json) {
294
+ let parsedValue;
295
+ try {
296
+ parsedValue = JSON.parse(json);
297
+ }
298
+ catch {
299
+ throw new Error('Invalid key share JSON');
300
+ }
301
+ if (!isRecord(parsedValue)) {
302
+ throw new Error('Invalid key share JSON');
303
+ }
304
+ const parsedType = parsedValue.type;
305
+ if (parsedType === 'symmetric') {
306
+ const keyMaterial = parsedValue.k;
307
+ if (typeof keyMaterial !== 'string') {
308
+ throw new Error('Invalid symmetric key share JSON');
309
+ }
310
+ const kidValue = parsedValue.kid;
311
+ if (kidValue !== undefined && typeof kidValue !== 'string') {
312
+ throw new Error('Invalid symmetric key share JSON');
313
+ }
314
+ return {
315
+ type: 'symmetric',
316
+ key: base64UrlToKey(keyMaterial),
317
+ kid: kidValue,
318
+ };
319
+ }
320
+ if (parsedType === 'publicKey') {
321
+ const publicKeyMaterial = parsedValue.pk;
322
+ if (typeof publicKeyMaterial !== 'string') {
323
+ throw new Error('Invalid public key share JSON');
324
+ }
325
+ return {
326
+ type: 'publicKey',
327
+ publicKey: base64UrlToKey(publicKeyMaterial),
328
+ };
329
+ }
330
+ throw new Error(`Unknown key share type: ${String(parsedType)}`);
331
+ }
332
+ //# sourceMappingURL=key-sharing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-sharing.js","sourceRoot":"","sources":["../src/key-sharing.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@syncular/client-plugin-encryption",
3
+ "version": "0.0.1-100",
4
+ "description": "End-to-end encryption plugin for the Syncular client",
5
+ "license": "MIT",
6
+ "author": "Benjamin Kniffler",
7
+ "homepage": "https://syncular.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/syncular/syncular.git",
11
+ "directory": "packages/client-plugin-encryption"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/syncular/syncular/issues"
15
+ },
16
+ "keywords": [
17
+ "sync",
18
+ "offline-first",
19
+ "realtime",
20
+ "database",
21
+ "typescript",
22
+ "encryption",
23
+ "e2ee",
24
+ "security"
25
+ ],
26
+ "private": false,
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "bun": "./src/index.ts",
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ }
38
+ }
39
+ },
40
+ "scripts": {
41
+ "test": "bun test --pass-with-no-tests",
42
+ "tsgo": "tsgo --noEmit",
43
+ "build": "tsgo",
44
+ "release": "bunx syncular-publish"
45
+ },
46
+ "dependencies": {
47
+ "@noble/ciphers": "^2.1.1",
48
+ "@noble/curves": "^2.0.1",
49
+ "@noble/hashes": "^2.0.1",
50
+ "@scure/bip39": "^2.0.1",
51
+ "@syncular/client": "0.0.1",
52
+ "@syncular/core": "0.0.1"
53
+ },
54
+ "devDependencies": {
55
+ "@syncular/config": "0.0.0",
56
+ "kysely-bun-sqlite": "^0.4.0"
57
+ },
58
+ "peerDependencies": {
59
+ "kysely": "^0.28.0"
60
+ },
61
+ "files": [
62
+ "dist",
63
+ "src"
64
+ ]
65
+ }
@@ -0,0 +1,68 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { createStaticFieldEncryptionKeys } from '../index';
3
+
4
+ const VALID_ZERO_KEY_BASE64URL = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
5
+
6
+ let originalBuffer: typeof Buffer | undefined;
7
+ let bufferOverridden = false;
8
+
9
+ function disableBufferRuntime(): void {
10
+ originalBuffer = globalThis.Buffer;
11
+ Object.defineProperty(globalThis, 'Buffer', {
12
+ value: undefined,
13
+ writable: true,
14
+ configurable: true,
15
+ });
16
+ bufferOverridden = true;
17
+ }
18
+
19
+ function restoreBufferRuntime(): void {
20
+ if (!bufferOverridden) return;
21
+ Object.defineProperty(globalThis, 'Buffer', {
22
+ value: originalBuffer,
23
+ writable: true,
24
+ configurable: true,
25
+ });
26
+ bufferOverridden = false;
27
+ }
28
+
29
+ afterEach(() => {
30
+ restoreBufferRuntime();
31
+ });
32
+
33
+ describe('createStaticFieldEncryptionKeys', () => {
34
+ test('rejects malformed base64url key material in non-Buffer runtimes', async () => {
35
+ disableBufferRuntime();
36
+
37
+ const keys = createStaticFieldEncryptionKeys({
38
+ keys: { default: '@@@@' },
39
+ });
40
+
41
+ await expect(keys.getKey('default')).rejects.toThrow(
42
+ 'Invalid base64url string'
43
+ );
44
+ });
45
+
46
+ test('rejects wrong-length decoded key material in non-Buffer runtimes', async () => {
47
+ disableBufferRuntime();
48
+
49
+ const keys = createStaticFieldEncryptionKeys({
50
+ keys: { default: 'QQ' },
51
+ });
52
+
53
+ await expect(keys.getKey('default')).rejects.toThrow(
54
+ 'Encryption key for kid "default" must be 32 bytes (got 1)'
55
+ );
56
+ });
57
+
58
+ test('accepts valid 32-byte base64url keys in non-Buffer runtimes', async () => {
59
+ disableBufferRuntime();
60
+
61
+ const keys = createStaticFieldEncryptionKeys({
62
+ keys: { default: VALID_ZERO_KEY_BASE64URL },
63
+ });
64
+
65
+ const decoded = await keys.getKey('default');
66
+ expect(decoded.length).toBe(32);
67
+ });
68
+ });