@agora-sdk/secure-chat-core 0.3.0 → 0.5.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/cjs/backup/passphrase-strength.d.ts +22 -0
- package/dist/cjs/backup/passphrase-strength.js +75 -0
- package/dist/cjs/backup/passphrase-strength.js.map +1 -0
- package/dist/cjs/context/secure-chat-context.d.ts +12 -1
- package/dist/cjs/context/secure-chat-context.js +3 -1
- package/dist/cjs/context/secure-chat-context.js.map +1 -1
- package/dist/cjs/contract/index.d.ts +2 -150
- package/dist/cjs/contract/index.js +11 -10
- package/dist/cjs/contract/index.js.map +1 -1
- package/dist/cjs/hooks/useSecureBackup.d.ts +67 -0
- package/dist/cjs/hooks/useSecureBackup.js +207 -0
- package/dist/cjs/hooks/useSecureBackup.js.map +1 -0
- package/dist/cjs/hooks/useSecureDevice.d.ts +21 -1
- package/dist/cjs/hooks/useSecureDevice.js +46 -5
- package/dist/cjs/hooks/useSecureDevice.js.map +1 -1
- package/dist/cjs/hooks/useSecureMessages.d.ts +16 -3
- package/dist/cjs/hooks/useSecureMessages.js +38 -15
- package/dist/cjs/hooks/useSecureMessages.js.map +1 -1
- package/dist/cjs/hooks/useSecureSafetyNumber.d.ts +30 -0
- package/dist/cjs/hooks/useSecureSafetyNumber.js +82 -0
- package/dist/cjs/hooks/useSecureSafetyNumber.js.map +1 -0
- package/dist/cjs/index.d.ts +12 -1
- package/dist/cjs/index.js +16 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/transport/socket.d.ts +15 -3
- package/dist/cjs/transport/socket.js +4 -2
- package/dist/cjs/transport/socket.js.map +1 -1
- package/dist/cjs/util/padding.d.ts +39 -0
- package/dist/cjs/util/padding.js +80 -0
- package/dist/cjs/util/padding.js.map +1 -0
- package/dist/cjs/util/safety-number.d.ts +27 -0
- package/dist/cjs/util/safety-number.js +84 -0
- package/dist/cjs/util/safety-number.js.map +1 -0
- package/dist/esm/backup/passphrase-strength.d.ts +22 -0
- package/dist/esm/backup/passphrase-strength.js +72 -0
- package/dist/esm/backup/passphrase-strength.js.map +1 -0
- package/dist/esm/context/secure-chat-context.d.ts +12 -1
- package/dist/esm/context/secure-chat-context.js +3 -1
- package/dist/esm/context/secure-chat-context.js.map +1 -1
- package/dist/esm/contract/index.d.ts +2 -150
- package/dist/esm/contract/index.js +11 -10
- package/dist/esm/contract/index.js.map +1 -1
- package/dist/esm/hooks/useSecureBackup.d.ts +67 -0
- package/dist/esm/hooks/useSecureBackup.js +204 -0
- package/dist/esm/hooks/useSecureBackup.js.map +1 -0
- package/dist/esm/hooks/useSecureDevice.d.ts +21 -1
- package/dist/esm/hooks/useSecureDevice.js +46 -5
- package/dist/esm/hooks/useSecureDevice.js.map +1 -1
- package/dist/esm/hooks/useSecureMessages.d.ts +16 -3
- package/dist/esm/hooks/useSecureMessages.js +38 -15
- package/dist/esm/hooks/useSecureMessages.js.map +1 -1
- package/dist/esm/hooks/useSecureSafetyNumber.d.ts +30 -0
- package/dist/esm/hooks/useSecureSafetyNumber.js +79 -0
- package/dist/esm/hooks/useSecureSafetyNumber.js.map +1 -0
- package/dist/esm/index.d.ts +12 -1
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transport/socket.d.ts +15 -3
- package/dist/esm/transport/socket.js +4 -2
- package/dist/esm/transport/socket.js.map +1 -1
- package/dist/esm/util/padding.d.ts +39 -0
- package/dist/esm/util/padding.js +75 -0
- package/dist/esm/util/padding.js.map +1 -0
- package/dist/esm/util/safety-number.d.ts +27 -0
- package/dist/esm/util/safety-number.js +81 -0
- package/dist/esm/util/safety-number.js.map +1 -0
- package/package.json +3 -2
|
@@ -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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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The outcome of {@link estimatePassphraseStrength}: a coarse 0–4 score plus display copy. */
|
|
2
|
+
export interface PassphraseStrength {
|
|
3
|
+
/** 0 (weakest) … 4 (strongest) — for a 5-segment meter. */
|
|
4
|
+
score: 0 | 1 | 2 | 3 | 4;
|
|
5
|
+
/** A short human label for the score (e.g. "Weak", "Strong"). */
|
|
6
|
+
label: string;
|
|
7
|
+
/** A specific reason the passphrase is weak, when applicable (e.g. a common password). */
|
|
8
|
+
warning?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Estimate the strength of a backup passphrase for a strength meter.
|
|
12
|
+
*
|
|
13
|
+
* @param passphrase - The candidate passphrase (never logged or sent anywhere).
|
|
14
|
+
* @returns A {@link PassphraseStrength} with a 0–4 score, a label, and an optional warning.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const { score, label, warning } = estimatePassphraseStrength(input);
|
|
19
|
+
* // render a 5-segment meter from `score`, show `warning` if present.
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function estimatePassphraseStrength(passphrase: string): PassphraseStrength;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Passphrase-strength estimator for backup UX.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists (CLAUDE.md #1 + spec §16.5): the blind server stores the passphrase-encrypted
|
|
4
|
+
// backup blob, so a weak passphrase is offline-brute-forceable on a DB exfil. The real argon2id KDF
|
|
5
|
+
// raises the cost per guess, but it can't rescue a guessable passphrase — so we nudge the user toward
|
|
6
|
+
// a strong one at entry time. This is a deliberately small, dependency-free heuristic (length +
|
|
7
|
+
// character-class diversity + a common-password penalty), NOT a substitute for zxcvbn-grade entropy
|
|
8
|
+
// estimation. It runs purely client-side and never logs or transmits the passphrase.
|
|
9
|
+
const LABELS = ["Very weak", "Weak", "Fair", "Strong", "Very strong"];
|
|
10
|
+
// A tiny set of the most-guessed passwords/substrings. Not exhaustive — a guardrail against the
|
|
11
|
+
// obvious, not a real dictionary check.
|
|
12
|
+
const COMMON = [
|
|
13
|
+
"password", "passw0rd", "12345", "123456", "qwerty", "letmein", "admin", "welcome",
|
|
14
|
+
"iloveyou", "abc123", "hunter2", "monkey", "dragon", "trustno1", "secret",
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Estimate the strength of a backup passphrase for a strength meter.
|
|
18
|
+
*
|
|
19
|
+
* @param passphrase - The candidate passphrase (never logged or sent anywhere).
|
|
20
|
+
* @returns A {@link PassphraseStrength} with a 0–4 score, a label, and an optional warning.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const { score, label, warning } = estimatePassphraseStrength(input);
|
|
25
|
+
* // render a 5-segment meter from `score`, show `warning` if present.
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function estimatePassphraseStrength(passphrase) {
|
|
29
|
+
const pw = passphrase ?? "";
|
|
30
|
+
if (pw.length === 0)
|
|
31
|
+
return { score: 0, label: LABELS[0] };
|
|
32
|
+
const lower = pw.toLowerCase();
|
|
33
|
+
const hitsCommon = COMMON.some((c) => lower.includes(c));
|
|
34
|
+
// Character-class diversity.
|
|
35
|
+
let classes = 0;
|
|
36
|
+
if (/[a-z]/.test(pw))
|
|
37
|
+
classes++;
|
|
38
|
+
if (/[A-Z]/.test(pw))
|
|
39
|
+
classes++;
|
|
40
|
+
if (/[0-9]/.test(pw))
|
|
41
|
+
classes++;
|
|
42
|
+
if (/[^a-zA-Z0-9]/.test(pw))
|
|
43
|
+
classes++;
|
|
44
|
+
// Length is the dominant factor (passphrases >> complex-but-short passwords).
|
|
45
|
+
let score = 0;
|
|
46
|
+
if (pw.length >= 8)
|
|
47
|
+
score++;
|
|
48
|
+
if (pw.length >= 12)
|
|
49
|
+
score++;
|
|
50
|
+
if (pw.length >= 16)
|
|
51
|
+
score++;
|
|
52
|
+
// Diversity adds at most one step, and only once there's some length.
|
|
53
|
+
if (classes >= 3 && pw.length >= 8)
|
|
54
|
+
score++;
|
|
55
|
+
// A short, single-class passphrase never rates above "weak".
|
|
56
|
+
if (pw.length < 8 || classes <= 1)
|
|
57
|
+
score = Math.min(score, 1);
|
|
58
|
+
// A recognizably common password is capped hard regardless of shape.
|
|
59
|
+
if (hitsCommon)
|
|
60
|
+
score = Math.min(score, 1);
|
|
61
|
+
const clamped = Math.max(0, Math.min(4, score));
|
|
62
|
+
return {
|
|
63
|
+
score: clamped,
|
|
64
|
+
label: LABELS[clamped],
|
|
65
|
+
warning: hitsCommon
|
|
66
|
+
? "This looks like a common password — choose something less guessable."
|
|
67
|
+
: clamped <= 1
|
|
68
|
+
? "Use a longer passphrase (4+ random words) with mixed character types."
|
|
69
|
+
: undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=passphrase-strength.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passphrase-strength.js","sourceRoot":"","sources":["../../../src/backup/passphrase-strength.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,gGAAgG;AAChG,oGAAoG;AACpG,sGAAsG;AACtG,gGAAgG;AAChG,oGAAoG;AACpG,qFAAqF;AAYrF,MAAM,MAAM,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,CAAU,CAAC;AAE/E,gGAAgG;AAChG,wCAAwC;AACxC,MAAM,MAAM,GAAG;IACb,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS;IAClF,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ;CAC1E,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,0BAA0B,CAAC,UAAkB;IAC3D,MAAM,EAAE,GAAG,UAAU,IAAI,EAAE,CAAC;IAC5B,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAE3D,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAEzD,6BAA6B;IAC7B,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,8EAA8E;IAC9E,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC;QAAE,KAAK,EAAE,CAAC;IAC5B,IAAI,EAAE,CAAC,MAAM,IAAI,EAAE;QAAE,KAAK,EAAE,CAAC;IAC7B,IAAI,EAAE,CAAC,MAAM,IAAI,EAAE;QAAE,KAAK,EAAE,CAAC;IAC7B,sEAAsE;IACtE,IAAI,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC;QAAE,KAAK,EAAE,CAAC;IAE5C,6DAA6D;IAC7D,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,IAAI,CAAC;QAAE,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAE9D,qEAAqE;IACrE,IAAI,UAAU;QAAE,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAgC,CAAC;IAC/E,OAAO;QACL,KAAK,EAAE,OAAO;QACd,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC;QACtB,OAAO,EAAE,UAAU;YACjB,CAAC,CAAC,sEAAsE;YACxE,CAAC,CAAC,OAAO,IAAI,CAAC;gBACZ,CAAC,CAAC,uEAAuE;gBACzE,CAAC,CAAC,SAAS;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { SecureChatRestClient } from "../transport/rest.js";
|
|
|
4
4
|
import { SecureChatSocketClient } from "../transport/socket.js";
|
|
5
5
|
import { SecureChatStore } from "../persistence/store.js";
|
|
6
6
|
import { SecureChatRepository } from "../persistence/repository.js";
|
|
7
|
+
import { PaddingPolicy } from "../util/padding.js";
|
|
7
8
|
/**
|
|
8
9
|
* The value exposed by {@link useSecureChat}: shared transport clients, the injected crypto, the
|
|
9
10
|
* persistence repository, the group-handle resolver, and the active project id.
|
|
@@ -31,6 +32,11 @@ export interface SecureChatContextValue {
|
|
|
31
32
|
* @returns An unsubscribe function.
|
|
32
33
|
*/
|
|
33
34
|
subscribeGroupChange: (listener: () => void) => () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Outbound message size-bucket padding policy. `useSecureMessages` pads plaintext to this before
|
|
37
|
+
* encryption so ciphertext size leaks less; defaults to `"ladder"`.
|
|
38
|
+
*/
|
|
39
|
+
padding: PaddingPolicy;
|
|
34
40
|
/** The Agora project id these clients are scoped to. */
|
|
35
41
|
projectId: string;
|
|
36
42
|
}
|
|
@@ -50,6 +56,11 @@ export interface SecureChatProviderProps {
|
|
|
50
56
|
baseUrl?: string;
|
|
51
57
|
/** Override the socket origin. Defaults to @agora-sdk/core `getSocketUrl()`. */
|
|
52
58
|
socketUrl?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Outbound message size-bucket padding policy (metadata hardening). `"ladder"` (default) pads each
|
|
61
|
+
* message up to a fixed size bucket so ciphertext length leaks less; `"none"` frames without padding.
|
|
62
|
+
*/
|
|
63
|
+
padding?: PaddingPolicy;
|
|
53
64
|
children: React.ReactNode;
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
@@ -66,7 +77,7 @@ export interface SecureChatProviderProps {
|
|
|
66
77
|
* </SecureChatProvider>
|
|
67
78
|
* ```
|
|
68
79
|
*/
|
|
69
|
-
export declare function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessToken, baseUrl, socketUrl, children, }: SecureChatProviderProps): React.JSX.Element;
|
|
80
|
+
export declare function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessToken, baseUrl, socketUrl, padding, children, }: SecureChatProviderProps): React.JSX.Element;
|
|
70
81
|
/**
|
|
71
82
|
* Access the nearest {@link SecureChatContextValue}.
|
|
72
83
|
*
|
|
@@ -27,7 +27,7 @@ const SecureChatContext = createContext(null);
|
|
|
27
27
|
* </SecureChatProvider>
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
export function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessToken, baseUrl, socketUrl, children, }) {
|
|
30
|
+
export function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessToken, baseUrl, socketUrl, padding = "ladder", children, }) {
|
|
31
31
|
const tokenRef = useRef(accessToken);
|
|
32
32
|
tokenRef.current = accessToken;
|
|
33
33
|
const resolveToken = useMemo(() => getAccessToken ?? (() => tokenRef.current), [getAccessToken]);
|
|
@@ -91,6 +91,7 @@ export function SecureChatProvider({ crypto, projectId, store, accessToken, getA
|
|
|
91
91
|
rememberGroup,
|
|
92
92
|
getGroupVersion,
|
|
93
93
|
subscribeGroupChange,
|
|
94
|
+
padding,
|
|
94
95
|
projectId,
|
|
95
96
|
}), [
|
|
96
97
|
rest,
|
|
@@ -101,6 +102,7 @@ export function SecureChatProvider({ crypto, projectId, store, accessToken, getA
|
|
|
101
102
|
rememberGroup,
|
|
102
103
|
getGroupVersion,
|
|
103
104
|
subscribeGroupChange,
|
|
105
|
+
padding,
|
|
104
106
|
projectId,
|
|
105
107
|
]);
|
|
106
108
|
return _jsx(SecureChatContext.Provider, { value: value, children: children });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-chat-context.js","sourceRoot":"","sources":["../../../src/context/secure-chat-context.tsx"],"names":[],"mappings":";AAAA,yFAAyF;AACzF,EAAE;AACF,gGAAgG;AAChG,kGAAkG;AAClG,+FAA+F;AAC/F,mGAAmG;AACnG,gEAAgE;AAEhE,OAAc,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAClG,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"secure-chat-context.js","sourceRoot":"","sources":["../../../src/context/secure-chat-context.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,150 +1,2 @@
|
|
|
1
|
-
export type
|
|
2
|
-
export type
|
|
3
|
-
export type SecureHandshakeKind = "welcome" | "commit" | "proposal";
|
|
4
|
-
/** A Welcome targeted at the ONE device whose claimed KeyPackage was consumed. */
|
|
5
|
-
export interface WelcomeEnvelope {
|
|
6
|
-
targetDeviceId: string;
|
|
7
|
-
payload: string;
|
|
8
|
-
epoch: string;
|
|
9
|
-
}
|
|
10
|
-
/** A Commit/Proposal broadcast to the whole group (no target device). */
|
|
11
|
-
export interface HandshakeBlob {
|
|
12
|
-
payload: string;
|
|
13
|
-
epoch: string;
|
|
14
|
-
}
|
|
15
|
-
export interface RegisterDeviceBody {
|
|
16
|
-
deviceId: string;
|
|
17
|
-
displayName?: string | null;
|
|
18
|
-
signaturePublicKey: string;
|
|
19
|
-
credential: string;
|
|
20
|
-
ciphersuite: number;
|
|
21
|
-
}
|
|
22
|
-
export interface PublishKeyPackagesBody {
|
|
23
|
-
keyPackages: {
|
|
24
|
-
keyPackageRef: string;
|
|
25
|
-
keyPackage: string;
|
|
26
|
-
ciphersuite: number;
|
|
27
|
-
expiresAt?: string | null;
|
|
28
|
-
}[];
|
|
29
|
-
}
|
|
30
|
-
export interface CreateSecureConversationBody {
|
|
31
|
-
type: SecureConversationType;
|
|
32
|
-
mlsGroupId: string;
|
|
33
|
-
spaceId?: string | null;
|
|
34
|
-
name?: string | null;
|
|
35
|
-
memberUserIds?: string[];
|
|
36
|
-
welcomes?: WelcomeEnvelope[];
|
|
37
|
-
}
|
|
38
|
-
export interface AddSecureMemberBody {
|
|
39
|
-
userId: string;
|
|
40
|
-
commit: HandshakeBlob;
|
|
41
|
-
welcomes: WelcomeEnvelope[];
|
|
42
|
-
}
|
|
43
|
-
export interface RemoveSecureMemberBody {
|
|
44
|
-
commit: HandshakeBlob;
|
|
45
|
-
}
|
|
46
|
-
export interface SendSecureMessageBody {
|
|
47
|
-
ciphertext: string;
|
|
48
|
-
epoch: string;
|
|
49
|
-
senderDeviceId: string;
|
|
50
|
-
contentType?: string | null;
|
|
51
|
-
}
|
|
52
|
-
export interface UploadKeyBackupBody {
|
|
53
|
-
deviceId?: string | null;
|
|
54
|
-
blob: string;
|
|
55
|
-
nonce: string;
|
|
56
|
-
kdf: "argon2id" | "pbkdf2";
|
|
57
|
-
kdfParams: Record<string, unknown>;
|
|
58
|
-
cipher: "xchacha20poly1305" | "aes-256-gcm";
|
|
59
|
-
version: number;
|
|
60
|
-
}
|
|
61
|
-
export interface SecureDeviceModel {
|
|
62
|
-
id: string;
|
|
63
|
-
projectId: string;
|
|
64
|
-
userId: string;
|
|
65
|
-
deviceId: string;
|
|
66
|
-
displayName: string | null;
|
|
67
|
-
signaturePublicKey: string;
|
|
68
|
-
credential: string;
|
|
69
|
-
ciphersuite: number;
|
|
70
|
-
revokedAt: string | null;
|
|
71
|
-
lastSeenAt: string | null;
|
|
72
|
-
createdAt: string;
|
|
73
|
-
updatedAt: string;
|
|
74
|
-
}
|
|
75
|
-
export interface SecureKeyPackageClaim {
|
|
76
|
-
deviceId: string;
|
|
77
|
-
keyPackageRef: string;
|
|
78
|
-
keyPackage: string;
|
|
79
|
-
ciphersuite: number;
|
|
80
|
-
}
|
|
81
|
-
export interface SecureConversationMemberModel {
|
|
82
|
-
id: string;
|
|
83
|
-
projectId: string;
|
|
84
|
-
conversationId: string;
|
|
85
|
-
userId: string;
|
|
86
|
-
role: SecureMemberRole;
|
|
87
|
-
isActive: boolean;
|
|
88
|
-
joinedAtEpoch: string | null;
|
|
89
|
-
lastReadAt: string | null;
|
|
90
|
-
leftAt: string | null;
|
|
91
|
-
createdAt: string;
|
|
92
|
-
updatedAt: string;
|
|
93
|
-
}
|
|
94
|
-
export interface SecureConversationModel {
|
|
95
|
-
id: string;
|
|
96
|
-
projectId: string;
|
|
97
|
-
type: SecureConversationType;
|
|
98
|
-
mlsGroupId: string;
|
|
99
|
-
spaceId: string | null;
|
|
100
|
-
currentEpoch: string;
|
|
101
|
-
name: string | null;
|
|
102
|
-
createdById: string | null;
|
|
103
|
-
lastMessageAt: string | null;
|
|
104
|
-
memberCount?: number;
|
|
105
|
-
unreadCount?: number;
|
|
106
|
-
currentMember?: SecureConversationMemberModel;
|
|
107
|
-
createdAt: string;
|
|
108
|
-
updatedAt: string;
|
|
109
|
-
}
|
|
110
|
-
export interface SecureMessageModel {
|
|
111
|
-
id: string;
|
|
112
|
-
projectId: string;
|
|
113
|
-
conversationId: string;
|
|
114
|
-
senderUserId: string | null;
|
|
115
|
-
senderDeviceId: string | null;
|
|
116
|
-
epoch: string;
|
|
117
|
-
ciphertext: string;
|
|
118
|
-
contentType: string;
|
|
119
|
-
createdAt: string;
|
|
120
|
-
}
|
|
121
|
-
export interface SecureHandshakeModel {
|
|
122
|
-
id: string;
|
|
123
|
-
seq: string;
|
|
124
|
-
kind: SecureHandshakeKind;
|
|
125
|
-
conversationId: string;
|
|
126
|
-
epoch: string;
|
|
127
|
-
payload: string;
|
|
128
|
-
senderDeviceId: string | null;
|
|
129
|
-
targetDeviceId: string | null;
|
|
130
|
-
}
|
|
131
|
-
export interface SecureKeyBackupModel {
|
|
132
|
-
id: string;
|
|
133
|
-
projectId: string;
|
|
134
|
-
userId: string;
|
|
135
|
-
deviceId: string | null;
|
|
136
|
-
blob: string;
|
|
137
|
-
nonce: string;
|
|
138
|
-
kdf: string;
|
|
139
|
-
kdfParams: Record<string, unknown>;
|
|
140
|
-
cipher: string;
|
|
141
|
-
version: number;
|
|
142
|
-
createdAt: string;
|
|
143
|
-
updatedAt: string;
|
|
144
|
-
}
|
|
145
|
-
/** Standard error envelope: `{ error, code, field? }` with `secure-chat/*` codes. */
|
|
146
|
-
export interface SecureChatErrorBody {
|
|
147
|
-
error: string;
|
|
148
|
-
code: string;
|
|
149
|
-
field?: string;
|
|
150
|
-
}
|
|
1
|
+
export type { SecureDeviceModel, SecureKeyPackageClaim, SecureConversationMemberModel, SecureConversationModel, SecureMessageModel, SecureHandshakeModel, SecureKeyBackupModel, } from "@agora-server/contract";
|
|
2
|
+
export type { RegisterDeviceBody, PublishKeyPackagesBody, CreateSecureConversationBody, AddSecureMemberBody, RemoveSecureMemberBody, SendSecureMessageBody, UploadKeyBackupBody, WelcomeEnvelope, HandshakeBlob, } from "@agora-server/contract";
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
// Secure-chat wire
|
|
1
|
+
// Secure-chat wire types — re-exported from the published `@agora-server/contract` (Apache-2.0).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// The dependency arrow is **SDK → contract**: agora-server owns the wire contract, this SDK depends
|
|
4
|
+
// on it. This module used to hold a byte-faithful *copy* of the types (a stand-in until the contract
|
|
5
|
+
// published); now that `@agora-server/contract` is published, it is a thin **type-only re-export** so
|
|
6
|
+
// there is exactly one source of truth and zero drift. The internal import path
|
|
7
|
+
// (`../contract/index.js`) is kept stable so call sites don't churn, and the re-export is scoped to
|
|
8
|
+
// the secure-chat surface (not the contract's reactions/pagination/etc.).
|
|
6
9
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// Do NOT publish a separate `@agora-sdk/secure-chat-contract` (that would invert the dependency —
|
|
10
|
-
// see STATUS.md). Do not let this copy drift in the meantime.
|
|
10
|
+
// Type-only on purpose: `@agora-server/contract` is ESM-only, but these `export type` re-exports are
|
|
11
|
+
// erased from the emitted JS, so core's dual ESM/CJS build never `require()`s it at runtime.
|
|
11
12
|
//
|
|
12
|
-
// Wire conventions: every binary value is **base64
|
|
13
|
-
//
|
|
13
|
+
// Wire conventions (owned by the contract): every binary value is **base64**; MLS epochs are
|
|
14
|
+
// **decimal strings** (u64 exceeds JS safe-int range).
|
|
14
15
|
export {};
|
|
15
16
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/contract/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/contract/index.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,EAAE;AACF,oGAAoG;AACpG,qGAAqG;AACrG,sGAAsG;AACtG,gFAAgF;AAChF,oGAAoG;AACpG,0EAA0E;AAC1E,EAAE;AACF,qGAAqG;AACrG,6FAA6F;AAC7F,EAAE;AACF,6FAA6F;AAC7F,uDAAuD"}
|