@codesense/conseal 0.2.2 → 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.
Files changed (3) hide show
  1. package/README.md +21 -1
  2. package/dist/index.js +20 -34
  3. package/package.json +6 -2
package/README.md CHANGED
@@ -135,10 +135,30 @@ import { saveCryptoKey, loadCryptoKey, deleteCryptoKey } from 'conseal'
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.js CHANGED
@@ -63,6 +63,7 @@ async function combinePassphraseAndSecretKey(passphrase, secretKey) {
63
63
  // src/pbkdf2.ts
64
64
  var ITERATIONS = 6e5;
65
65
  var SALT_LENGTH = 16;
66
+ var IV_LENGTH = 12;
66
67
  async function deriveWrappingKey(passphrase, salt) {
67
68
  const keyMaterial = await crypto.subtle.importKey(
68
69
  "raw",
@@ -74,14 +75,21 @@ async function deriveWrappingKey(passphrase, salt) {
74
75
  return crypto.subtle.deriveKey(
75
76
  { name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
76
77
  keyMaterial,
77
- { name: "AES-KW", length: 256 },
78
+ { name: "AES-GCM", length: 256 },
78
79
  false,
79
- ["wrapKey", "unwrapKey"]
80
+ ["encrypt", "decrypt"]
80
81
  );
81
82
  }
82
83
  async function resolvePassphrase(passphrase, secretKey) {
83
84
  return secretKey ? combinePassphraseAndSecretKey(passphrase, secretKey) : passphrase;
84
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
+ }
85
93
  async function wrapKey(passphrase, key, secretKey) {
86
94
  if (!key.extractable) {
87
95
  throw new Error("wrapKey: key must be extractable (extractable: true)");
@@ -89,36 +97,23 @@ async function wrapKey(passphrase, key, secretKey) {
89
97
  const effective = await resolvePassphrase(passphrase, secretKey);
90
98
  const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
91
99
  const wrappingKey = await deriveWrappingKey(effective, salt);
92
- const wrappedKey = await crypto.subtle.wrapKey("raw", key, wrappingKey, "AES-KW");
93
- 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 };
94
107
  }
95
108
  async function unwrapKey(passphrase, wrappedKey, salt, secretKey) {
96
109
  const effective = await resolvePassphrase(passphrase, secretKey);
97
110
  const wrappingKey = await deriveWrappingKey(effective, salt);
98
- return crypto.subtle.unwrapKey(
99
- "raw",
100
- wrappedKey,
101
- wrappingKey,
102
- "AES-KW",
103
- { name: "AES-GCM", length: 256 },
104
- false,
105
- // extractable: false — safe for IndexedDB storage
106
- ["encrypt", "decrypt"]
107
- );
111
+ return decryptWrappedKey(wrappingKey, wrappedKey, false);
108
112
  }
109
113
  async function rekey(oldPassphrase, newPassphrase, wrappedKey, salt, secretKey) {
110
114
  const effectiveOld = await resolvePassphrase(oldPassphrase, secretKey);
111
115
  const oldWrappingKey = await deriveWrappingKey(effectiveOld, salt);
112
- const aek = await crypto.subtle.unwrapKey(
113
- "raw",
114
- wrappedKey,
115
- oldWrappingKey,
116
- "AES-KW",
117
- { name: "AES-GCM", length: 256 },
118
- true,
119
- // extractable: true — needed so wrapKey() can wrap it again
120
- ["encrypt", "decrypt"]
121
- );
116
+ const aek = await decryptWrappedKey(oldWrappingKey, wrappedKey, true);
122
117
  return wrapKey(newPassphrase, aek, secretKey);
123
118
  }
124
119
  async function rekeySecretKey(passphrase, oldSecretKey, newSecretKey, wrappedKey, salt) {
@@ -127,16 +122,7 @@ async function rekeySecretKey(passphrase, oldSecretKey, newSecretKey, wrappedKey
127
122
  }
128
123
  const effectiveOld = await resolvePassphrase(passphrase, oldSecretKey);
129
124
  const oldWrappingKey = await deriveWrappingKey(effectiveOld, salt);
130
- const aek = await crypto.subtle.unwrapKey(
131
- "raw",
132
- wrappedKey,
133
- oldWrappingKey,
134
- "AES-KW",
135
- { name: "AES-GCM", length: 256 },
136
- true,
137
- // extractable: true — needed so wrapKey() can wrap it again
138
- ["encrypt", "decrypt"]
139
- );
125
+ const aek = await decryptWrappedKey(oldWrappingKey, wrappedKey, true);
140
126
  return wrapKey(passphrase, aek, newSecretKey);
141
127
  }
142
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesense/conseal",
3
- "version": "0.2.2",
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",
@@ -14,7 +14,8 @@
14
14
  "scripts": {
15
15
  "build": "tsup",
16
16
  "test": "vitest run",
17
- "test:watch": "vitest"
17
+ "test:watch": "vitest",
18
+ "test:browser": "vitest run --config vitest.browser.config.ts"
18
19
  },
19
20
  "files": [
20
21
  "dist"
@@ -24,9 +25,12 @@
24
25
  },
25
26
  "devDependencies": {
26
27
  "@scure/bip39": "^2.0.0",
28
+ "@vitest/browser": "^4.1.2",
29
+ "@vitest/browser-playwright": "^4.1.2",
27
30
  "fake-indexeddb": "^6.2.5",
28
31
  "happy-dom": "^20.0.0",
29
32
  "jsdom": "^29.0.1",
33
+ "playwright": "^1.51.0",
30
34
  "tsup": "^8.0.0",
31
35
  "typescript": "^6.0.0",
32
36
  "vitest": "^4.1.2"