@codesense/conseal 0.2.1 → 0.3.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 CHANGED
@@ -102,14 +102,14 @@ const result = await unseal(key, ciphertext, iv)
102
102
  ### IndexedDB key storage
103
103
 
104
104
  ```ts
105
- import { save, load, remove } from 'conseal/storage'
105
+ import { saveCryptoKey, loadCryptoKey, deleteCryptoKey } from 'conseal'
106
106
  ```
107
107
 
108
108
  | Function | Description |
109
109
  |---|---|
110
- | `save(name, key)` | Persists a CryptoKey to IndexedDB. |
111
- | `load(name)` | Loads a CryptoKey. Returns `null` if not found. |
112
- | `remove(name)` | Deletes a CryptoKey. |
110
+ | `saveCryptoKey(name, key)` | Persists a CryptoKey to IndexedDB. |
111
+ | `loadCryptoKey(name)` | Loads a CryptoKey. Returns `null` if not found. |
112
+ | `deleteCryptoKey(name)` | Deletes a CryptoKey. |
113
113
 
114
114
  ### Utilities
115
115
 
@@ -135,10 +135,30 @@ import { save, load, remove } from 'conseal/storage'
135
135
 
136
136
  ```bash
137
137
  npm install
138
- npm test # run tests
138
+ npm test # unit tests (Vitest + happy-dom)
139
139
  npm run build # build to dist/
140
140
  ```
141
141
 
142
+ ### Cross-browser tests
143
+
144
+ SubtleCrypto behaviour is not identical across engines. The browser suite runs the full test suite in real Chromium, Firefox, and WebKit engines via Playwright.
145
+
146
+ First-time setup — download browser binaries (~300 MB, one-off):
147
+
148
+ ```bash
149
+ npx playwright install
150
+ ```
151
+
152
+ Then run:
153
+
154
+ ```bash
155
+ npm run test:browser
156
+ ```
157
+
158
+ WebKit is the highest-value target: every browser on iOS uses WebKit under the hood regardless of brand, so this provides real Safari/iOS coverage without a device.
159
+
160
+ Both suites run automatically on CI for every push and pull request to `main`.
161
+
142
162
  ## License
143
163
 
144
164
  Dual-licensed under [AGPL-3.0](LICENSE) and a [commercial license](COMMERCIAL-LICENSE.md).
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export { load, remove, save } from './storage.js';
2
-
3
1
  /**
4
2
  * AES-256-GCM symmetric encryption.
5
3
  *
@@ -192,4 +190,11 @@ declare function fromBase64Url(b64url: string): Uint8Array;
192
190
  /** Returns the SHA-256 hash of the input data as an ArrayBuffer. */
