@agora-sdk/secure-chat-core 0.4.0 → 0.6.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.
Files changed (55) hide show
  1. package/dist/cjs/backup/passphrase-strength.d.ts +22 -0
  2. package/dist/cjs/backup/passphrase-strength.js +75 -0
  3. package/dist/cjs/backup/passphrase-strength.js.map +1 -0
  4. package/dist/cjs/context/secure-chat-context.d.ts +12 -1
  5. package/dist/cjs/context/secure-chat-context.js +3 -1
  6. package/dist/cjs/context/secure-chat-context.js.map +1 -1
  7. package/dist/cjs/hooks/useSecureBackup.d.ts +67 -0
  8. package/dist/cjs/hooks/useSecureBackup.js +207 -0
  9. package/dist/cjs/hooks/useSecureBackup.js.map +1 -0
  10. package/dist/cjs/hooks/useSecureDevice.d.ts +21 -1
  11. package/dist/cjs/hooks/useSecureDevice.js +46 -5
  12. package/dist/cjs/hooks/useSecureDevice.js.map +1 -1
  13. package/dist/cjs/hooks/useSecureMessages.d.ts +16 -3
  14. package/dist/cjs/hooks/useSecureMessages.js +38 -15
  15. package/dist/cjs/hooks/useSecureMessages.js.map +1 -1
  16. package/dist/cjs/hooks/useSecureSafetyNumber.d.ts +30 -0
  17. package/dist/cjs/hooks/useSecureSafetyNumber.js +82 -0
  18. package/dist/cjs/hooks/useSecureSafetyNumber.js.map +1 -0
  19. package/dist/cjs/index.d.ts +12 -1
  20. package/dist/cjs/index.js +16 -1
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/util/padding.d.ts +39 -0
  23. package/dist/cjs/util/padding.js +80 -0
  24. package/dist/cjs/util/padding.js.map +1 -0
  25. package/dist/cjs/util/safety-number.d.ts +27 -0
  26. package/dist/cjs/util/safety-number.js +84 -0
  27. package/dist/cjs/util/safety-number.js.map +1 -0
  28. package/dist/esm/backup/passphrase-strength.d.ts +22 -0
  29. package/dist/esm/backup/passphrase-strength.js +72 -0
  30. package/dist/esm/backup/passphrase-strength.js.map +1 -0
  31. package/dist/esm/context/secure-chat-context.d.ts +12 -1
  32. package/dist/esm/context/secure-chat-context.js +3 -1
  33. package/dist/esm/context/secure-chat-context.js.map +1 -1
  34. package/dist/esm/hooks/useSecureBackup.d.ts +67 -0
  35. package/dist/esm/hooks/useSecureBackup.js +204 -0
  36. package/dist/esm/hooks/useSecureBackup.js.map +1 -0
  37. package/dist/esm/hooks/useSecureDevice.d.ts +21 -1
  38. package/dist/esm/hooks/useSecureDevice.js +46 -5
  39. package/dist/esm/hooks/useSecureDevice.js.map +1 -1
  40. package/dist/esm/hooks/useSecureMessages.d.ts +16 -3
  41. package/dist/esm/hooks/useSecureMessages.js +38 -15
  42. package/dist/esm/hooks/useSecureMessages.js.map +1 -1
  43. package/dist/esm/hooks/useSecureSafetyNumber.d.ts +30 -0
  44. package/dist/esm/hooks/useSecureSafetyNumber.js +79 -0
  45. package/dist/esm/hooks/useSecureSafetyNumber.js.map +1 -0
  46. package/dist/esm/index.d.ts +12 -1
  47. package/dist/esm/index.js +7 -0
  48. package/dist/esm/index.js.map +1 -1
  49. package/dist/esm/util/padding.d.ts +39 -0
  50. package/dist/esm/util/padding.js +75 -0
  51. package/dist/esm/util/padding.js.map +1 -0
  52. package/dist/esm/util/safety-number.d.ts +27 -0
  53. package/dist/esm/util/safety-number.js +81 -0
  54. package/dist/esm/util/safety-number.js.map +1 -0
  55. package/package.json +2 -2
@@ -1,11 +1,24 @@
1
1
  import { SecureMessageModel } from "../contract/index.js";
2
- import { GroupHandle } from "@agora-sdk/secure-chat-crypto";
3
- /** A stored message paired with its decrypted text (when a group handle is available). */
2
+ import { GroupHandle, type SecureDecryptFailureReason } from "@agora-sdk/secure-chat-crypto";
3
+ /**
4
+ * Decryption outcome for a stored message:
5
+ * - `ok` — decrypted + authenticated; `plaintext` is set.
6
+ * - `pending` — not decryptable yet (no group handle, or the message's epoch is ahead of ours); it will
7
+ * be retried when the group advances. `plaintext` is null.
8
+ * - `rejected` — fails closed: the MLS core rejected it (replay, over-window gap, bad auth, malformed,
9
+ * too-old epoch). NEVER retried; `plaintext` is null and `rejectedReason` says why.
10
+ */
11
+ export type SecureMessageStatus = "ok" | "pending" | "rejected";
12
+ /** A stored message paired with its decryption outcome. */
4
13
  export interface DecryptedSecureMessage {
5
14
  /** The raw message row from the server (still holds the base64 ciphertext). */
6
15
  model: SecureMessageModel;
7
- /** Decrypted text, or null when no group handle is available or decryption is pending/failed. */
16
+ /** Decrypted text, or null when {@link DecryptedSecureMessage.status} is `pending` or `rejected`. */
8
17
  plaintext: string | null;
18
+ /** Decryption outcome — drives fail-closed handling and what the UI renders. */
19
+ status: SecureMessageStatus;
20
+ /** When `status` is `rejected`, why the MLS core refused the message. */
21
+ rejectedReason?: SecureDecryptFailureReason;
9
22
  }
10
23
  /** Options for {@link useSecureMessages}. */
