@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.
- package/README.md +21 -1
- package/dist/index.js +20 -34
- 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 #
|
|
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-
|
|
78
|
+
{ name: "AES-GCM", length: 256 },
|
|
78
79
|
false,
|
|
79
|
-
["
|
|
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
|
|
93
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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"
|