193
191
  declare function digest(data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
194
192
 
195
- export { AEK_KEY_ID, type SealedEnvelope, combinePassphraseAndSecretKey, decodeEnvelope, digest, encodeEnvelope, exportPublicKeyAsJwk, fromBase64, fromBase64Url, generateAesKey, generateECDHKeyPair, generateECDSAKeyPair, generateMnemonic, generateSecretKey, importAesKey, importPublicKeyFromJwk, init, recoverWithMnemonic, rekey, rekeySecretKey, seal, sealEnvelope, sealMessage, sign, toBase64, toBase64Url, unseal, unsealEnvelope, unsealMessage, unwrapKey, verify, wrapKey };
193
+ /** Persists a CryptoKey to IndexedDB under the given name. Overwrites if name exists. */
194
+ declare function saveCryptoKey(name: string, key: CryptoKey): Promise<void>;
195
+ /** Loads a CryptoKey from IndexedDB. Returns null if the name is not found. */
196
+ declare function loadCryptoKey(name: string): Promise<CryptoKey | null>;
197
+ /** Removes a CryptoKey from IndexedDB. No-op if the name does not exist. */
198
+ declare function deleteCryptoKey(name: string): Promise<void>;
199
+
200
+ export { AEK_KEY_ID, type SealedEnvelope, combinePassphraseAndSecretKey, decodeEnvelope, deleteCryptoKey, digest, encodeEnvelope, exportPublicKeyAsJwk, fromBase64, fromBase64Url, generateAesKey, generateECDHKeyPair, generateECDSAKeyPair, generateMnemonic, generateSecretKey, importAesKey, importPublicKeyFromJwk, init, loadCryptoKey, recoverWithMnemonic, rekey, rekeySecretKey, saveCryptoKey, seal, sealEnvelope, sealMessage, sign, toBase64, toBase64Url, unseal, unsealEnvelope, unsealMessage, unwrapKey, verify, wrapKey };
package/dist/index.js CHANGED
@@ -1,9 +1,3 @@
1
- import {
2
- load,
3
- remove,
4
- save
5
- } from "./chunk-MDWFWP7Z.js";
6
-
7
1
  // src/aes.ts
8
2
  async function seal(key, plaintext) {
9
3
  const iv = crypto.getRandomValues(new Uint8Array(12));
@@ -69,6 +63,7 @@ async function combinePassphraseAndSecretKey(passphrase, secretKey) {
69
63
  // src/pbkdf2.ts
70
64
  var ITERATIONS = 6e5;
71
65
  var SALT_LENGTH = 16;
66
+ var IV_LENGTH = 12;
72
67
  async function deriveWrappingKey(passphrase, salt) {
73
68
  const keyMaterial = await crypto.subtle.importKey(
74
69
  "raw",
@@ -80,14 +75,21 @@ async function deriveWrappingKey(passphrase, salt) {
80
75
  return crypto.subtle.deriveKey(
81
76
  { name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
82
77
  keyMaterial,
83
- { name: "AES-KW", length: 256 },
78
+ { name: "AES-GCM", length: 256 },
84
79
  false,
85
- ["wrapKey", "unwrapKey"]
80
+ ["encrypt", "decrypt"]
86
81
  );
87
82
  }
88
83
  async function resolvePassphrase(passphrase, secretKey) {
89
84
  return secretKey ? combinePassphraseAndSecretKey(passphrase, secretKey) : passphrase;
90
85
  }
86
+ async function decryptWrappedKey(wrappingKey, wrappedKey, extractable) {
87
+ const bytes = new Uint8Array(wrappedKey);
88
+ const iv = bytes.slice(0, IV_LENGTH);
89
+ const ciphertext = bytes.slice(IV_LENGTH);
90
+ const raw = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, wrappingKey, ciphertext);
91
+ return crypto.subtle.importKey("raw", raw, { name: "AES-GCM", length: 256 }, extractable, ["encrypt", "decrypt"]);
92
+ }
91
93
  async function wrapKey(passphrase, key, secretKey) {
92
94
  if (!key.extractable) {
93
95
  throw new Error("wrapKey: key must be extractable (extractable: true)");
@@ -95,36 +97,23 @@ async function wrapKey(passphrase, key, secretKey) {
95
97
  const effective = await resolvePassphrase(passphrase, secretKey);
96
98
  const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
97
99
  const wrappingKey = await deriveWrappingKey(effective, salt);
98
- const wrappedKey = await crypto.subtle.wrapKey("raw", key, wrappingKey, "AES-KW");
99
- return { wrappedKey, salt };
100
+ const raw = await crypto.subtle.exportKey("raw", key);
101
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
102
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, wrappingKey, raw);
103
+ const wrapped = new Uint8Array(IV_LENGTH + ciphertext.byteLength);
104
+ wrapped.set(iv, 0);
105
+ wrapped.set(new Uint8Array(ciphertext), IV_LENGTH);
106
+ return { wrappedKey: wrapped.buffer, salt };
100
107
  }
101
108
  async function unwrapKey(passphrase, wrappedKey, salt, secretKey) {
102
109
  const effective = await resolvePassphrase(passphrase, secretKey);
103
110
  const wrappingKey = await deriveWrappingKey(effective, salt);
104
- return crypto.subtle.unwrapKey(
105
- "raw",
106
- wrappedKey,
107
- wrappingKey,
108
- "AES-KW",
109
- { name: "AES-GCM", length: 256 },
110
- false,
111
- // extractable: false — safe for IndexedDB storage
112
- ["encrypt", "decrypt"]
113
- );
111
+ return decryptWrappedKey(wrappingKey, wrappedKey, false);
114
112
  }
115
113
  async function rekey(oldPassphrase, newPassphrase, wrappedKey, salt, secretKey) {
116
114
  const effectiveOld = await resolvePassphrase(oldPassphrase, secretKey);
117
115
  const oldWrappingKey = await deriveWrappingKey(effectiveOld, salt);
118
- const aek = await crypto.subtle.unwrapKey(
119
- "raw",
120
- wrappedKey,
121
- oldWrappingKey,
122
- "AES-KW",
123
- { name: "AES-GCM", length: 256 },
124
- true,
125
- // extractable: true — needed so wrapKey() can wrap it again
126
- ["encrypt", "decrypt"]
127
- );
116
+ const aek = await decryptWrappedKey(oldWrappingKey, wrappedKey, true);
128
117
  return wrapKey(newPassphrase, aek, secretKey);
129
118
  }
130
119
  async function rekeySecretKey(passphrase, oldSecretKey, newSecretKey, wrappedKey, salt) {
@@ -133,16 +122,7 @@ async function rekeySecretKey(passphrase, oldSecretKey, newSecretKey, wrappedKey
133
122
  }
134
123
  const effectiveOld = await resolvePassphrase(passphrase, oldSecretKey);
135
124
  const oldWrappingKey = await deriveWrappingKey(effectiveOld, salt);
136
- const aek = await crypto.subtle.unwrapKey(
137
- "raw",
138
- wrappedKey,
139
- oldWrappingKey,
140
- "AES-KW",
141
- { name: "AES-GCM", length: 256 },
142
- true,
143
- // extractable: true — needed so wrapKey() can wrap it again
144
- ["encrypt", "decrypt"]
145
- );
125
+ const aek = await decryptWrappedKey(oldWrappingKey, wrappedKey, true);
146
126
  return wrapKey(passphrase, aek, newSecretKey);
147
127
  }
148
128
 
@@ -209,11 +189,69 @@ async function verify(publicKey, signature, data) {
209
189
  return crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, publicKey, signature, data);
210
190
  }
211
191
 
192
+ // src/storage.ts
193
+ var DB_NAME = "conseal-keys";
194
+ var STORE = "keys";
195
+ var VERSION = 1;
196
+ function openDb() {
197
+ return new Promise((resolve, reject) => {
198
+ const req = indexedDB.open(DB_NAME, VERSION);
199
+ req.onupgradeneeded = () => {
200
+ req.result.createObjectStore(STORE);
201
+ };
202
+ req.onsuccess = () => resolve(req.result);
203
+ req.onerror = () => reject(req.error);
204
+ });
205
+ }
206
+ async function saveCryptoKey(name, key) {
207
+ const db = await openDb();
208
+ try {
209
+ return await new Promise((resolve, reject) => {
210
+ const tx = db.transaction(STORE, "readwrite");
211
+ tx.objectStore(STORE).put(key, name);
212
+ tx.oncomplete = () => resolve();
213
+ tx.onerror = () => reject(tx.error);
214
+ });
215
+ } finally {
216
+ db.close();
217
+ }
218
+ }
219
+ async function loadCryptoKey(name) {
220
+ const db = await openDb();
221
+ try {
222
+ return await new Promise((resolve, reject) => {
223
+ const tx = db.transaction(STORE, "readonly");
224
+ const req = tx.objectStore(STORE).get(name);
225
+ let result = null;
226
+ req.onsuccess = () => {
227
+ result = req.result ?? null;
228
+ };
229
+ tx.oncomplete = () => resolve(result);
230
+ tx.onerror = () => reject(tx.error);
231
+ });
232
+ } finally {
233
+ db.close();
234
+ }
235
+ }
236
+ async function deleteCryptoKey(name) {
237
+ const db = await openDb();
238
+ try {
239
+ return await new Promise((resolve, reject) => {
240
+ const tx = db.transaction(STORE, "readwrite");
241
+ tx.objectStore(STORE).delete(name);
242
+ tx.oncomplete = () => resolve();
243
+ tx.onerror = () => reject(tx.error);
244
+ });
245
+ } finally {
246
+ db.close();
247
+ }
248
+ }
249
+
212
250
  // src/init.ts
213
251
  var AEK_KEY_ID = "aek";
214
252
  async function init(wrappedKey, salt, passphrase, secretKey) {
215
253
  const aek = await unwrapKey(passphrase, wrappedKey, salt, secretKey);
216
- await save(AEK_KEY_ID, aek);
254
+ await saveCryptoKey(AEK_KEY_ID, aek);
217
255
  }
218
256
 
219
257
  // node_modules/@noble/hashes/utils.js
@@ -2997,6 +3035,7 @@ export {
2997
3035
  AEK_KEY_ID,
2998
3036
  combinePassphraseAndSecretKey,
2999
3037
  decodeEnvelope,
3038
+ deleteCryptoKey,
3000
3039
  digest,
3001
3040
  encodeEnvelope,
3002
3041
  exportPublicKeyAsJwk,
@@ -3010,12 +3049,11 @@ export {
3010
3049
  importAesKey,
3011
3050
  importPublicKeyFromJwk,
3012
3051
  init,
3013
- load,
3052
+ loadCryptoKey,
3014
3053
  recoverWithMnemonic,
3015
3054
  rekey,
3016
3055
  rekeySecretKey,
3017
- remove,
3018
- save,
3056
+ saveCryptoKey,
3019
3057
  seal,
3020
3058
  sealEnvelope,
3021
3059
  sealMessage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesense/conseal",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Browser-side zero-knowledge cryptography library using SubtleCrypto.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,16 +9,13 @@
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
- },
13
- "./storage": {
14
- "import": "./dist/storage.js",
15
- "types": "./dist/storage.d.ts"
16
12
  }
17
13
  },
18
14
  "scripts": {
19
15
  "build": "tsup",
20
16
  "test": "vitest run",
21
- "test:watch": "vitest"
17
+ "test:watch": "vitest",
18
+ "test:browser": "vitest run --config vitest.browser.config.ts"
22
19
  },
23
20
  "files": [
24
21
  "dist"
@@ -28,9 +25,12 @@
28
25
  },
29
26
  "devDependencies": {
30
27
  "@scure/bip39": "^2.0.0",
28
+ "@vitest/browser": "^4.1.2",
29
+ "@vitest/browser-playwright": "^4.1.2",
31
30
  "fake-indexeddb": "^6.2.5",
32
31
  "happy-dom": "^20.0.0",
33
32
  "jsdom": "^29.0.1",
33
+ "playwright": "^1.51.0",
34
34
  "tsup": "^8.0.0",
35
35
  "typescript": "^6.0.0",
36
36
  "vitest": "^4.1.2"
@@ -1,63 +0,0 @@
1
- // src/storage.ts
2
- var DB_NAME = "conseal-keys";
3
- var STORE = "keys";
4
- var VERSION = 1;
5
- function openDb() {
6
- return new Promise((resolve, reject) => {
7
- const req = indexedDB.open(DB_NAME, VERSION);
8
- req.onupgradeneeded = () => {
9
- req.result.createObjectStore(STORE);
10
- };
11
- req.onsuccess = () => resolve(req.result);
12
- req.onerror = () => reject(req.error);
13
- });
14
- }
15
- async function save(name, key) {
16
- const db = await openDb();
17
- try {
18
- return await new Promise((resolve, reject) => {
19
- const tx = db.transaction(STORE, "readwrite");
20
- tx.objectStore(STORE).put(key, name);
21
- tx.oncomplete = () => resolve();
22
- tx.onerror = () => reject(tx.error);
23
- });
24
- } finally {
25
- db.close();
26
- }
27
- }
28
- async function load(name) {
29
- const db = await openDb();
30
- try {
31
- return await new Promise((resolve, reject) => {
32
- const tx = db.transaction(STORE, "readonly");
33
- const req = tx.objectStore(STORE).get(name);
34
- let result = null;
35
- req.onsuccess = () => {
36
- result = req.result ?? null;
37
- };
38
- tx.oncomplete = () => resolve(result);
39
- tx.onerror = () => reject(tx.error);
40
- });
41
- } finally {
42
- db.close();
43
- }
44
- }
45
- async function remove(name) {
46
- const db = await openDb();
47
- try {
48
- return await new Promise((resolve, reject) => {
49
- const tx = db.transaction(STORE, "readwrite");
50
- tx.objectStore(STORE).delete(name);
51
- tx.oncomplete = () => resolve();
52
- tx.onerror = () => reject(tx.error);
53
- });
54
- } finally {
55
- db.close();
56
- }
57
- }
58
-
59
- export {
60
- save,
61
- load,
62
- remove
63
- };
package/dist/storage.d.ts DELETED
@@ -1,8 +0,0 @@
1
- /** Persists a CryptoKey to IndexedDB under the given name. Overwrites if name exists. */
2
- declare function save(name: string, key: CryptoKey): Promise<void>;
3
- /** Loads a CryptoKey from IndexedDB. Returns null if the name is not found. */
4
- declare function load(name: string): Promise<CryptoKey | null>;
5
- /** Removes a CryptoKey from IndexedDB. No-op if the name does not exist. */
6
- declare function remove(name: string): Promise<void>;
7
-
8
- export { load, remove, save };
package/dist/storage.js DELETED
@@ -1,10 +0,0 @@
1
- import {
2
- load,
3
- remove,
4
- save
5
- } from "./chunk-MDWFWP7Z.js";
6
- export {
7
- load,
8
- remove,
9
- save
10
- };