@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 +25 -5
- package/dist/index.d.ts +8 -3
- package/dist/index.js +82 -44
- package/package.json +6 -6
- package/dist/chunk-MDWFWP7Z.js +0 -63
- package/dist/storage.d.ts +0 -8
- package/dist/storage.js +0 -10
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 {
|
|
105
|
+
import { saveCryptoKey, loadCryptoKey, deleteCryptoKey } from 'conseal'
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
| Function | Description |
|
|
109
109
|
|---|---|
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
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 #
|
|
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
|
-
|
|
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-
|
|
78
|
+
{ name: "AES-GCM", length: 256 },
|
|
84
79
|
false,
|
|
85
|
-
["
|
|
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
|
|
99
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3052
|
+
loadCryptoKey,
|
|
3014
3053
|
recoverWithMnemonic,
|
|
3015
3054
|
rekey,
|
|
3016
3055
|
rekeySecretKey,
|
|
3017
|
-
|
|
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.
|
|
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"
|
package/dist/chunk-MDWFWP7Z.js
DELETED
|
@@ -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 };
|