11
24
  export interface UseSecureMessagesOptions {
@@ -8,7 +8,9 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.useSecureMessages = useSecureMessages;
10
10
  const react_1 = require("react");
11
+ const secure_chat_crypto_1 = require("@agora-sdk/secure-chat-crypto");
11
12
  const base64_js_1 = require("../util/base64.js");
13
+ const padding_js_1 = require("../util/padding.js");
12
14
  const secure_chat_context_js_1 = require("../context/secure-chat-context.js");
13
15
  /**
14
16
  * Load, decrypt, send, and live-receive messages in one secure conversation.
@@ -28,7 +30,7 @@ const secure_chat_context_js_1 = require("../context/secure-chat-context.js");
28
30
  * ```
29
31
  */
30
32
  function useSecureMessages(conversationId, options = {}) {
31
- const { rest, crypto, socket, repo, resolveGroup, getGroupVersion, subscribeGroupChange } = (0, secure_chat_context_js_1.useSecureChat)();
33
+ const { rest, crypto, socket, repo, resolveGroup, getGroupVersion, subscribeGroupChange, padding } = (0, secure_chat_context_js_1.useSecureChat)();
32
34
  const [messages, setMessages] = (0, react_1.useState)([]);
33
35
  const [before, setBefore] = (0, react_1.useState)(undefined);
34
36
  const [hasMore, setHasMore] = (0, react_1.useState)(true);
@@ -95,15 +97,34 @@ function useSecureMessages(conversationId, options = {}) {
95
97
  };
96
98
  }, [options.senderDeviceId, repo]);
97
99
  const decrypt = (0, react_1.useCallback)(async (model) => {
100
+ // No handle yet (still resolving) → retryable once it arrives.
98
101
  if (!group)
99
- return { model, plaintext: null };
102
+ return { model, plaintext: null, status: "pending" };
103
+ let plaintext;
100
104
  try {
101
- const { plaintext } = await crypto.decryptMessage(group, (0, base64_js_1.fromBase64)(model.ciphertext));
102
- return { model, plaintext: (0, base64_js_1.bytesToUtf8)(plaintext) };
105
+ ({ plaintext } = await crypto.decryptMessage(group, (0, base64_js_1.fromBase64)(model.ciphertext)));
106
+ }
107
+ catch (err) {
108
+ // Classify, don't conflate. A message from an epoch we HAVEN'T reached yet is legitimately
109
+ // buffered (a future Commit will advance us, then this re-decrypts). Anything else that fails
110
+ // at an epoch we HAVE reached is a terminal rejection — the MLS core refused it (replay,
111
+ // over-window gap, bad auth, malformed, too-old epoch). Fail closed: never show it as text and
112
+ // never silently retry it forever (the old behavior masked replays/forgeries as "pending").
113
+ if (BigInt(model.epoch) > group.epoch) {
114
+ return { model, plaintext: null, status: "pending" };
115
+ }
116
+ const rejectedReason = err instanceof secure_chat_crypto_1.SecureChatDecryptError ? err.reason : "unknown";
117
+ return { model, plaintext: null, status: "rejected", rejectedReason };
118
+ }
119
+ // Decrypt + MLS authentication succeeded, so the bytes are from a real group member. Strip the
120
+ // size-bucket padding frame (see util/padding). A bad frame here is NOT a decrypt failure — it's a
121
+ // framing/version mismatch from an authenticated sender — so fail closed as "malformed" rather
122
+ // than rendering raw padded bytes as text.
123
+ try {
124
+ return { model, plaintext: (0, base64_js_1.bytesToUtf8)((0, padding_js_1.unpadPlaintext)(plaintext)), status: "ok" };
103
125
  }
104
126
  catch {
105
- // Buffer/skip: epoch not yet reached, or undecryptable. Surface ciphertext without text.
106
- return { model, plaintext: null };
127
+ return { model, plaintext: null, status: "rejected", rejectedReason: "malformed" };
107
128
  }
108
129
  }, [crypto, group]);
109
130
  const load = (0, react_1.useCallback)(async (reset) => {
@@ -145,30 +166,32 @@ function useSecureMessages(conversationId, options = {}) {
145
166
  throw new Error("Cannot send: no MLS group handle for this conversation.");
146
167
  if (!senderDeviceId)
147
168
  throw new Error("Cannot send: senderDeviceId is required.");
148
- const { ciphertext, epoch } = await crypto.encryptMessage(group, (0, base64_js_1.utf8ToBytes)(text));
169
+ // Pad the plaintext to a size bucket BEFORE encryption so the ciphertext length leaks less
170
+ // (the receiver strips the frame in `decrypt`). See util/padding for the framing.
171
+ const { ciphertext, epoch } = await crypto.encryptMessage(group, (0, padding_js_1.padPlaintext)((0, base64_js_1.utf8ToBytes)(text), padding));
149
172
  const sent = await rest.sendMessage(conversationId, {
150
173
  ciphertext: (0, base64_js_1.toBase64)(ciphertext),
151
174
  epoch: epoch.toString(),
152
175
  senderDeviceId,
153
176
  });
154
177
  // Optimistic: we know our own plaintext without a round-trip through decrypt.
155
- setMessages((prev) => [{ model: sent, plaintext: text }, ...prev]);
156
- }, [crypto, rest, conversationId, group, senderDeviceId]);
178
+ setMessages((prev) => [{ model: sent, plaintext: text, status: "ok" }, ...prev]);
179
+ }, [crypto, rest, conversationId, group, senderDeviceId, padding]);
157
180
  (0, react_1.useEffect)(() => {
158
181
  refresh();
159
182
  // eslint-disable-next-line react-hooks/exhaustive-deps
160
183
  }, [conversationId]);
