@agora-sdk/secure-chat-react-js 0.6.4 → 0.7.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.
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { SecureChatStore } from "@agora-sdk/secure-chat-core";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by every {@link EncryptedStore} data operation (`get`/`set`/`delete`/`list`) while the store
|
|
4
|
+
* is locked. Surfacing it (rather than silently returning empty) keeps the store fail-closed: a caller
|
|
5
|
+
* must `unlock(password)` before any persistence happens. Carries no secret material.
|
|
6
|
+
*/
|
|
7
|
+
export declare class StoreLockedError extends Error {
|
|
8
|
+
constructor(message?: string);
|
|
9
|
+
}
|
|
10
|
+
/** Options for {@link createEncryptedStore}. */
|
|
11
|
+
export interface EncryptedStoreOptions {
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A {@link SecureChatStore} decorator that adds at-rest encryption: it seals each VALUE under a
|
|
15
|
+
* password-derived key before delegating to a base store, and opens it on read. Store KEYS pass
|
|
16
|
+
* through in the clear (see the file header for the honest scope). Beyond the four store methods it
|
|
17
|
+
* exposes `unlock`/`lock`/`isLocked`/`changePassword` so the app can drive lock state while the
|
|
18
|
+
* provider still receives a plain `SecureChatStore`.
|
|
19
|
+
*
|
|
20
|
+
* Construct via {@link createEncryptedStore}; do not `new` it directly.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const enc = createEncryptedStore(createIndexedDBStore());
|
|
25
|
+
* await enc.unlock(password); // derive KEK, unwrap/generate DEK
|
|
26
|
+
* <SecureChatProvider store={enc} crypto={crypto} projectId={id} />
|
|
27
|
+
* // later, on logout / idle:
|
|
28
|
+
* enc.lock(); // drops the in-memory DEK/KEK
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class EncryptedStore implements SecureChatStore {
|
|
32
|
+
#private;
|
|
33
|
+
private readonly base;
|
|
34
|
+
/**
|
|
35
|
+
* @param base - The underlying store to seal/open values against (normally `createIndexedDBStore()`).
|
|
36
|
+
* @param _opts - {@link EncryptedStoreOptions} (reserved; no effect today).
|
|
37
|
+
*/
|
|
38
|
+
constructor(base: SecureChatStore, _opts?: EncryptedStoreOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Whether the store is currently locked (no DEK in memory). All data operations throw
|
|
41
|
+
* {@link StoreLockedError} while locked.
|
|
42
|
+
*
|
|
43
|
+
* @returns `true` if locked, `false` once {@link unlock} has succeeded.
|
|
44
|
+
*/
|
|
45
|
+
isLocked(): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Unlock the store with the user's password. On first use (no meta record) this generates a fresh
|
|
48
|
+
* salt + DEK, wraps the DEK under the password-derived KEK, and persists the meta record. On a
|
|
49
|
+
* re-open it derives the KEK from the stored salt and unwraps the existing DEK. After success the
|
|
50
|
+
* (non-extractable) DEK is held in memory and data operations work.
|
|
51
|
+
*
|
|
52
|
+
* @param password - The user's password. Never logged, thrown, or persisted in the clear.
|
|
53
|
+
* @returns A promise that resolves once the DEK is in memory.
|
|
54
|
+
* @throws {Error} A generic "wrong password or corrupt store" error if the meta record exists but
|
|
55
|
+
* the DEK cannot be unwrapped (wrong password OR tampered store — deliberately indistinguishable).
|
|
56
|
+
* The store stays locked on failure.
|
|
57
|
+
*/
|
|
58
|
+
unlock(password: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Lock the store by dropping the in-memory DEK. Subsequent data operations throw
|
|
61
|
+
* {@link StoreLockedError} until {@link unlock} is called again. This is a disk-lock: it does not
|
|
62
|
+
* purge plaintext already cached elsewhere (provider/crypto in-memory state) — that is a later
|
|
63
|
+
* feature.
|
|
64
|
+
*/
|
|
65
|
+
lock(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Change the password without re-encrypting any data: verify/derive against the SAME DEK and re-wrap
|
|
68
|
+
* it under a KEK derived from the new password + a fresh salt, then rewrite the meta record.
|
|
69
|
+
*
|
|
70
|
+
* Requires the store to be unlocked (the in-memory DEK is the source of truth); `oldPassword` is
|
|
71
|
+
* additionally verified by re-deriving its KEK and unwrapping the stored DEK, so a wrong old password
|
|
72
|
+
* fails closed without touching the meta record.
|
|
73
|
+
*
|
|
74
|
+
* @param oldPassword - The current password (verified before any change).
|
|
75
|
+
* @param newPassword - The replacement password.
|
|
76
|
+
* @returns A promise that resolves once the meta record has been rewritten.
|
|
77
|
+
* @throws {StoreLockedError} If the store is locked (unlock first).
|
|
78
|
+
* @throws {Error} A generic "wrong password or corrupt store" error if `oldPassword` does not unwrap
|
|
79
|
+
* the stored DEK. The meta record is left unchanged.
|
|
80
|
+
*/
|
|
81
|
+
changePassword(oldPassword: string, newPassword: string): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Read and open the value at `key`. A miss returns `null`. On any decrypt/authentication failure it
|
|
84
|
+
* THROWS (fail closed) and never returns raw or partially-decrypted bytes.
|
|
85
|
+
*
|
|
86
|
+
* @param key - The store key (passed through to the base store unencrypted).
|
|
87
|
+
* @returns The decrypted bytes, or `null` if the key is absent.
|
|
88
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
89
|
+
* @throws {Error} If the stored blob is malformed or fails AES-GCM authentication (tamper/corruption).
|
|
90
|
+
*/
|
|
91
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Seal `value` and write it at `key`. Format: `[version:1][nonce:12][AES-GCM ciphertext+tag]`, with
|
|
94
|
+
* a fresh CSPRNG nonce per write.
|
|
95
|
+
*
|
|
96
|
+
* @param key - The store key (passed through unencrypted).
|
|
97
|
+
* @param value - The plaintext bytes to seal.
|
|
98
|
+
* @returns A promise that resolves once the sealed blob is persisted.
|
|
99
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
100
|
+
*/
|
|
101
|
+
set(key: string, value: Uint8Array): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Remove `key` from the base store (no-op when absent). Requires the store to be unlocked.
|
|
104
|
+
*
|
|
105
|
+
* @param key - The store key to delete.
|
|
106
|
+
* @returns A promise that resolves once the key is removed.
|
|
107
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
108
|
+
*/
|
|
109
|
+
delete(key: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* List base-store keys starting with `prefix`, with the reserved meta key filtered out so it never
|
|
112
|
+
* surfaces to the repository.
|
|
113
|
+
*
|
|
114
|
+
* @param prefix - The key prefix (`""` for every key).
|
|
115
|
+
* @returns The matching keys, excluding the internal `__enc__` meta record.
|
|
116
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
117
|
+
*/
|
|
118
|
+
list(prefix: string): Promise<string[]>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Wrap a base {@link SecureChatStore} (normally `createIndexedDBStore()`) with at-rest encryption.
|
|
122
|
+
* Returns a locked store: the app must call `unlock(password)` before mounting `<SecureChatProvider>`,
|
|
123
|
+
* and may `lock()` it on logout/idle. Values are sealed with AES-256-GCM under a DEK that is wrapped by
|
|
124
|
+
* an argon2id-derived KEK; store keys pass through in the clear (see this module's header for scope).
|
|
125
|
+
*
|
|
126
|
+
* @param base - The underlying durable store to seal/open values against.
|
|
127
|
+
* @param opts - {@link EncryptedStoreOptions} (reserved; no effect today).
|
|
128
|
+
* @returns A locked {@link EncryptedStore} — call `unlock(password)` to enable persistence.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* import { createIndexedDBStore, createEncryptedStore } from "@agora-sdk/secure-chat-react-js";
|
|
133
|
+
*
|
|
134
|
+
* const store = createEncryptedStore(createIndexedDBStore());
|
|
135
|
+
* await store.unlock(userPassword); // first use mints the DEK; later opens unwrap it
|
|
136
|
+
* // pass `store` to <SecureChatProvider store={store} … />
|
|
137
|
+
* store.lock(); // drop the in-memory DEK when locking the app
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export declare function createEncryptedStore(base: SecureChatStore, opts?: EncryptedStoreOptions): EncryptedStore;
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// Encryption-at-rest decorator for the web SecureChatStore.
|
|
2
|
+
//
|
|
3
|
+
// Where it sits in the blind-server / client-crypto model: everything the secure-chat client persists
|
|
4
|
+
// flows through one `SecureChatStore` key→blob seam — MLS group/ratchet secrets (`group:`), the device
|
|
5
|
+
// signing key (`device`), DECRYPTED message plaintext (`msg:`, the durable history), and delivery
|
|
6
|
+
// cursors. On web the base store is `createIndexedDBStore()`, which writes plaintext readable by any
|
|
7
|
+
// same-origin script or anyone with disk access. The blind server still never sees any of it; this
|
|
8
|
+
// decorator closes the *local* at-rest gap by sealing every VALUE under a password-derived key before
|
|
9
|
+
// it reaches the base store, and opening it on the way out.
|
|
10
|
+
//
|
|
11
|
+
// Key hierarchy (Approach A): a password is stretched with argon2id into a Key-Encryption-Key (KEK,
|
|
12
|
+
// once per unlock), which unwraps a random AES-256-GCM Data-Encryption-Key (DEK) held only as a
|
|
13
|
+
// NON-EXTRACTABLE WebCrypto CryptoKey. Per-value AES-GCM uses the DEK with a fresh random nonce. The
|
|
14
|
+
// AEAD unlock IS the password check — there is no separate stored hash to leak. A password change
|
|
15
|
+
// re-wraps the SAME DEK (no bulk re-encryption), so existing values stay readable.
|
|
16
|
+
//
|
|
17
|
+
// SECURITY POSTURE (honest, per CLAUDE.md §1):
|
|
18
|
+
// • Values are sealed; store KEYS pass through in the clear. Keys leak conversation ids and
|
|
19
|
+
// per-conversation message counts — which the blind server already observes — but never plaintext
|
|
20
|
+
// or key material. Encrypting keys/metadata is a deliberately-deferred later feature.
|
|
21
|
+
// • Fail closed everywhere: locked → every op throws `StoreLockedError`; a wrong password or a
|
|
22
|
+
// tampered value → the AEAD throws and we rethrow a GENERIC error (we never distinguish wrong
|
|
23
|
+
// password from tamper, and never return raw/undecrypted bytes).
|
|
24
|
+
// • Nothing here ever logs, throws, or serializes the password, KEK, DEK, or any plaintext.
|
|
25
|
+
// • All crypto is WebCrypto + @noble/hashes argon2id — no hand-rolled primitives, CSPRNG nonces only.
|
|
26
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
27
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
28
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
29
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
30
|
+
};
|
|
31
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
32
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
33
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
34
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
35
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
36
|
+
};
|
|
37
|
+
var _EncryptedStore_instances, _EncryptedStore_dek, _EncryptedStore_requireUnlocked, _EncryptedStore_deriveKek, _EncryptedStore_unwrapDek, _EncryptedStore_parseMeta;
|
|
38
|
+
import { toBase64, fromBase64 } from "@agora-sdk/secure-chat-core";
|
|
39
|
+
import { argon2idAsync } from "@noble/hashes/argon2.js";
|
|
40
|
+
/**
|
|
41
|
+
* argon2id parameters for deriving the KEK from the password — RFC 9106 "FIRST RECOMMENDED"
|
|
42
|
+
* high-memory profile (m=64 MiB, t=3, p=1) with a 32-byte output, matching
|
|
43
|
+
* `@agora-sdk/secure-chat-crypto`'s backup codec. Unlock is interactive-but-rare, so the ~0.5–1s cost
|
|
44
|
+
* is acceptable and buys strong resistance to offline brute-force of the password if the on-disk store
|
|
45
|
+
* is exfiltrated.
|
|
46
|
+
*/
|
|
47
|
+
const ARGON2_PARAMS = { m: 65536, t: 3, p: 1, dkLen: 32 };
|
|
48
|
+
/** The on-disk format version stamped into the meta record and every sealed value. */
|
|
49
|
+
const VERSION = 1;
|
|
50
|
+
/** Reserved base-store key for the (never-encrypted, never-listed) meta record. */
|
|
51
|
+
const META_KEY = "__enc__";
|
|
52
|
+
/** Random salt length for argon2id (bytes). */
|
|
53
|
+
const SALT_BYTES = 16;
|
|
54
|
+
/** AES-GCM nonce length (bytes) — 96-bit, the WebCrypto/NIST-recommended IV size. */
|
|
55
|
+
const NONCE_BYTES = 12;
|
|
56
|
+
const utf8 = (s) => new TextEncoder().encode(s);
|
|
57
|
+
/**
|
|
58
|
+
* Coerce bytes to a WebCrypto `BufferSource` backed by a plain `ArrayBuffer`. `@noble/hashes` and
|
|
59
|
+
* `subarray` views are typed `Uint8Array<ArrayBufferLike>`, which TS will not accept where WebCrypto
|
|
60
|
+
* wants an `ArrayBuffer`-backed `BufferSource`; copying into a fresh `Uint8Array` guarantees that
|
|
61
|
+
* backing. The copies are small (nonces, the wrapped DEK, per-value ciphertext) — no secret is exposed.
|
|
62
|
+
*/
|
|
63
|
+
const buf = (bytes) => new Uint8Array(bytes);
|
|
64
|
+
/**
|
|
65
|
+
* Thrown by every {@link EncryptedStore} data operation (`get`/`set`/`delete`/`list`) while the store
|
|
66
|
+
* is locked. Surfacing it (rather than silently returning empty) keeps the store fail-closed: a caller
|
|
67
|
+
* must `unlock(password)` before any persistence happens. Carries no secret material.
|
|
68
|
+
*/
|
|
69
|
+
export class StoreLockedError extends Error {
|
|
70
|
+
constructor(message = "secure-chat: store is locked — call unlock(password) first") {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "StoreLockedError";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A {@link SecureChatStore} decorator that adds at-rest encryption: it seals each VALUE under a
|
|
77
|
+
* password-derived key before delegating to a base store, and opens it on read. Store KEYS pass
|
|
78
|
+
* through in the clear (see the file header for the honest scope). Beyond the four store methods it
|
|
79
|
+
* exposes `unlock`/`lock`/`isLocked`/`changePassword` so the app can drive lock state while the
|
|
80
|
+
* provider still receives a plain `SecureChatStore`.
|
|
81
|
+
*
|
|
82
|
+
* Construct via {@link createEncryptedStore}; do not `new` it directly.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const enc = createEncryptedStore(createIndexedDBStore());
|
|
87
|
+
* await enc.unlock(password); // derive KEK, unwrap/generate DEK
|
|
88
|
+
* <SecureChatProvider store={enc} crypto={crypto} projectId={id} />
|
|
89
|
+
* // later, on logout / idle:
|
|
90
|
+
* enc.lock(); // drops the in-memory DEK/KEK
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export class EncryptedStore {
|
|
94
|
+
/**
|
|
95
|
+
* @param base - The underlying store to seal/open values against (normally `createIndexedDBStore()`).
|
|
96
|
+
* @param _opts - {@link EncryptedStoreOptions} (reserved; no effect today).
|
|
97
|
+
*/
|
|
98
|
+
constructor(base, _opts = {}) {
|
|
99
|
+
_EncryptedStore_instances.add(this);
|
|
100
|
+
this.base = base;
|
|
101
|
+
/** The unwrapped, non-extractable per-value AES-GCM key. `null` ⇒ locked. */
|
|
102
|
+
_EncryptedStore_dek.set(this, null);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether the store is currently locked (no DEK in memory). All data operations throw
|
|
106
|
+
* {@link StoreLockedError} while locked.
|
|
107
|
+
*
|
|
108
|
+
* @returns `true` if locked, `false` once {@link unlock} has succeeded.
|
|
109
|
+
*/
|
|
110
|
+
isLocked() {
|
|
111
|
+
return __classPrivateFieldGet(this, _EncryptedStore_dek, "f") === null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Unlock the store with the user's password. On first use (no meta record) this generates a fresh
|
|
115
|
+
* salt + DEK, wraps the DEK under the password-derived KEK, and persists the meta record. On a
|
|
116
|
+
* re-open it derives the KEK from the stored salt and unwraps the existing DEK. After success the
|
|
117
|
+
* (non-extractable) DEK is held in memory and data operations work.
|
|
118
|
+
*
|
|
119
|
+
* @param password - The user's password. Never logged, thrown, or persisted in the clear.
|
|
120
|
+
* @returns A promise that resolves once the DEK is in memory.
|
|
121
|
+
* @throws {Error} A generic "wrong password or corrupt store" error if the meta record exists but
|
|
122
|
+
* the DEK cannot be unwrapped (wrong password OR tampered store — deliberately indistinguishable).
|
|
123
|
+
* The store stays locked on failure.
|
|
124
|
+
*/
|
|
125
|
+
async unlock(password) {
|
|
126
|
+
const metaBytes = await this.base.get(META_KEY);
|
|
127
|
+
if (metaBytes === null) {
|
|
128
|
+
// First unlock: mint a salt + DEK, wrap it under the KEK, persist the meta record.
|
|
129
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
130
|
+
const kek = await __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_deriveKek).call(this, password, salt);
|
|
131
|
+
// Extractable so we can wrap it now; the in-memory handle below is re-imported non-extractable.
|
|
132
|
+
const freshDek = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
133
|
+
"encrypt",
|
|
134
|
+
"decrypt",
|
|
135
|
+
]);
|
|
136
|
+
const wrapNonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
137
|
+
const wrapped = new Uint8Array(await crypto.subtle.wrapKey("raw", freshDek, kek, { name: "AES-GCM", iv: wrapNonce }));
|
|
138
|
+
const meta = {
|
|
139
|
+
version: VERSION,
|
|
140
|
+
kdf: "argon2id",
|
|
141
|
+
kdfParams: { salt: toBase64(salt), m: ARGON2_PARAMS.m, t: ARGON2_PARAMS.t, p: ARGON2_PARAMS.p },
|
|
142
|
+
wrappedDEK: toBase64(wrapped),
|
|
143
|
+
wrapNonce: toBase64(wrapNonce),
|
|
144
|
+
};
|
|
145
|
+
await this.base.set(META_KEY, utf8(JSON.stringify(meta)));
|
|
146
|
+
// Re-derive the in-memory DEK as NON-EXTRACTABLE (the extractable handle is dropped here).
|
|
147
|
+
__classPrivateFieldSet(this, _EncryptedStore_dek, await __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_unwrapDek).call(this, kek, wrapped, wrapNonce), "f");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Re-open: derive the KEK from the stored salt and unwrap the existing DEK.
|
|
151
|
+
const meta = __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_parseMeta).call(this, metaBytes);
|
|
152
|
+
const kek = await __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_deriveKek).call(this, password, fromBase64(meta.kdfParams.salt));
|
|
153
|
+
try {
|
|
154
|
+
__classPrivateFieldSet(this, _EncryptedStore_dek, await __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_unwrapDek).call(this, kek, fromBase64(meta.wrappedDEK), fromBase64(meta.wrapNonce)), "f");
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Wrong password and a tampered wrappedDEK are indistinguishable here, and we keep it that way.
|
|
158
|
+
// Stay locked; surface nothing about the password or the failure cause.
|
|
159
|
+
__classPrivateFieldSet(this, _EncryptedStore_dek, null, "f");
|
|
160
|
+
throw new Error("secure-chat: unlock failed (wrong password or corrupt store)");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Lock the store by dropping the in-memory DEK. Subsequent data operations throw
|
|
165
|
+
* {@link StoreLockedError} until {@link unlock} is called again. This is a disk-lock: it does not
|
|
166
|
+
* purge plaintext already cached elsewhere (provider/crypto in-memory state) — that is a later
|
|
167
|
+
* feature.
|
|
168
|
+
*/
|
|
169
|
+
lock() {
|
|
170
|
+
__classPrivateFieldSet(this, _EncryptedStore_dek, null, "f");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Change the password without re-encrypting any data: verify/derive against the SAME DEK and re-wrap
|
|
174
|
+
* it under a KEK derived from the new password + a fresh salt, then rewrite the meta record.
|
|
175
|
+
*
|
|
176
|
+
* Requires the store to be unlocked (the in-memory DEK is the source of truth); `oldPassword` is
|
|
177
|
+
* additionally verified by re-deriving its KEK and unwrapping the stored DEK, so a wrong old password
|
|
178
|
+
* fails closed without touching the meta record.
|
|
179
|
+
*
|
|
180
|
+
* @param oldPassword - The current password (verified before any change).
|
|
181
|
+
* @param newPassword - The replacement password.
|
|
182
|
+
* @returns A promise that resolves once the meta record has been rewritten.
|
|
183
|
+
* @throws {StoreLockedError} If the store is locked (unlock first).
|
|
184
|
+
* @throws {Error} A generic "wrong password or corrupt store" error if `oldPassword` does not unwrap
|
|
185
|
+
* the stored DEK. The meta record is left unchanged.
|
|
186
|
+
*/
|
|
187
|
+
async changePassword(oldPassword, newPassword) {
|
|
188
|
+
if (__classPrivateFieldGet(this, _EncryptedStore_dek, "f") === null)
|
|
189
|
+
throw new StoreLockedError();
|
|
190
|
+
const metaBytes = await this.base.get(META_KEY);
|
|
191
|
+
if (metaBytes === null)
|
|
192
|
+
throw new Error("secure-chat: cannot change password before first unlock");
|
|
193
|
+
const meta = __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_parseMeta).call(this, metaBytes);
|
|
194
|
+
// Verify the old password by unwrapping the stored DEK with its KEK (we re-wrap THIS exact DEK).
|
|
195
|
+
const oldKek = await __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_deriveKek).call(this, oldPassword, fromBase64(meta.kdfParams.salt));
|
|
196
|
+
let dekToRewrap;
|
|
197
|
+
try {
|
|
198
|
+
// Re-wrap needs an EXTRACTABLE handle, so unwrap extractable here (held only for the wrap below).
|
|
199
|
+
dekToRewrap = await crypto.subtle.unwrapKey("raw", buf(fromBase64(meta.wrappedDEK)), oldKek, { name: "AES-GCM", iv: buf(fromBase64(meta.wrapNonce)) }, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new Error("secure-chat: unlock failed (wrong password or corrupt store)");
|
|
203
|
+
}
|
|
204
|
+
// Derive a new KEK from the new password + a fresh salt, and re-wrap the same DEK under it.
|
|
205
|
+
const newSalt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
206
|
+
const newKek = await __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_deriveKek).call(this, newPassword, newSalt);
|
|
207
|
+
const newWrapNonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
208
|
+
const newWrapped = new Uint8Array(await crypto.subtle.wrapKey("raw", dekToRewrap, newKek, { name: "AES-GCM", iv: newWrapNonce }));
|
|
209
|
+
const newMeta = {
|
|
210
|
+
version: VERSION,
|
|
211
|
+
kdf: "argon2id",
|
|
212
|
+
kdfParams: { salt: toBase64(newSalt), m: ARGON2_PARAMS.m, t: ARGON2_PARAMS.t, p: ARGON2_PARAMS.p },
|
|
213
|
+
wrappedDEK: toBase64(newWrapped),
|
|
214
|
+
wrapNonce: toBase64(newWrapNonce),
|
|
215
|
+
};
|
|
216
|
+
await this.base.set(META_KEY, utf8(JSON.stringify(newMeta)));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Read and open the value at `key`. A miss returns `null`. On any decrypt/authentication failure it
|
|
220
|
+
* THROWS (fail closed) and never returns raw or partially-decrypted bytes.
|
|
221
|
+
*
|
|
222
|
+
* @param key - The store key (passed through to the base store unencrypted).
|
|
223
|
+
* @returns The decrypted bytes, or `null` if the key is absent.
|
|
224
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
225
|
+
* @throws {Error} If the stored blob is malformed or fails AES-GCM authentication (tamper/corruption).
|
|
226
|
+
*/
|
|
227
|
+
async get(key) {
|
|
228
|
+
const dek = __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_requireUnlocked).call(this);
|
|
229
|
+
const sealed = await this.base.get(key);
|
|
230
|
+
if (sealed === null)
|
|
231
|
+
return null;
|
|
232
|
+
if (sealed.length < 1 + NONCE_BYTES || sealed[0] !== VERSION) {
|
|
233
|
+
throw new Error("secure-chat: stored value is malformed (bad header)");
|
|
234
|
+
}
|
|
235
|
+
const nonce = sealed.subarray(1, 1 + NONCE_BYTES);
|
|
236
|
+
const ciphertext = sealed.subarray(1 + NONCE_BYTES);
|
|
237
|
+
let plain;
|
|
238
|
+
try {
|
|
239
|
+
plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf(nonce) }, dek, buf(ciphertext));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// AEAD failure ⇒ wrong key or tampered ciphertext. Fail closed; never return raw bytes.
|
|
243
|
+
throw new Error("secure-chat: value decrypt failed (corrupt or tampered store)");
|
|
244
|
+
}
|
|
245
|
+
return new Uint8Array(plain);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Seal `value` and write it at `key`. Format: `[version:1][nonce:12][AES-GCM ciphertext+tag]`, with
|
|
249
|
+
* a fresh CSPRNG nonce per write.
|
|
250
|
+
*
|
|
251
|
+
* @param key - The store key (passed through unencrypted).
|
|
252
|
+
* @param value - The plaintext bytes to seal.
|
|
253
|
+
* @returns A promise that resolves once the sealed blob is persisted.
|
|
254
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
255
|
+
*/
|
|
256
|
+
async set(key, value) {
|
|
257
|
+
const dek = __classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_requireUnlocked).call(this);
|
|
258
|
+
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
259
|
+
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf(nonce) }, dek, buf(value)));
|
|
260
|
+
const sealed = new Uint8Array(1 + NONCE_BYTES + ct.length);
|
|
261
|
+
sealed[0] = VERSION;
|
|
262
|
+
sealed.set(nonce, 1);
|
|
263
|
+
sealed.set(ct, 1 + NONCE_BYTES);
|
|
264
|
+
await this.base.set(key, sealed);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Remove `key` from the base store (no-op when absent). Requires the store to be unlocked.
|
|
268
|
+
*
|
|
269
|
+
* @param key - The store key to delete.
|
|
270
|
+
* @returns A promise that resolves once the key is removed.
|
|
271
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
272
|
+
*/
|
|
273
|
+
async delete(key) {
|
|
274
|
+
__classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_requireUnlocked).call(this);
|
|
275
|
+
await this.base.delete(key);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* List base-store keys starting with `prefix`, with the reserved meta key filtered out so it never
|
|
279
|
+
* surfaces to the repository.
|
|
280
|
+
*
|
|
281
|
+
* @param prefix - The key prefix (`""` for every key).
|
|
282
|
+
* @returns The matching keys, excluding the internal `__enc__` meta record.
|
|
283
|
+
* @throws {StoreLockedError} If the store is locked.
|
|
284
|
+
*/
|
|
285
|
+
async list(prefix) {
|
|
286
|
+
__classPrivateFieldGet(this, _EncryptedStore_instances, "m", _EncryptedStore_requireUnlocked).call(this);
|
|
287
|
+
const keys = await this.base.list(prefix);
|
|
288
|
+
return keys.filter((k) => k !== META_KEY);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
_EncryptedStore_dek = new WeakMap(), _EncryptedStore_instances = new WeakSet(), _EncryptedStore_requireUnlocked = function _EncryptedStore_requireUnlocked() {
|
|
292
|
+
if (__classPrivateFieldGet(this, _EncryptedStore_dek, "f") === null)
|
|
293
|
+
throw new StoreLockedError();
|
|
294
|
+
return __classPrivateFieldGet(this, _EncryptedStore_dek, "f");
|
|
295
|
+
}, _EncryptedStore_deriveKek =
|
|
296
|
+
/** Stretch the password + salt into a non-extractable AES-GCM KEK with wrap/unwrap usages. */
|
|
297
|
+
async function _EncryptedStore_deriveKek(password, salt) {
|
|
298
|
+
const raw = await argon2idAsync(utf8(password), salt, ARGON2_PARAMS);
|
|
299
|
+
try {
|
|
300
|
+
return await crypto.subtle.importKey("raw", buf(raw), { name: "AES-GCM" }, false, [
|
|
301
|
+
"wrapKey",
|
|
302
|
+
"unwrapKey",
|
|
303
|
+
]);
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
raw.fill(0); // zeroize the raw KEK bytes; the CryptoKey handle is non-extractable.
|
|
307
|
+
}
|
|
308
|
+
}, _EncryptedStore_unwrapDek =
|
|
309
|
+
/** Unwrap the stored DEK under `kek` into a NON-EXTRACTABLE per-value AES-GCM key. */
|
|
310
|
+
async function _EncryptedStore_unwrapDek(kek, wrapped, wrapNonce) {
|
|
311
|
+
return crypto.subtle.unwrapKey("raw", buf(wrapped), kek, { name: "AES-GCM", iv: buf(wrapNonce) }, { name: "AES-GCM", length: 256 }, false, // non-extractable: the DEK bytes can never be read back out of WebCrypto.
|
|
312
|
+
["encrypt", "decrypt"]);
|
|
313
|
+
}, _EncryptedStore_parseMeta = function _EncryptedStore_parseMeta(bytes) {
|
|
314
|
+
let meta;
|
|
315
|
+
try {
|
|
316
|
+
meta = JSON.parse(new TextDecoder().decode(bytes));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
throw new Error("secure-chat: store meta record is malformed");
|
|
320
|
+
}
|
|
321
|
+
if (meta.version !== VERSION ||
|
|
322
|
+
meta.kdf !== "argon2id" ||
|
|
323
|
+
typeof meta.kdfParams?.salt !== "string" ||
|
|
324
|
+
typeof meta.wrappedDEK !== "string" ||
|
|
325
|
+
typeof meta.wrapNonce !== "string") {
|
|
326
|
+
throw new Error("secure-chat: unsupported or malformed store meta record");
|
|
327
|
+
}
|
|
328
|
+
return meta;
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Wrap a base {@link SecureChatStore} (normally `createIndexedDBStore()`) with at-rest encryption.
|
|
332
|
+
* Returns a locked store: the app must call `unlock(password)` before mounting `<SecureChatProvider>`,
|
|
333
|
+
* and may `lock()` it on logout/idle. Values are sealed with AES-256-GCM under a DEK that is wrapped by
|
|
334
|
+
* an argon2id-derived KEK; store keys pass through in the clear (see this module's header for scope).
|
|
335
|
+
*
|
|
336
|
+
* @param base - The underlying durable store to seal/open values against.
|
|
337
|
+
* @param opts - {@link EncryptedStoreOptions} (reserved; no effect today).
|
|
338
|
+
* @returns A locked {@link EncryptedStore} — call `unlock(password)` to enable persistence.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```ts
|
|
342
|
+
* import { createIndexedDBStore, createEncryptedStore } from "@agora-sdk/secure-chat-react-js";
|
|
343
|
+
*
|
|
344
|
+
* const store = createEncryptedStore(createIndexedDBStore());
|
|
345
|
+
* await store.unlock(userPassword); // first use mints the DEK; later opens unwrap it
|
|
346
|
+
* // pass `store` to <SecureChatProvider store={store} … />
|
|
347
|
+
* store.lock(); // drop the in-memory DEK when locking the app
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
export function createEncryptedStore(base, opts = {}) {
|
|
351
|
+
return new EncryptedStore(base, opts);
|
|
352
|
+
}
|
|
353
|
+
//# sourceMappingURL=encrypted-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encrypted-store.js","sourceRoot":"","sources":["../../src/encrypted-store.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export * from "@agora-sdk/secure-chat-core";
|
|
|
2
2
|
export { createWebSecureChatCrypto } from "./crypto-web.js";
|
|
3
3
|
export { createIndexedDBStore } from "./indexeddb-store.js";
|
|
4
4
|
export type { IndexedDBStoreOptions } from "./indexeddb-store.js";
|
|
5
|
+
export { createEncryptedStore, EncryptedStore, StoreLockedError } from "./encrypted-store.js";
|
|
6
|
+
export type { EncryptedStoreOptions } from "./encrypted-store.js";
|
package/dist/esm/index.js
CHANGED
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export * from "@agora-sdk/secure-chat-core";
|
|
7
7
|
export { createWebSecureChatCrypto } from "./crypto-web.js";
|
|
8
8
|
export { createIndexedDBStore } from "./indexeddb-store.js";
|
|
9
|
+
export { createEncryptedStore, EncryptedStore, StoreLockedError } from "./encrypted-store.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,iGAAiG;AACjG,kGAAkG;AAClG,8CAA8C;AAE9C,cAAc,6BAA6B,CAAC;AAE5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,iGAAiG;AACjG,kGAAkG;AAClG,8CAA8C;AAE9C,cAAc,6BAA6B,CAAC;AAE5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agora-sdk/secure-chat-react-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Agora SDK Plus, maintained by Jenova Marie",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"module": "dist/esm/index.js",
|
|
28
28
|
"types": "dist/esm/index.d.ts",
|
|
29
29
|
"type": "module",
|
|
30
|
-
"comment:esm-only": "ESM-only: this web binding depends on
|
|
30
|
+
"comment:esm-only": "ESM-only: this web binding depends on the ESM-only @agora-sdk/secure-chat-crypto/ts-mls core, so a CJS build would never load at runtime. Web/React consumers always bundle (Vite/webpack/Metro) anyway.",
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
@@ -35,11 +35,12 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@agora-sdk/secure-chat-
|
|
38
|
+
"@noble/hashes": "2.0.1",
|
|
39
|
+
"@agora-sdk/secure-chat-core": "0.7.0",
|
|
40
|
+
"@agora-sdk/secure-chat-crypto": "0.7.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"@
|
|
43
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
43
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
45
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
45
46
|
},
|