@codesense/conseal 0.1.8 → 0.2.1
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/dist/index.d.ts +33 -9
- package/dist/index.js +69 -32
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -30,21 +30,44 @@ declare function generateAesKey(extractable?: boolean): Promise<CryptoKey>;
|
|
|
30
30
|
*/
|
|
31
31
|
declare function importAesKey(raw: ArrayBuffer | Uint8Array, extractable?: boolean): Promise<CryptoKey>;
|
|
32
32
|
|
|
33
|
-
/** Wraps a CryptoKey with a passphrase. The input key must have extractable: true. */
|
|
34
|
-
declare function wrapKey(passphrase: string, key: CryptoKey): Promise<{
|
|
33
|
+
/** Wraps a CryptoKey with a passphrase (and optional Secret Key). The input key must have extractable: true. */
|
|
34
|
+
declare function wrapKey(passphrase: string, key: CryptoKey, secretKey?: Uint8Array): Promise<{
|
|
35
35
|
wrappedKey: ArrayBuffer;
|
|
36
36
|
salt: Uint8Array;
|
|
37
37
|
}>;
|
|
38
38
|
/** Unwraps a CryptoKey. Always returns extractable: false. */
|
|
39
|
-
declare function unwrapKey(passphrase: string, wrappedKey: ArrayBuffer, salt: Uint8Array): Promise<CryptoKey>;
|
|
39
|
+
declare function unwrapKey(passphrase: string, wrappedKey: ArrayBuffer, salt: Uint8Array, secretKey?: Uint8Array): Promise<CryptoKey>;
|
|
40
40
|
/**
|
|
41
41
|
* Changes the passphrase protecting the AEK without re-encrypting any content.
|
|
42
|
-
*
|
|
42
|
+
* The Secret Key (if any) stays the same — it is used on both the unwrap and re-wrap sides.
|
|
43
43
|
*/
|
|
44
|
-
declare function rekey(oldPassphrase: string, newPassphrase: string, wrappedKey: ArrayBuffer, salt: Uint8Array): Promise<{
|
|
44
|
+
declare function rekey(oldPassphrase: string, newPassphrase: string, wrappedKey: ArrayBuffer, salt: Uint8Array, secretKey?: Uint8Array): Promise<{
|
|
45
45
|
wrappedKey: ArrayBuffer;
|
|
46
46
|
salt: Uint8Array;
|
|
47
47
|
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Rotates the Secret Key while keeping the passphrase the same.
|
|
50
|
+
* Use only when the Secret Key is compromised — this is a rare, heavyweight operation.
|
|
51
|
+
* Unwraps with passphrase + oldSecretKey, re-wraps with passphrase + newSecretKey.
|
|
52
|
+
*
|
|
53
|
+
* @param passphrase - the passphrase (unchanged)
|
|
54
|
+
* @param oldSecretKey - the current Secret Key (used to unwrap)
|
|
55
|
+
* @param newSecretKey - the replacement Secret Key (used to re-wrap)
|
|
56
|
+
* @param wrappedKey - the currently wrapped AEK
|
|
57
|
+
* @param salt - the salt used when the AEK was wrapped
|
|
58
|
+
*/
|
|
59
|
+
declare function rekeySecretKey(passphrase: string, oldSecretKey: Uint8Array, newSecretKey: Uint8Array, wrappedKey: ArrayBuffer, salt: Uint8Array): Promise<{
|
|
60
|
+
wrappedKey: ArrayBuffer;
|
|
61
|
+
salt: Uint8Array;
|
|
62
|
+
}>;
|
|
63
|
+
|
|
64
|
+
/** Generates a random 128-bit (16-byte) secret key. */
|
|
65
|
+
declare function generateSecretKey(): Uint8Array;
|
|
66
|
+
/**
|
|
67
|
+
* Combines a passphrase and secret key into a single opaque string for PBKDF2.
|
|
68
|
+
* SHA-256(passphrase + ':' + base64(secretKey)), hex-encoded.
|
|
69
|
+
*/
|
|
70
|
+
declare function combinePassphraseAndSecretKey(passphrase: string, secretKey: Uint8Array): Promise<string>;
|
|
48
71
|
|
|
49
72
|
/** Generates a long-term ECDH P-256 key pair for an account identity. */
|
|
50
73
|
declare function generateECDHKeyPair(): Promise<CryptoKeyPair>;
|
|
@@ -96,10 +119,11 @@ declare function importPublicKeyFromJwk(jwk: JsonWebKey, algorithm: 'ECDH' | 'EC
|
|
|
96
119
|
/** The IndexedDB key id under which the AEK is stored after init(). */
|
|
97
120
|
declare const AEK_KEY_ID = "aek";
|
|
98
121
|
/**
|
|
99
|
-
* Unwraps the AEK with the given passphrase and
|
|
100
|
-
* After this completes, the AEK is available via
|
|
122
|
+
* Unwraps the AEK with the given passphrase (and optional Secret Key) and
|
|
123
|
+
* stores it in IndexedDB. After this completes, the AEK is available via
|
|
124
|
+
* load(AEK_KEY_ID).
|
|
101
125
|
*/
|
|
102
|
-
declare function init(wrappedKey: ArrayBuffer, salt: Uint8Array, passphrase: string): Promise<void>;
|
|
126
|
+
declare function init(wrappedKey: ArrayBuffer, salt: Uint8Array, passphrase: string, secretKey?: Uint8Array): Promise<void>;
|
|
103
127
|
|
|
104
128
|
/** Generates a fresh 24-word BIP-39 mnemonic (256 bits of entropy). */
|
|
105
129
|
declare function generateMnemonic(): string;
|
|
@@ -168,4 +192,4 @@ declare function fromBase64Url(b64url: string): Uint8Array;
|
|
|
168
192
|
/** Returns the SHA-256 hash of the input data as an ArrayBuffer. */
|
|
169
193
|
declare function digest(data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
|
|
170
194
|
|
|
171
|
-
export { AEK_KEY_ID, type SealedEnvelope, decodeEnvelope, digest, encodeEnvelope, exportPublicKeyAsJwk, fromBase64, fromBase64Url, generateAesKey, generateECDHKeyPair, generateECDSAKeyPair, generateMnemonic, importAesKey, importPublicKeyFromJwk, init, recoverWithMnemonic, rekey, seal, sealEnvelope, sealMessage, sign, toBase64, toBase64Url, unseal, unsealEnvelope, unsealMessage, unwrapKey, verify, wrapKey };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,39 @@ async function importAesKey(raw, extractable = false) {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// src/digest.ts
|
|
37
|
+
async function digest(data) {
|
|
38
|
+
return crypto.subtle.digest("SHA-256", data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/base64.ts
|
|
42
|
+
function toBase64(buf) {
|
|
43
|
+
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
44
|
+
let binary = "";
|
|
45
|
+
for (const byte of u8) binary += String.fromCharCode(byte);
|
|
46
|
+
return btoa(binary);
|
|
47
|
+
}
|
|
48
|
+
function fromBase64(b64) {
|
|
49
|
+
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
50
|
+
}
|
|
51
|
+
function toBase64Url(buf) {
|
|
52
|
+
return toBase64(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
53
|
+
}
|
|
54
|
+
function fromBase64Url(b64url) {
|
|
55
|
+
const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
56
|
+
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/secret-key.ts
|
|
60
|
+
function generateSecretKey() {
|
|
61
|
+
return crypto.getRandomValues(new Uint8Array(16));
|
|
62
|
+
}
|
|
63
|
+
async function combinePassphraseAndSecretKey(passphrase, secretKey) {
|
|
64
|
+
const input = `${passphrase}:${toBase64(secretKey)}`;
|
|
65
|
+
const hash = await digest(new TextEncoder().encode(input));
|
|
66
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
67
|
+
}
|
|
68
|
+
|
|
36
69
|
// src/pbkdf2.ts
|
|
37
70
|
var ITERATIONS = 6e5;
|
|
38
71
|
var SALT_LENGTH = 16;
|
|
@@ -52,17 +85,22 @@ async function deriveWrappingKey(passphrase, salt) {
|
|
|
52
85
|
["wrapKey", "unwrapKey"]
|
|
53
86
|
);
|
|
54
87
|
}
|
|
55
|
-
async function
|
|
88
|
+
async function resolvePassphrase(passphrase, secretKey) {
|
|
89
|
+
return secretKey ? combinePassphraseAndSecretKey(passphrase, secretKey) : passphrase;
|
|
90
|
+
}
|
|
91
|
+
async function wrapKey(passphrase, key, secretKey) {
|
|
56
92
|
if (!key.extractable) {
|
|
57
93
|
throw new Error("wrapKey: key must be extractable (extractable: true)");
|
|
58
94
|
}
|
|
95
|
+
const effective = await resolvePassphrase(passphrase, secretKey);
|
|
59
96
|
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
60
|
-
const wrappingKey = await deriveWrappingKey(
|
|
97
|
+
const wrappingKey = await deriveWrappingKey(effective, salt);
|
|
61
98
|
const wrappedKey = await crypto.subtle.wrapKey("raw", key, wrappingKey, "AES-KW");
|
|
62
99
|
return { wrappedKey, salt };
|
|
63
100
|
}
|
|
64
|
-
async function unwrapKey(passphrase, wrappedKey, salt) {
|
|
65
|
-
const
|
|
101
|
+
async function unwrapKey(passphrase, wrappedKey, salt, secretKey) {
|
|
102
|
+
const effective = await resolvePassphrase(passphrase, secretKey);
|
|
103
|
+
const wrappingKey = await deriveWrappingKey(effective, salt);
|
|
66
104
|
return crypto.subtle.unwrapKey(
|
|
67
105
|
"raw",
|
|
68
106
|
wrappedKey,
|
|
@@ -74,8 +112,27 @@ async function unwrapKey(passphrase, wrappedKey, salt) {
|
|
|
74
112
|
["encrypt", "decrypt"]
|
|
75
113
|
);
|
|
76
114
|
}
|
|
77
|
-
async function rekey(oldPassphrase, newPassphrase, wrappedKey, salt) {
|
|
78
|
-
const
|
|
115
|
+
async function rekey(oldPassphrase, newPassphrase, wrappedKey, salt, secretKey) {
|
|
116
|
+
const effectiveOld = await resolvePassphrase(oldPassphrase, secretKey);
|
|
117
|
+
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
|
+
);
|
|
128
|
+
return wrapKey(newPassphrase, aek, secretKey);
|
|
129
|
+
}
|
|
130
|
+
async function rekeySecretKey(passphrase, oldSecretKey, newSecretKey, wrappedKey, salt) {
|
|
131
|
+
if (oldSecretKey.length === newSecretKey.length && oldSecretKey.every((b, i) => b === newSecretKey[i])) {
|
|
132
|
+
throw new Error("rekeySecretKey: oldSecretKey and newSecretKey must be different");
|
|
133
|
+
}
|
|
134
|
+
const effectiveOld = await resolvePassphrase(passphrase, oldSecretKey);
|
|
135
|
+
const oldWrappingKey = await deriveWrappingKey(effectiveOld, salt);
|
|
79
136
|
const aek = await crypto.subtle.unwrapKey(
|
|
80
137
|
"raw",
|
|
81
138
|
wrappedKey,
|
|
@@ -86,7 +143,7 @@ async function rekey(oldPassphrase, newPassphrase, wrappedKey, salt) {
|
|
|
86
143
|
// extractable: true — needed so wrapKey() can wrap it again
|
|
87
144
|
["encrypt", "decrypt"]
|
|
88
145
|
);
|
|
89
|
-
return wrapKey(
|
|
146
|
+
return wrapKey(passphrase, aek, newSecretKey);
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
// src/jwk.ts
|
|
@@ -154,8 +211,8 @@ async function verify(publicKey, signature, data) {
|
|
|
154
211
|
|
|
155
212
|
// src/init.ts
|
|
156
213
|
var AEK_KEY_ID = "aek";
|
|
157
|
-
async function init(wrappedKey, salt, passphrase) {
|
|
158
|
-
const aek = await unwrapKey(passphrase, wrappedKey, salt);
|
|
214
|
+
async function init(wrappedKey, salt, passphrase, secretKey) {
|
|
215
|
+
const aek = await unwrapKey(passphrase, wrappedKey, salt, secretKey);
|
|
159
216
|
await save(AEK_KEY_ID, aek);
|
|
160
217
|
}
|
|
161
218
|
|
|
@@ -2897,24 +2954,6 @@ async function recoverWithMnemonic(mnemonic) {
|
|
|
2897
2954
|
);
|
|
2898
2955
|
}
|
|
2899
2956
|
|
|
2900
|
-
// src/base64.ts
|
|
2901
|
-
function toBase64(buf) {
|
|
2902
|
-
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
2903
|
-
let binary = "";
|
|
2904
|
-
for (const byte of u8) binary += String.fromCharCode(byte);
|
|
2905
|
-
return btoa(binary);
|
|
2906
|
-
}
|
|
2907
|
-
function fromBase64(b64) {
|
|
2908
|
-
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
2909
|
-
}
|
|
2910
|
-
function toBase64Url(buf) {
|
|
2911
|
-
return toBase64(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
2912
|
-
}
|
|
2913
|
-
function fromBase64Url(b64url) {
|
|
2914
|
-
const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
2915
|
-
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
2957
|
// src/envelope.ts
|
|
2919
2958
|
async function sealEnvelope(plaintext, passcode) {
|
|
2920
2959
|
const dek = await crypto.subtle.generateKey(
|
|
@@ -2954,13 +2993,9 @@ function decodeEnvelope(json) {
|
|
|
2954
2993
|
salt: fromBase64(validated.salt)
|
|
2955
2994
|
};
|
|
2956
2995
|
}
|
|
2957
|
-
|
|
2958
|
-
// src/digest.ts
|
|
2959
|
-
async function digest(data) {
|
|
2960
|
-
return crypto.subtle.digest("SHA-256", data);
|
|
2961
|
-
}
|
|
2962
2996
|
export {
|
|
2963
2997
|
AEK_KEY_ID,
|
|
2998
|
+
combinePassphraseAndSecretKey,
|
|
2964
2999
|
decodeEnvelope,
|
|
2965
3000
|
digest,
|
|
2966
3001
|
encodeEnvelope,
|
|
@@ -2971,12 +3006,14 @@ export {
|
|
|
2971
3006
|
generateECDHKeyPair,
|
|
2972
3007
|
generateECDSAKeyPair,
|
|
2973
3008
|
generateMnemonic2 as generateMnemonic,
|
|
3009
|
+
generateSecretKey,
|
|
2974
3010
|
importAesKey,
|
|
2975
3011
|
importPublicKeyFromJwk,
|
|
2976
3012
|
init,
|
|
2977
3013
|
load,
|
|
2978
3014
|
recoverWithMnemonic,
|
|
2979
3015
|
rekey,
|
|
3016
|
+
rekeySecretKey,
|
|
2980
3017
|
remove,
|
|
2981
3018
|
save,
|
|
2982
3019
|
seal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codesense/conseal",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Browser-side zero-knowledge cryptography library using SubtleCrypto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
"node": ">=18"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
+
"@scure/bip39": "^2.0.0",
|
|
30
31
|
"fake-indexeddb": "^6.2.5",
|
|
31
32
|
"happy-dom": "^20.0.0",
|
|
33
|
+
"jsdom": "^29.0.1",
|
|
32
34
|
"tsup": "^8.0.0",
|
|
33
35
|
"typescript": "^6.0.0",
|
|
34
|
-
"vitest": "^4.
|
|
35
|
-
"@scure/bip39": "^2.0.0"
|
|
36
|
+
"vitest": "^4.1.2"
|
|
36
37
|
}
|
|
37
38
|
}
|