161
- // Decrypt history that was listed before the group handle resolved. On reload, the first page loads
162
- // while resolveGroup is still in flight, so those rows come back `plaintext: null`; once the handle
163
- // arrives (decrypt is recreated with it), re-decrypt the still-undecrypted rows in place no
164
- // re-fetch, scroll/pagination preserved.
184
+ // Re-decrypt buffered rows when the group handle advances. On reload the first page loads while
185
+ // resolveGroup is still in flight (those rows come back `pending`); a Commit/join also advances the
186
+ // epoch, letting previously-ahead rows decrypt. Retry ONLY `pending` rows never `rejected` ones, so
187
+ // a replay/forgery the core already refused isn't retried on every epoch bump. `ok` rows are kept.
165
188
  (0, react_1.useEffect)(() => {
166
189
  if (!group)
167
190
  return;
168
- if (!messagesRef.current.some((m) => m.plaintext === null))
191
+ if (!messagesRef.current.some((m) => m.status === "pending"))
169
192
  return;
170
193
  let alive = true;
171
- Promise.all(messagesRef.current.map((m) => (m.plaintext === null ? decrypt(m.model) : Promise.resolve(m)))).then((next) => {
194
+ Promise.all(messagesRef.current.map((m) => (m.status === "pending" ? decrypt(m.model) : Promise.resolve(m)))).then((next) => {
172
195
  if (alive)
173
196
  setMessages(next);
174
197
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useSecureMessages.js","sourceRoot":"","sources":["../../../src/hooks/useSecureMessages.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useSecureMessages.js","sourceRoot":"","sources":["../../../src/hooks/useSecureMessages.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { type SafetyNumber } from "../util/safety-number.js";
2
+ /** The state and actions returned by {@link useSecureSafetyNumber}. */
3
+ export interface UseSecureSafetyNumberValues {
4
+ /** The derived safety number, or `null` until ready / when the conversation is not a resolvable DM. */
5
+ safetyNumber: SafetyNumber | null;
6
+ /** True while the number is being (re)computed. */
7
+ loading: boolean;
8
+ /** The last error from resolving the roster or computing, or `null`. */
9
+ error: unknown;
10
+ /** Force a recompute (e.g. after the user re-verifies). */
11
+ refresh: () => void;
12
+ }
13
+ /**
14
+ * Compute the out-of-band key-verification safety number for a direct-message conversation.
15
+ *
16
+ * Resolves the conversation's MLS group, reads its two members' public signature keys, and derives a
17
+ * symmetric {@link SafetyNumber}. Recomputes when the conversation's group handle advances (e.g. a
18
+ * processed Commit changes the roster). Returns `safetyNumber: null` when the group is unresolved or is
19
+ * not a 2-member DM.
20
+ *
21
+ * @param conversationId - The conversation to compute the safety number for.
22
+ * @returns {@link UseSecureSafetyNumberValues}.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const { safetyNumber } = useSecureSafetyNumber(conversationId);
27
+ * // render safetyNumber?.groups for the user to compare with their peer
28
+ * ```
29
+ */
30
+ export declare function useSecureSafetyNumber(conversationId: string): UseSecureSafetyNumberValues;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ // useSecureSafetyNumber — derive the key-verification safety number for a DM conversation.
3
+ //
4
+ // Where it sits in the blind-server model: the server is untrusted and could swap a peer's KeyPackage
5
+ // (active MITM on a TOFU system). This hook reads the two devices' PUBLIC identity keys from the local
6
+ // MLS roster (crypto.exportGroupIdentities) and computes a symmetric, human-comparable safety number
7
+ // (see util/safety-number) the two users confirm out-of-band. Headless: the styled UI lives in the app
8
+ // / demo. DM-focused — for anything other than a 2-member group it returns null rather than guess.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.useSecureSafetyNumber = useSecureSafetyNumber;
11
+ const react_1 = require("react");
12
+ const safety_number_js_1 = require("../util/safety-number.js");
13
+ const secure_chat_context_js_1 = require("../context/secure-chat-context.js");
14
+ /**
15
+ * Compute the out-of-band key-verification safety number for a direct-message conversation.
16
+ *
17
+ * Resolves the conversation's MLS group, reads its two members' public signature keys, and derives a
18
+ * symmetric {@link SafetyNumber}. Recomputes when the conversation's group handle advances (e.g. a
19
+ * processed Commit changes the roster). Returns `safetyNumber: null` when the group is unresolved or is
20
+ * not a 2-member DM.
21
+ *
22
+ * @param conversationId - The conversation to compute the safety number for.
23
+ * @returns {@link UseSecureSafetyNumberValues}.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * const { safetyNumber } = useSecureSafetyNumber(conversationId);
28
+ * // render safetyNumber?.groups for the user to compare with their peer
29
+ * ```
30
+ */
31
+ function useSecureSafetyNumber(conversationId) {
32
+ const { crypto, repo, resolveGroup, getGroupVersion, subscribeGroupChange } = (0, secure_chat_context_js_1.useSecureChat)();
33
+ const [safetyNumber, setSafetyNumber] = (0, react_1.useState)(null);
34
+ const [loading, setLoading] = (0, react_1.useState)(true);
35
+ const [error, setError] = (0, react_1.useState)(null);
36
+ const [tick, setTick] = (0, react_1.useState)(0);
37
+ // Recompute when this conversation's group handle advances (a roster change), without diffing handles.
38
+ const [groupVersion, setGroupVersion] = (0, react_1.useState)(0);
39
+ (0, react_1.useEffect)(() => {
40
+ return subscribeGroupChange(() => setGroupVersion(getGroupVersion(conversationId)));
41
+ }, [subscribeGroupChange, getGroupVersion, conversationId]);
42
+ const refresh = (0, react_1.useCallback)(() => setTick((t) => t + 1), []);
43
+ // Guards the per-run state writes against an out-of-date async resolve (conversation switch / unmount).
44
+ const runIdRef = (0, react_1.useRef)(0);
45
+ (0, react_1.useEffect)(() => {
46
+ const runId = ++runIdRef.current;
47
+ setLoading(true);
48
+ setError(null);
49
+ (async () => {
50
+ const group = await resolveGroup(conversationId);
51
+ if (!group)
52
+ return null;
53
+ const [persisted, members] = await Promise.all([
54
+ repo.loadDevice(),
55
+ crypto.exportGroupIdentities(group),
56
+ ]);
57
+ // DM only: need exactly the local device + one peer. Anything else → no safety number.
58
+ if (members.length !== 2 || !persisted)
59
+ return null;
60
+ const local = members.find((m) => m.deviceId === persisted.deviceId);
61
+ const remote = members.find((m) => m.deviceId !== persisted.deviceId);
62
+ if (!local || !remote)
63
+ return null;
64
+ return (0, safety_number_js_1.computeSafetyNumber)(local, remote);
65
+ })()
66
+ .then((sn) => {
67
+ if (runIdRef.current === runId) {
68
+ setSafetyNumber(sn);
69
+ setLoading(false);
70
+ }
71
+ })
72
+ .catch((err) => {
73
+ if (runIdRef.current === runId) {
74
+ setError(err);
75
+ setSafetyNumber(null);
76
+ setLoading(false);
77
+ }
78
+ });
79
+ }, [crypto, repo, resolveGroup, conversationId, groupVersion, tick]);
80
+ return { safetyNumber, loading, error, refresh };
81
+ }
82
+ //# sourceMappingURL=useSecureSafetyNumber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSecureSafetyNumber.js","sourceRoot":"","sources":["../../../src/hooks/useSecureSafetyNumber.tsx"],"names":[],"mappings":";AAAA,2FAA2F;AAC3F,EAAE;AACF,sGAAsG;AACtG,uGAAuG;AACvG,qGAAqG;AACrG,uGAAuG;AACvG,mGAAmG;;AAmCnG,sDAqDC;AAtFD,iCAAiE;AACjE,+DAAkF;AAClF,8EAAkE;AAclE;;;;;;;;;;;;;;;;GAgBG;AACH,SAAgB,qBAAqB,CAAC,cAAsB;IAC1D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,GAAG,IAAA,sCAAa,GAAE,CAAC;IAE9F,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,IAAA,gBAAQ,EAAsB,IAAI,CAAC,CAAC;IAC5E,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,IAAA,gBAAQ,EAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,IAAA,gBAAQ,EAAU,IAAI,CAAC,CAAC;IAClD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,IAAA,gBAAQ,EAAC,CAAC,CAAC,CAAC;IAEpC,uGAAuG;IACvG,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,IAAA,gBAAQ,EAAC,CAAC,CAAC,CAAC;IACpD,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,OAAO,oBAAoB,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IACtF,CAAC,EAAE,CAAC,oBAAoB,EAAE,eAAe,EAAE,cAAc,CAAC,CAAC,CAAC;IAE5D,MAAM,OAAO,GAAG,IAAA,mBAAW,EAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAE7D,wGAAwG;IACxG,MAAM,QAAQ,GAAG,IAAA,cAAM,EAAC,CAAC,CAAC,CAAC;IAE3B,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,MAAM,KAAK,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC;QACjC,UAAU,CAAC,IAAI,CAAC,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,cAAc,CAAC,CAAC;YACjD,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC;YACxB,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBAC7C,IAAI,CAAC,UAAU,EAAE;gBACjB,MAAM,CAAC,qBAAqB,CAAC,KAAK,CAAC;aACpC,CAAC,CAAC;YACH,uFAAuF;YACvF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAC;YACpD,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,QAAQ,CAAC,CAAC;YACrE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,QAAQ,CAAC,CAAC;YACtE,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAC;YACnC,OAAO,IAAA,sCAAmB,EAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC5C,CAAC,CAAC,EAAE;aACD,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE;YACX,IAAI,QAAQ,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;gBAC/B,eAAe,CAAC,EAAE,CAAC,CAAC;gBACpB,UAAU,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,IAAI,QAAQ,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;gBAC/B,QAAQ,CAAC,GAAG,CAAC,CAAC;gBACd,eAAe,CAAC,IAAI,CAAC,CAAC;gBACtB,UAAU,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,CAAC;IACP,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC;IAErE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACnD,CAAC"}
@@ -8,14 +8,25 @@ export { useSecureMessages } from "./hooks/useSecureMessages.js";
8
8
  export type { UseSecureMessagesOptions, UseSecureMessagesValues, DecryptedSecureMessage, } from "./hooks/useSecureMessages.js";
9
9
  export { useSecureHandshakes } from "./hooks/useSecureHandshakes.js";
10
10
  export type { UseSecureHandshakesOptions, UseSecureHandshakesValues, } from "./hooks/useSecureHandshakes.js";
11
+ export { useSecureBackup } from "./hooks/useSecureBackup.js";
12
+ export type { UseSecureBackupValues } from "./hooks/useSecureBackup.js";
13
+ export { useSecureSafetyNumber } from "./hooks/useSecureSafetyNumber.js";
14
+ export type { UseSecureSafetyNumberValues } from "./hooks/useSecureSafetyNumber.js";
15
+ export { estimatePassphraseStrength } from "./backup/passphrase-strength.js";
16
+ export type { PassphraseStrength } from "./backup/passphrase-strength.js";
11
17
  export { SecureChatRestClient } from "./transport/rest.js";
12
18
  export type { SecureChatRestConfig } from "./transport/rest.js";
13
19
  export { SecureChatSocketClient } from "./transport/socket.js";
14
20
  export type { SecureSocket, SecureServerEvents, SecureClientEvents, SecureChatSocketConfig, } from "./transport/socket.js";
15
- export type { SecureChatCrypto, DeviceIdentity, KeyPackageBundle, GroupHandle, TargetedWelcome, CommitResult, PassphraseBackup, } from "@agora-sdk/secure-chat-crypto";
21
+ export type { SecureChatCrypto, DeviceIdentity, KeyPackageBundle, GroupHandle, GroupMemberIdentity, TargetedWelcome, CommitResult, PassphraseBackup, SecureDecryptFailureReason, } from "@agora-sdk/secure-chat-crypto";
22
+ export { SecureChatDecryptError } from "@agora-sdk/secure-chat-crypto";
16
23
  export type * from "./contract/index.js";
17
24
  export type { SecureChatStore } from "./persistence/store.js";
18
25
  export { MemoryStore } from "./persistence/memory-store.js";
19
26
  export { SecureChatRepository } from "./persistence/repository.js";
20
27
  export type { PersistedDevice } from "./persistence/repository.js";
21
28
  export { toBase64, fromBase64, utf8ToBytes, bytesToUtf8 } from "./util/base64.js";
29
+ export { padPlaintext, unpadPlaintext, nextBucket } from "./util/padding.js";
30
+ export type { PaddingPolicy } from "./util/padding.js";
31
+ export { computeSafetyNumber } from "./util/safety-number.js";
32
+ export type { SafetyNumber } from "./util/safety-number.js";
package/dist/cjs/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // injection of a `SecureChatCrypto`. Platform packages (@agora-sdk/secure-chat-react-js, etc.)
6
6
  // re-export this and add the concrete crypto + persistence.
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.bytesToUtf8 = exports.utf8ToBytes = exports.fromBase64 = exports.toBase64 = exports.SecureChatRepository = exports.MemoryStore = exports.SecureChatSocketClient = exports.SecureChatRestClient = exports.useSecureHandshakes = exports.useSecureMessages = exports.useSecureConversations = exports.useSecureDevice = exports.useSecureChat = exports.SecureChatProvider = void 0;
8
+ exports.computeSafetyNumber = exports.nextBucket = exports.unpadPlaintext = exports.padPlaintext = exports.bytesToUtf8 = exports.utf8ToBytes = exports.fromBase64 = exports.toBase64 = exports.SecureChatRepository = exports.MemoryStore = exports.SecureChatDecryptError = exports.SecureChatSocketClient = exports.SecureChatRestClient = exports.estimatePassphraseStrength = exports.useSecureSafetyNumber = exports.useSecureBackup = exports.useSecureHandshakes = exports.useSecureMessages = exports.useSecureConversations = exports.useSecureDevice = exports.useSecureChat = exports.SecureChatProvider = void 0;
9
9
  // ── context / provider ──────────────────────────────────────────────────────
10
10
  var secure_chat_context_js_1 = require("./context/secure-chat-context.js");
11
11
  Object.defineProperty(exports, "SecureChatProvider", { enumerable: true, get: function () { return secure_chat_context_js_1.SecureChatProvider; } });
@@ -19,11 +19,20 @@ var useSecureMessages_js_1 = require("./hooks/useSecureMessages.js");
19
19
  Object.defineProperty(exports, "useSecureMessages", { enumerable: true, get: function () { return useSecureMessages_js_1.useSecureMessages; } });
20
20
  var useSecureHandshakes_js_1 = require("./hooks/useSecureHandshakes.js");
21
21
  Object.defineProperty(exports, "useSecureHandshakes", { enumerable: true, get: function () { return useSecureHandshakes_js_1.useSecureHandshakes; } });
22
+ var useSecureBackup_js_1 = require("./hooks/useSecureBackup.js");
23
+ Object.defineProperty(exports, "useSecureBackup", { enumerable: true, get: function () { return useSecureBackup_js_1.useSecureBackup; } });
24
+ var useSecureSafetyNumber_js_1 = require("./hooks/useSecureSafetyNumber.js");
25
+ Object.defineProperty(exports, "useSecureSafetyNumber", { enumerable: true, get: function () { return useSecureSafetyNumber_js_1.useSecureSafetyNumber; } });
26
+ // ── backup (passphrase strength meter) ────────────────────────────────────────
27
+ var passphrase_strength_js_1 = require("./backup/passphrase-strength.js");
28
+ Object.defineProperty(exports, "estimatePassphraseStrength", { enumerable: true, get: function () { return passphrase_strength_js_1.estimatePassphraseStrength; } });
22
29
  // ── transport (for advanced / non-React use) ─────────────────────────────────
23
30
  var rest_js_1 = require("./transport/rest.js");
24
31
  Object.defineProperty(exports, "SecureChatRestClient", { enumerable: true, get: function () { return rest_js_1.SecureChatRestClient; } });
25
32
  var socket_js_1 = require("./transport/socket.js");
26
33
  Object.defineProperty(exports, "SecureChatSocketClient", { enumerable: true, get: function () { return socket_js_1.SecureChatSocketClient; } });
34
+ var secure_chat_crypto_1 = require("@agora-sdk/secure-chat-crypto");
35
+ Object.defineProperty(exports, "SecureChatDecryptError", { enumerable: true, get: function () { return secure_chat_crypto_1.SecureChatDecryptError; } });
27
36
  var memory_store_js_1 = require("./persistence/memory-store.js");
28
37
  Object.defineProperty(exports, "MemoryStore", { enumerable: true, get: function () { return memory_store_js_1.MemoryStore; } });
29
38
  var repository_js_1 = require("./persistence/repository.js");
@@ -34,4 +43,10 @@ Object.defineProperty(exports, "toBase64", { enumerable: true, get: function ()
34
43
  Object.defineProperty(exports, "fromBase64", { enumerable: true, get: function () { return base64_js_1.fromBase64; } });
35
44
  Object.defineProperty(exports, "utf8ToBytes", { enumerable: true, get: function () { return base64_js_1.utf8ToBytes; } });
36
45
  Object.defineProperty(exports, "bytesToUtf8", { enumerable: true, get: function () { return base64_js_1.bytesToUtf8; } });
46
+ var padding_js_1 = require("./util/padding.js");
47
+ Object.defineProperty(exports, "padPlaintext", { enumerable: true, get: function () { return padding_js_1.padPlaintext; } });
48
+ Object.defineProperty(exports, "unpadPlaintext", { enumerable: true, get: function () { return padding_js_1.unpadPlaintext; } });
49
+ Object.defineProperty(exports, "nextBucket", { enumerable: true, get: function () { return padding_js_1.nextBucket; } });
50
+ var safety_number_js_1 = require("./util/safety-number.js");
51
+ Object.defineProperty(exports, "computeSafetyNumber", { enumerable: true, get: function () { return safety_number_js_1.computeSafetyNumber; } });
37
52
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,8FAA8F;AAC9F,EAAE;AACF,6FAA6F;AAC7F,+FAA+F;AAC/F,4DAA4D;;;AAE5D,+EAA+E;AAC/E,2EAAqF;AAA5E,4HAAA,kBAAkB,OAAA;AAAE,uHAAA,aAAa,OAAA;AAM1C,gFAAgF;AAChF,iEAA6D;AAApD,qHAAA,eAAe,OAAA;AAExB,+EAA2E;AAAlE,mIAAA,sBAAsB,OAAA;AAE/B,qEAAiE;AAAxD,yHAAA,iBAAiB,OAAA;AAM1B,yEAAqE;AAA5D,6HAAA,mBAAmB,OAAA;AAM5B,gFAAgF;AAChF,+CAA2D;AAAlD,+GAAA,oBAAoB,OAAA;AAE7B,mDAA+D;AAAtD,mHAAA,sBAAsB,OAAA;AAwB/B,iEAA4D;AAAnD,8GAAA,WAAW,OAAA;AACpB,6DAAmE;AAA1D,qHAAA,oBAAoB,OAAA;AAG7B,gFAAgF;AAChF,8CAAkF;AAAzE,qGAAA,QAAQ,OAAA;AAAE,uGAAA,UAAU,OAAA;AAAE,wGAAA,WAAW,OAAA;AAAE,wGAAA,WAAW,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,8FAA8F;AAC9F,EAAE;AACF,6FAA6F;AAC7F,+FAA+F;AAC/F,4DAA4D;;;AAE5D,+EAA+E;AAC/E,2EAAqF;AAA5E,4HAAA,kBAAkB,OAAA;AAAE,uHAAA,aAAa,OAAA;AAM1C,gFAAgF;AAChF,iEAA6D;AAApD,qHAAA,eAAe,OAAA;AAExB,+EAA2E;AAAlE,mIAAA,sBAAsB,OAAA;AAE/B,qEAAiE;AAAxD,yHAAA,iBAAiB,OAAA;AAM1B,yEAAqE;AAA5D,6HAAA,mBAAmB,OAAA;AAK5B,iEAA6D;AAApD,qHAAA,eAAe,OAAA;AAExB,6EAAyE;AAAhE,iIAAA,qBAAqB,OAAA;AAG9B,iFAAiF;AACjF,0EAA6E;AAApE,oIAAA,0BAA0B,OAAA;AAGnC,gFAAgF;AAChF,+CAA2D;AAAlD,+GAAA,oBAAoB,OAAA;AAE7B,mDAA+D;AAAtD,mHAAA,sBAAsB,OAAA;AAoB/B,oEAAuE;AAA9D,4HAAA,sBAAsB,OAAA;AAO/B,iEAA4D;AAAnD,8GAAA,WAAW,OAAA;AACpB,6DAAmE;AAA1D,qHAAA,oBAAoB,OAAA;AAG7B,gFAAgF;AAChF,8CAAkF;AAAzE,qGAAA,QAAQ,OAAA;AAAE,uGAAA,UAAU,OAAA;AAAE,wGAAA,WAAW,OAAA;AAAE,wGAAA,WAAW,OAAA;AACvD,gDAA6E;AAApE,0GAAA,YAAY,OAAA;AAAE,4GAAA,cAAc,OAAA;AAAE,wGAAA,UAAU,OAAA;AAEjD,4DAA8D;AAArD,uHAAA,mBAAmB,OAAA"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * How aggressively to pad outbound message plaintext.
3
+ *
4
+ * - `"ladder"` — round the framed length up to the next size bucket (the default; blunts length
5
+ * fingerprinting).
6
+ * - `"none"` — still frame the message (so unpadding stays uniform across clients) but add no extra
7
+ * bytes. Use only when bandwidth matters more than traffic-shape privacy.
8
+ */
9
+ export type PaddingPolicy = "ladder" | "none";
10
+ /**
11
+ * Round a framed byte length up to the next size bucket: the smallest ladder rung
12
+ * (32, 64, … , 8192) that is ≥ `n`, or — above the ladder — the next multiple of 8192.
13
+ *
14
+ * @param n - The unpadded framed length (header + content) in bytes.
15
+ * @returns The bucket size to pad up to (always ≥ 32, always ≥ `n`).
16
+ */
17
+ export declare function nextBucket(n: number): number;
18
+ /**
19
+ * Wrap message content in a size-bucket padding frame for encryption. The output is
20
+ * `[version][contentLen][content][zero pad]`; under `"ladder"` its length is one of the fixed buckets
21
+ * from {@link nextBucket}. Padding bytes are zeros — they ride inside the MLS ciphertext, so they need
22
+ * not be random.
23
+ *
24
+ * @param content - The plaintext bytes to send (e.g. `utf8ToBytes(text)`).
25
+ * @param policy - The {@link PaddingPolicy}; defaults to `"ladder"`.
26
+ * @returns The framed, padded bytes to hand to `crypto.encryptMessage`.
27
+ */
28
+ export declare function padPlaintext(content: Uint8Array, policy?: PaddingPolicy): Uint8Array;
29
+ /**
30
+ * Recover the original content from a {@link padPlaintext} frame after decryption. Validates the version
31
+ * byte and bounds-checks the declared length, then slices off the header and trailing zero padding.
32
+ *
33
+ * @param frame - The decrypted frame bytes (from `crypto.decryptMessage`).
34
+ * @returns The original content bytes.
35
+ * @throws {Error} On a frame that is too short, carries an unknown version, or declares a length that
36
+ * overruns the buffer — a successfully-authenticated MLS message with a bad frame is a real
37
+ * framing/version mismatch, so the caller fails closed rather than rendering raw padded bytes.
38
+ */
39
+ export declare function unpadPlaintext(frame: Uint8Array): Uint8Array;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ // Message size-bucket padding — a metadata-hardening frame applied to plaintext BEFORE MLS encryption.
3
+ //
4
+ // Where it sits in the blind-server model: the server (and any DB/network observer) is trusted to relay
5
+ // ciphertext but learns *envelope* metadata in the Signal model — including ciphertext SIZE. Raw chat
6
+ // text encrypts to a ciphertext whose length tracks the message length, leaking traffic shape. We wrap
7
+ // the plaintext in a self-describing frame and zero-pad it up to a fixed bucket ladder, so short
8
+ // messages collapse into a few sizes. MLS then encrypts the frame, so the ciphertext inherits the
9
+ // bucketing (modulo a constant MLS framing overhead). This is pure byte-shuffling — NO crypto, no keys
10
+ // — and the frame is symmetric: every client pads on send and unpads on receive identically.
11
+ //
12
+ // Frame layout: [version:1B = 1][contentLen:uint32 BE][content bytes][zero padding → bucket].
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.nextBucket = nextBucket;
15
+ exports.padPlaintext = padPlaintext;
16
+ exports.unpadPlaintext = unpadPlaintext;
17
+ /** The smallest bucket; also the floor for an empty message (a 5-byte header → bucket 32). */
18
+ const LADDER = [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192];
19
+ /** Above the ladder we round up to multiples of this (the largest ladder rung). */
20
+ const STEP = 8192;
21
+ /** Frame header: 1 version byte + a 4-byte big-endian content length. */
22
+ const HEADER = 5;
23
+ /** Frame format version, bound as the first byte so the codec can evolve without ambiguity. */
24
+ const VERSION = 1;
25
+ /**
26
+ * Round a framed byte length up to the next size bucket: the smallest ladder rung
27
+ * (32, 64, … , 8192) that is ≥ `n`, or — above the ladder — the next multiple of 8192.
28
+ *
29
+ * @param n - The unpadded framed length (header + content) in bytes.
30
+ * @returns The bucket size to pad up to (always ≥ 32, always ≥ `n`).
31
+ */
32
+ function nextBucket(n) {
33
+ for (const b of LADDER)
34
+ if (n <= b)
35
+ return b;
36
+ return Math.ceil(n / STEP) * STEP;
37
+ }
38
+ /**
39
+ * Wrap message content in a size-bucket padding frame for encryption. The output is
40
+ * `[version][contentLen][content][zero pad]`; under `"ladder"` its length is one of the fixed buckets
41
+ * from {@link nextBucket}. Padding bytes are zeros — they ride inside the MLS ciphertext, so they need
42
+ * not be random.
43
+ *
44
+ * @param content - The plaintext bytes to send (e.g. `utf8ToBytes(text)`).
45
+ * @param policy - The {@link PaddingPolicy}; defaults to `"ladder"`.
46
+ * @returns The framed, padded bytes to hand to `crypto.encryptMessage`.
47
+ */
48
+ function padPlaintext(content, policy = "ladder") {
49
+ const framed = HEADER + content.length;
50
+ const target = policy === "ladder" ? nextBucket(framed) : framed;
51
+ const out = new Uint8Array(target); // zero-filled → the padding is already in place
52
+ out[0] = VERSION;
53
+ out[1] = (content.length >>> 24) & 0xff;
54
+ out[2] = (content.length >>> 16) & 0xff;
55
+ out[3] = (content.length >>> 8) & 0xff;
56
+ out[4] = content.length & 0xff;
57
+ out.set(content, HEADER);
58
+ return out;
59
+ }
60
+ /**
61
+ * Recover the original content from a {@link padPlaintext} frame after decryption. Validates the version
62
+ * byte and bounds-checks the declared length, then slices off the header and trailing zero padding.
63
+ *
64
+ * @param frame - The decrypted frame bytes (from `crypto.decryptMessage`).
65
+ * @returns The original content bytes.
66
+ * @throws {Error} On a frame that is too short, carries an unknown version, or declares a length that
67
+ * overruns the buffer — a successfully-authenticated MLS message with a bad frame is a real
68
+ * framing/version mismatch, so the caller fails closed rather than rendering raw padded bytes.
69
+ */
70
+ function unpadPlaintext(frame) {
71
+ if (frame.length < HEADER)
72
+ throw new Error("secure-chat: padding frame too short");
73
+ if (frame[0] !== VERSION)
74
+ throw new Error(`secure-chat: unsupported padding frame version ${frame[0]}`);
75
+ const len = (frame[1] << 24) | (frame[2] << 16) | (frame[3] << 8) | frame[4];
76
+ if (len < 0 || HEADER + len > frame.length)
77
+ throw new Error("secure-chat: padding frame length out of range");
78
+ return frame.slice(HEADER, HEADER + len);
79
+ }
80
+ //# sourceMappingURL=padding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"padding.js","sourceRoot":"","sources":["../../../src/util/padding.ts"],"names":[],"mappings":";AAAA,uGAAuG;AACvG,EAAE;AACF,wGAAwG;AACxG,sGAAsG;AACtG,uGAAuG;AACvG,iGAAiG;AACjG,kGAAkG;AAClG,uGAAuG;AACvG,6FAA6F;AAC7F,EAAE;AACF,8FAA8F;;AA4B9F,gCAGC;AAYD,oCAWC;AAYD,wCAMC;AAtED,8FAA8F;AAC9F,MAAM,MAAM,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAC;AACxE,mFAAmF;AACnF,MAAM,IAAI,GAAG,IAAI,CAAC;AAClB,yEAAyE;AACzE,MAAM,MAAM,GAAG,CAAC,CAAC;AACjB,+FAA+F;AAC/F,MAAM,OAAO,GAAG,CAAC,CAAC;AAYlB;;;;;;GAMG;AACH,SAAgB,UAAU,CAAC,CAAS;IAClC,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;AACpC,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAAC,OAAmB,EAAE,SAAwB,QAAQ;IAChF,MAAM,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACvC,MAAM,MAAM,GAAG,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACjE,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,gDAAgD;IACpF,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IACjB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACxC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACxC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC;IACvC,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAC/B,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACzB,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,cAAc,CAAC,KAAiB;IAC9C,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACnF,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACxG,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7E,IAAI,GAAG,GAAG,CAAC,IAAI,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IAC9G,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { GroupMemberIdentity } from "@agora-sdk/secure-chat-crypto";
2
+ /** A derived safety number, in the forms a UI needs. */
3
+ export interface SafetyNumber {
4
+ /** All 60 decimal digits with no separators. */
5
+ digits: string;
6
+ /** The 60 digits as 12 groups of 5 (for display / read-aloud). */
7
+ groups: string[];
8
+ /** The raw combined fingerprint bytes (sorted-concatenated per-party hashes) — e.g. for a QR code. */
9
+ fingerprint: Uint8Array;
10
+ }
11
+ /**
12
+ * Derive the safety number for two parties from their public identities. The result is **symmetric** —
13
+ * `computeSafetyNumber(a, b)` equals `computeSafetyNumber(b, a)` — because the two per-party
14
+ * fingerprints are sorted before concatenation. Uses WebCrypto SHA-256 (available in browsers and
15
+ * Node 18+).
16
+ *
17
+ * @param a - One party's `{ deviceId, signaturePublicKey }` (e.g. the local device).
18
+ * @param b - The other party's identity (e.g. the remote peer).
19
+ * @returns The {@link SafetyNumber} (60 digits, 12 groups of 5, plus raw fingerprint bytes).
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const { groups } = await computeSafetyNumber(localIdentity, peerIdentity);
24
+ * // groups → ["12345", "67890", … 12 of them]; compare with the peer out-of-band.
25
+ * ```
26
+ */
27
+ export declare function computeSafetyNumber(a: GroupMemberIdentity, b: GroupMemberIdentity): Promise<SafetyNumber>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ // Safety number — a human-comparable fingerprint of two devices' identity keys (key verification).
3
+ //
4
+ // Where it sits in the blind-server model: the server is blind but UNTRUSTED, so it could in principle
5
+ // hand a victim a KeyPackage carrying an attacker's signature key (a classic active MITM on a TOFU
6
+ // system). A safety number lets two users confirm out-of-band (read aloud / compare on screen / scan a
7
+ // QR) that they actually hold each other's real identity keys. It is derived ONLY from public
8
+ // signature keys + device ids — no secrets — and is symmetric: both sides compute the same number.
9
+ //
10
+ // Shape (Signal-style): each party's identity is hashed into a 30-digit number; the two are sorted (so
11
+ // the result is order-independent) and concatenated into 60 decimal digits, shown as 12 groups of 5.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.computeSafetyNumber = computeSafetyNumber;
14
+ /** Format/version byte bound into the hash so the derivation can evolve unambiguously. */
15
+ const VERSION = 0;
16
+ /** Hash iterations per party. Public-key fingerprints don't need KDF-grade work; this is a fixed,
17
+ * app-wide constant purely so every Agora client derives an identical number (interop, not secrecy). */
18
+ const ITERATIONS = 1024;
19
+ /** Bytes of the final hash consumed: 6 chunks × 5 bytes → 6 × 5 digits = 30 digits per party. */
20
+ const CHUNKS = 6;
21
+ const CHUNK_BYTES = 5;
22
+ const utf8 = (s) => new TextEncoder().encode(s);
23
+ function concat(...parts) {
24
+ const total = parts.reduce((n, p) => n + p.length, 0);
25
+ const out = new Uint8Array(total);
26
+ let off = 0;
27
+ for (const p of parts) {
28
+ out.set(p, off);
29
+ off += p.length;
30
+ }
31
+ return out;
32
+ }
33
+ async function sha256(bytes) {
34
+ // Cast to BufferSource: our Uint8Array is always ArrayBuffer-backed, but the lib type is the wider
35
+ // Uint8Array<ArrayBufferLike> (which could be SharedArrayBuffer) that digest() won't accept directly.
36
+ return new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
37
+ }
38
+ /** Encode one 5-byte chunk as a 5-digit decimal string (40-bit big-endian value mod 100000). */
39
+ function chunkToDigits(hash, offset) {
40
+ // 2**40 < 2**53, so plain Number arithmetic is exact here.
41
+ let value = 0;
42
+ for (let i = 0; i < CHUNK_BYTES; i++)
43
+ value = value * 256 + hash[offset + i];
44
+ return (value % 100000).toString().padStart(5, "0");
45
+ }
46
+ /** Per-party fingerprint: an iterated hash of (version || pubkey || deviceId), then 30 digits + the
47
+ * truncated hash bytes. Signal-shaped (the key is re-mixed in each round). */
48
+ async function partyFingerprint(party) {
49
+ const key = party.signaturePublicKey;
50
+ let hash = concat(Uint8Array.of(VERSION), key, utf8(party.deviceId));
51
+ for (let i = 0; i < ITERATIONS; i++)
52
+ hash = await sha256(concat(hash, key));
53
+ let digits = "";
54
+ for (let c = 0; c < CHUNKS; c++)
55
+ digits += chunkToDigits(hash, c * CHUNK_BYTES);
56
+ return { digits, bytes: hash.slice(0, CHUNKS * CHUNK_BYTES) };
57
+ }
58
+ /**
59
+ * Derive the safety number for two parties from their public identities. The result is **symmetric** —
60
+ * `computeSafetyNumber(a, b)` equals `computeSafetyNumber(b, a)` — because the two per-party
61
+ * fingerprints are sorted before concatenation. Uses WebCrypto SHA-256 (available in browsers and
62
+ * Node 18+).
63
+ *
64
+ * @param a - One party's `{ deviceId, signaturePublicKey }` (e.g. the local device).
65
+ * @param b - The other party's identity (e.g. the remote peer).
66
+ * @returns The {@link SafetyNumber} (60 digits, 12 groups of 5, plus raw fingerprint bytes).
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const { groups } = await computeSafetyNumber(localIdentity, peerIdentity);
71
+ * // groups → ["12345", "67890", … 12 of them]; compare with the peer out-of-band.
72
+ * ```
73
+ */
74
+ async function computeSafetyNumber(a, b) {
75
+ const [fpA, fpB] = await Promise.all([partyFingerprint(a), partyFingerprint(b)]);
76
+ // Sort so the number is the same regardless of who is "a" vs "b".
77
+ const [first, second] = fpA.digits <= fpB.digits ? [fpA, fpB] : [fpB, fpA];
78
+ const digits = first.digits + second.digits;
79
+ const groups = [];
80
+ for (let i = 0; i < digits.length; i += 5)
81
+ groups.push(digits.slice(i, i + 5));
82
+ return { digits, groups, fingerprint: concat(first.bytes, second.bytes) };
83
+ }
84
+ //# sourceMappingURL=safety-number.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safety-number.js","sourceRoot":"","sources":["../../../src/util/safety-number.ts"],"names":[],"mappings":"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"}