rechrome 1.36.0 → 1.37.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/README.md +74 -3
- package/e2e.js +164 -0
- package/e2e.ts +106 -0
- package/package.json +19 -5
- package/rechrome.js +537 -34
- package/rechrome.ts +537 -34
- package/secret.js +116 -0
- package/secret.ts +113 -0
- package/serve.js +259 -51
- package/serve.ts +259 -51
- package/shares.js +349 -0
- package/shares.ts +257 -0
- package/signal-relay.js +112 -0
- package/signal-relay.ts +99 -0
- package/webrtc.js +879 -0
- package/webrtc.ts +535 -0
package/secret.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// secret.ts
|
|
3
|
+
import { hkdfSync, randomBytes, timingSafeEqual } from "crypto";
|
|
4
|
+
var TOKEN_PREFIX = "rech1_";
|
|
5
|
+
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
6
|
+
var B58 = "[1-9A-HJ-NP-Za-km-z]";
|
|
7
|
+
var BODY_LEN = 22;
|
|
8
|
+
var SUM_LEN = 6;
|
|
9
|
+
var SECRET_BYTES = 16;
|
|
10
|
+
var TOKEN_PATTERN = new RegExp(`\\brech1_${B58}{${BODY_LEN + SUM_LEN}}\\b`, "g");
|
|
11
|
+
var EXACT = new RegExp(`^rech1_(${B58}{${BODY_LEN}})(${B58}{${SUM_LEN}})$`);
|
|
12
|
+
function base58(bytes, width) {
|
|
13
|
+
let n = 0n;
|
|
14
|
+
for (const b of bytes)
|
|
15
|
+
n = n << 8n | BigInt(b);
|
|
16
|
+
let s = "";
|
|
17
|
+
while (n > 0n) {
|
|
18
|
+
s = ALPHABET[Number(n % 58n)] + s;
|
|
19
|
+
n /= 58n;
|
|
20
|
+
}
|
|
21
|
+
if (s.length > width)
|
|
22
|
+
throw new Error("base58 value wider than its field");
|
|
23
|
+
return s.padStart(width, "1");
|
|
24
|
+
}
|
|
25
|
+
function unbase58(text, length) {
|
|
26
|
+
let n = 0n;
|
|
27
|
+
for (const c of text) {
|
|
28
|
+
const i = ALPHABET.indexOf(c);
|
|
29
|
+
if (i < 0)
|
|
30
|
+
return null;
|
|
31
|
+
n = n * 58n + BigInt(i);
|
|
32
|
+
}
|
|
33
|
+
const out = new Uint8Array(length);
|
|
34
|
+
for (let i = length - 1;i >= 0; i--) {
|
|
35
|
+
out[i] = Number(n & 0xffn);
|
|
36
|
+
n >>= 8n;
|
|
37
|
+
}
|
|
38
|
+
return n === 0n ? out : null;
|
|
39
|
+
}
|
|
40
|
+
var CRC_TABLE = (() => {
|
|
41
|
+
const t = new Uint32Array(256);
|
|
42
|
+
for (let i = 0;i < 256; i++) {
|
|
43
|
+
let c = i;
|
|
44
|
+
for (let k = 0;k < 8; k++)
|
|
45
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
46
|
+
t[i] = c >>> 0;
|
|
47
|
+
}
|
|
48
|
+
return t;
|
|
49
|
+
})();
|
|
50
|
+
function crc32(bytes) {
|
|
51
|
+
let c = 4294967295;
|
|
52
|
+
for (const b of bytes)
|
|
53
|
+
c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
|
|
54
|
+
return (c ^ 4294967295) >>> 0;
|
|
55
|
+
}
|
|
56
|
+
var u32 = (n) => new Uint8Array([n >>> 24, n >>> 16 & 255, n >>> 8 & 255, n & 255]);
|
|
57
|
+
function formatToken(secret) {
|
|
58
|
+
if (secret.length !== SECRET_BYTES)
|
|
59
|
+
throw new Error(`a share secret is ${SECRET_BYTES} bytes`);
|
|
60
|
+
return `${TOKEN_PREFIX}${base58(secret, BODY_LEN)}${base58(u32(crc32(secret)), SUM_LEN)}`;
|
|
61
|
+
}
|
|
62
|
+
function newToken() {
|
|
63
|
+
return formatToken(new Uint8Array(randomBytes(SECRET_BYTES)));
|
|
64
|
+
}
|
|
65
|
+
function parseToken(token) {
|
|
66
|
+
const m = EXACT.exec(token.trim());
|
|
67
|
+
if (!m)
|
|
68
|
+
return { ok: false, reason: "format" };
|
|
69
|
+
const secret = unbase58(m[1], SECRET_BYTES), sum = unbase58(m[2], 4);
|
|
70
|
+
if (!secret || !sum)
|
|
71
|
+
return { ok: false, reason: "format" };
|
|
72
|
+
const expected = u32(crc32(secret));
|
|
73
|
+
return sum.every((b, i) => b === expected[i]) ? { ok: true, secret } : { ok: false, reason: "checksum" };
|
|
74
|
+
}
|
|
75
|
+
function isToken(text) {
|
|
76
|
+
return parseToken(text).ok;
|
|
77
|
+
}
|
|
78
|
+
function looksLikeToken(text) {
|
|
79
|
+
return EXACT.test(text.trim());
|
|
80
|
+
}
|
|
81
|
+
function secretOf(token) {
|
|
82
|
+
const parsed = parseToken(token);
|
|
83
|
+
if (!parsed.ok)
|
|
84
|
+
throw new Error(parsed.reason === "checksum" ? "This rech1_ link is damaged (checksum mismatch): it was copied incompletely or mistyped." : "Not a rech1_ link.");
|
|
85
|
+
return parsed.secret;
|
|
86
|
+
}
|
|
87
|
+
var derive = (token, label, bytes) => new Uint8Array(hkdfSync("sha256", secretOf(token), new Uint8Array(0), `rech/${label}`, bytes));
|
|
88
|
+
var roomOf = (token) => base58(derive(token, "room", 12), 17);
|
|
89
|
+
var roomAuthOf = (token) => Buffer.from(derive(token, "auth", 32)).toString("hex");
|
|
90
|
+
var bearerOf = (token) => `rb_${base58(derive(token, "bearer", 24), 33)}`;
|
|
91
|
+
var credentialIdOf = (token) => base58(derive(token, "id", 6), 9);
|
|
92
|
+
function safeEqual(a, b) {
|
|
93
|
+
const x = Buffer.from(a), y = Buffer.from(b);
|
|
94
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
95
|
+
}
|
|
96
|
+
function redactSecrets(text) {
|
|
97
|
+
return text.replace(TOKEN_PATTERN, (t) => `${t.slice(0, TOKEN_PREFIX.length + 4)}\u2026`).replace(/([#&?]key=)[^&\s"]*/g, "$1\u2026");
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
TOKEN_PATTERN,
|
|
101
|
+
TOKEN_PREFIX,
|
|
102
|
+
base58,
|
|
103
|
+
bearerOf,
|
|
104
|
+
crc32,
|
|
105
|
+
credentialIdOf,
|
|
106
|
+
formatToken,
|
|
107
|
+
isToken,
|
|
108
|
+
looksLikeToken,
|
|
109
|
+
newToken,
|
|
110
|
+
parseToken,
|
|
111
|
+
redactSecrets,
|
|
112
|
+
roomAuthOf,
|
|
113
|
+
roomOf,
|
|
114
|
+
safeEqual,
|
|
115
|
+
unbase58
|
|
116
|
+
};
|
package/secret.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Share secrets: `rech1_` + 22 base58 chars (128 random bits) + 6 base58 chars (CRC32).
|
|
2
|
+
//
|
|
3
|
+
// The unique prefix lets secret scanners match with few false positives (`sk-` would be
|
|
4
|
+
// mistaken for OpenAI-style keys), base58 drops look-alike characters (0 O I l) and keeps the
|
|
5
|
+
// token one double-click word, and the checksum catches a truncated or mistyped link offline.
|
|
6
|
+
// Everything a party needs is derived one-way from the secret with HKDF, so a signal server or
|
|
7
|
+
// HTTP proxy never holds anything that would let it derive another value or the secret itself.
|
|
8
|
+
import { hkdfSync, randomBytes, timingSafeEqual } from "crypto";
|
|
9
|
+
|
|
10
|
+
export const TOKEN_PREFIX = "rech1_";
|
|
11
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
12
|
+
const B58 = "[1-9A-HJ-NP-Za-km-z]";
|
|
13
|
+
const BODY_LEN = 22, SUM_LEN = 6, SECRET_BYTES = 16;
|
|
14
|
+
/** Matches a token anywhere in text (logs, files); for scanners and redaction. */
|
|
15
|
+
export const TOKEN_PATTERN = new RegExp(`\\brech1_${B58}{${BODY_LEN + SUM_LEN}}\\b`, "g");
|
|
16
|
+
const EXACT = new RegExp(`^rech1_(${B58}{${BODY_LEN}})(${B58}{${SUM_LEN}})$`);
|
|
17
|
+
|
|
18
|
+
export function base58(bytes: Uint8Array, width: number): string {
|
|
19
|
+
let n = 0n;
|
|
20
|
+
for (const b of bytes) n = (n << 8n) | BigInt(b);
|
|
21
|
+
let s = "";
|
|
22
|
+
while (n > 0n) { s = ALPHABET[Number(n % 58n)] + s; n /= 58n; }
|
|
23
|
+
if (s.length > width) throw new Error("base58 value wider than its field");
|
|
24
|
+
return s.padStart(width, "1");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function unbase58(text: string, length: number): Uint8Array | null {
|
|
28
|
+
let n = 0n;
|
|
29
|
+
for (const c of text) {
|
|
30
|
+
const i = ALPHABET.indexOf(c);
|
|
31
|
+
if (i < 0) return null;
|
|
32
|
+
n = n * 58n + BigInt(i);
|
|
33
|
+
}
|
|
34
|
+
const out = new Uint8Array(length);
|
|
35
|
+
for (let i = length - 1; i >= 0; i--) { out[i] = Number(n & 0xffn); n >>= 8n; }
|
|
36
|
+
return n === 0n ? out : null; // leftover bits: not a fixed-width encoding of `length` bytes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const CRC_TABLE = (() => {
|
|
40
|
+
const t = new Uint32Array(256);
|
|
41
|
+
for (let i = 0; i < 256; i++) {
|
|
42
|
+
let c = i;
|
|
43
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
44
|
+
t[i] = c >>> 0;
|
|
45
|
+
}
|
|
46
|
+
return t;
|
|
47
|
+
})();
|
|
48
|
+
export function crc32(bytes: Uint8Array): number {
|
|
49
|
+
let c = 0xffffffff;
|
|
50
|
+
for (const b of bytes) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
51
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
const u32 = (n: number) => new Uint8Array([n >>> 24, (n >>> 16) & 255, (n >>> 8) & 255, n & 255]);
|
|
54
|
+
|
|
55
|
+
export function formatToken(secret: Uint8Array): string {
|
|
56
|
+
if (secret.length !== SECRET_BYTES) throw new Error(`a share secret is ${SECRET_BYTES} bytes`);
|
|
57
|
+
return `${TOKEN_PREFIX}${base58(secret, BODY_LEN)}${base58(u32(crc32(secret)), SUM_LEN)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function newToken(): string {
|
|
61
|
+
return formatToken(new Uint8Array(randomBytes(SECRET_BYTES)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type ParsedToken = { ok: true; secret: Uint8Array } | { ok: false; reason: "format" | "checksum" };
|
|
65
|
+
|
|
66
|
+
/** Parse a token, telling a non-token apart from a damaged one (a truncated paste, a typo). */
|
|
67
|
+
export function parseToken(token: string): ParsedToken {
|
|
68
|
+
const m = EXACT.exec(token.trim());
|
|
69
|
+
if (!m) return { ok: false, reason: "format" };
|
|
70
|
+
const secret = unbase58(m[1], SECRET_BYTES), sum = unbase58(m[2], 4);
|
|
71
|
+
if (!secret || !sum) return { ok: false, reason: "format" };
|
|
72
|
+
const expected = u32(crc32(secret));
|
|
73
|
+
return sum.every((b, i) => b === expected[i]) ? { ok: true, secret } : { ok: false, reason: "checksum" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isToken(text: string): boolean {
|
|
77
|
+
return parseToken(text).ok;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Looks like a token (prefix and shape) whether or not its checksum holds. */
|
|
81
|
+
export function looksLikeToken(text: string): boolean {
|
|
82
|
+
return EXACT.test(text.trim());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function secretOf(token: string): Uint8Array {
|
|
86
|
+
const parsed = parseToken(token);
|
|
87
|
+
if (!parsed.ok) throw new Error(parsed.reason === "checksum"
|
|
88
|
+
? "This rech1_ link is damaged (checksum mismatch): it was copied incompletely or mistyped."
|
|
89
|
+
: "Not a rech1_ link.");
|
|
90
|
+
return parsed.secret;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const derive = (token: string, label: string, bytes: number) =>
|
|
94
|
+
new Uint8Array(hkdfSync("sha256", secretOf(token), new Uint8Array(0), `rech/${label}`, bytes));
|
|
95
|
+
|
|
96
|
+
/** Signal room id (the signal server sees it). */
|
|
97
|
+
export const roomOf = (token: string) => base58(derive(token, "room", 12), 17);
|
|
98
|
+
/** Signal room admission (the signal server sees it; one-way). */
|
|
99
|
+
export const roomAuthOf = (token: string) => Buffer.from(derive(token, "auth", 32)).toString("hex");
|
|
100
|
+
/** Bearer for HTTP transports (a proxy in front of a listener sees it; one-way). */
|
|
101
|
+
export const bearerOf = (token: string) => `rb_${base58(derive(token, "bearer", 24), 33)}`;
|
|
102
|
+
/** Stable, non-secret id for a device credential (log lines, `share devices`). */
|
|
103
|
+
export const credentialIdOf = (token: string) => base58(derive(token, "id", 6), 9);
|
|
104
|
+
|
|
105
|
+
export function safeEqual(a: string, b: string): boolean {
|
|
106
|
+
const x = Buffer.from(a), y = Buffer.from(b);
|
|
107
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Hide every token (and legacy #key=…) in text meant for a terminal or a log. */
|
|
111
|
+
export function redactSecrets(text: string): string {
|
|
112
|
+
return text.replace(TOKEN_PATTERN, t => `${t.slice(0, TOKEN_PREFIX.length + 4)}…`).replace(/([#&?]key=)[^&\s"]*/g, "$1…");
|
|
113
|
+
}
|