@codesense/conseal 0.1.8 → 0.2.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/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
- * Internally unwraps with extractable: true so the key can be immediately re-wrapped.
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 stores it in IndexedDB.
100
- * After this completes, the AEK is available via loadKey(AEK_KEY_ID).
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 wrapKey(passphrase, key) {
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(passphrase, salt);
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 wrappingKey = await deriveWrappingKey(passphrase, salt);
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 oldWrappingKey = await deriveWrappingKey(oldPassphrase, salt);
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(newPassphrase, aek);
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.8",
3
+ "version": "0.2.0",
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.0.0",
35
- "@scure/bip39": "^2.0.0"
36
+ "vitest": "^4.1.2"
36
37
  }
37
38
  }