rechrome 1.47.0 → 1.48.1
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 +4 -0
- package/e2e.js +2 -2
- package/e2e.ts +2 -2
- package/extension-token.js +37 -1
- package/extension-token.ts +37 -1
- package/fill-secret.js +542 -40
- package/fill-secret.ts +293 -33
- package/listeners.js +9 -5
- package/listeners.ts +10 -4
- package/package.json +9 -2
- package/rechrome.js +96 -21
- package/rechrome.ts +96 -21
- package/record.js +288 -0
- package/record.ts +258 -0
- package/rtc-channel.js +303 -0
- package/rtc-channel.ts +130 -0
- package/secret.js +15 -10
- package/secret.ts +2 -70
- package/serve.js +140 -13
- package/serve.ts +140 -13
- package/shares.js +49 -12
- package/shares.ts +39 -6
- package/token.js +88 -0
- package/token.ts +74 -0
- package/webrtc.js +92 -85
- package/webrtc.ts +6 -128
package/README.md
CHANGED
|
@@ -177,6 +177,10 @@ Over **WebRTC** the two machines need no open port, VPN or domain. A small signa
|
|
|
177
177
|
encrypted with keys the signal server never sees. The first `rech` command after connecting starts
|
|
178
178
|
a small local bridge that keeps the connection open between commands.
|
|
179
179
|
|
|
180
|
+
**Opening the link in a browser shows what it gives**, without redeeming it: whether the host is
|
|
181
|
+
online, the share and when it ends, whether the invite is still open, its profiles, and its open
|
|
182
|
+
sessions. Anyone holding the link can see that. `rech connect` lists the open sessions too.
|
|
183
|
+
|
|
180
184
|
**The link is an invite, not a key.** `rech connect` redeems it once: this machine gets its own
|
|
181
185
|
device credential (saved to `.rechrome/.env.local`) and the invite stops working, so a link that
|
|
182
186
|
leaks after it was used is harmless, and if someone else used it first you'll know. Unused invites
|
package/e2e.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
//
|
|
2
|
+
// token.ts
|
|
3
3
|
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
4
4
|
var B58 = "[1-9A-HJ-NP-Za-km-z]";
|
|
5
5
|
var BODY_LEN = 22;
|
|
@@ -145,7 +145,7 @@ async function open(key, frame, th, state) {
|
|
|
145
145
|
var pack = (obj) => enc.encode(JSON.stringify(obj));
|
|
146
146
|
var unpack = (bytes) => JSON.parse(dec.decode(bytes));
|
|
147
147
|
function randomHex(n) {
|
|
148
|
-
return
|
|
148
|
+
return Array.from(globalThis.crypto.getRandomValues(new Uint8Array(n)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
149
149
|
}
|
|
150
150
|
export {
|
|
151
151
|
CONFIRM_TIMEOUT_MS,
|
package/e2e.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The AES keys are HKDF(secret, transcriptHash) where the transcript binds both DTLS
|
|
8
8
|
// fingerprints: a relay that swaps in its own fingerprints derives different keys, and the
|
|
9
9
|
// mandatory key-confirmation round fails closed.
|
|
10
|
-
import { parseToken } from "./
|
|
10
|
+
import { parseToken } from "./token.ts";
|
|
11
11
|
|
|
12
12
|
export const PROTO = "rech-e2e-1";
|
|
13
13
|
export const MAX_CHUNK = 12_000; // plaintext bytes per sealed frame, well under SCTP limits
|
|
@@ -102,5 +102,5 @@ export async function open(key: CryptoKey, frame: ArrayBuffer | Uint8Array, th:
|
|
|
102
102
|
export const pack = (obj: unknown) => enc.encode(JSON.stringify(obj));
|
|
103
103
|
export const unpack = (bytes: Uint8Array) => JSON.parse(dec.decode(bytes));
|
|
104
104
|
export function randomHex(n: number): string {
|
|
105
|
-
return
|
|
105
|
+
return Array.from(globalThis.crypto.getRandomValues(new Uint8Array(n)), b => b.toString(16).padStart(2, "0")).join("");
|
|
106
106
|
}
|
package/extension-token.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// extension-token.ts
|
|
3
|
-
import {
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
5
|
+
import { tmpdir } from "os";
|
|
4
6
|
import { join } from "path";
|
|
5
7
|
function varint(b, cursor) {
|
|
6
8
|
let value = 0;
|
|
@@ -111,7 +113,41 @@ function* logRecords(b) {
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
}
|
|
116
|
+
function readExtensionTokenFromSqlite(userDataDir, profileDir, extensionId) {
|
|
117
|
+
const path = join(userDataDir, profileDir, "LocalStorage");
|
|
118
|
+
if (!existsSync(path))
|
|
119
|
+
return null;
|
|
120
|
+
const query = (file, readonly) => {
|
|
121
|
+
const db = new Database(file, readonly ? { readonly: true } : undefined);
|
|
122
|
+
try {
|
|
123
|
+
const row = db.query("SELECT e.value AS value, e.value_compression_type AS compression FROM map_entries e JOIN maps m ON m.row_id = e.map_id WHERE m.storage_key = ? AND e.key = ?").get(Buffer.from(`chrome-extension://${extensionId}/`, "latin1"), Buffer.from("\x01auth-token", "latin1"));
|
|
124
|
+
if (!row || row.compression !== 0)
|
|
125
|
+
return null;
|
|
126
|
+
const value = Buffer.from(row.value).toString("latin1");
|
|
127
|
+
return /^\x01[A-Za-z0-9_-]{43}$/.test(value) ? value.slice(1) : null;
|
|
128
|
+
} finally {
|
|
129
|
+
db.close();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
return query(path, true);
|
|
134
|
+
} catch {}
|
|
135
|
+
const tmp = mkdtempSync(join(tmpdir(), "rech-ls-"));
|
|
136
|
+
try {
|
|
137
|
+
copyFileSync(path, join(tmp, "LocalStorage"));
|
|
138
|
+
if (existsSync(`${path}-wal`))
|
|
139
|
+
copyFileSync(`${path}-wal`, join(tmp, "LocalStorage-wal"));
|
|
140
|
+
return query(join(tmp, "LocalStorage"), false);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
} finally {
|
|
144
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
114
147
|
function readExtensionTokenFromProfile(userDataDir, profileDir, extensionId) {
|
|
148
|
+
return readExtensionTokenFromSqlite(userDataDir, profileDir, extensionId) ?? readExtensionTokenFromLeveldb(userDataDir, profileDir, extensionId);
|
|
149
|
+
}
|
|
150
|
+
function readExtensionTokenFromLeveldb(userDataDir, profileDir, extensionId) {
|
|
115
151
|
const dir = join(userDataDir, profileDir, "Local Storage", "leveldb");
|
|
116
152
|
const wanted = Buffer.from(`_chrome-extension://${extensionId}\x00\x01auth-token`, "latin1");
|
|
117
153
|
let newest = -1n, token = null;
|
package/extension-token.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
2
4
|
import { join } from "path";
|
|
3
5
|
|
|
4
6
|
function varint(b: Buffer, cursor: { p: number }): number {
|
|
@@ -87,7 +89,41 @@ function* logRecords(b: Buffer): Generator<Buffer> {
|
|
|
87
89
|
// Read files without opening the database or taking Chrome's LevelDB lock. Internal
|
|
88
90
|
// sequence numbers, not filenames/byte order, determine the latest value. Decode
|
|
89
91
|
// SST prefix compression and Snappy, and scope the key to the exact extension origin.
|
|
92
|
+
// Chromium 156+ keeps localStorage in a SQLite file `<profile>/LocalStorage` (maps + map_entries)
|
|
93
|
+
// instead of `Local Storage/leveldb`; keys and values keep the same 0x01-prefixed Latin-1 encoding.
|
|
94
|
+
function readExtensionTokenFromSqlite(userDataDir: string, profileDir: string, extensionId: string): string | null {
|
|
95
|
+
const path = join(userDataDir, profileDir, "LocalStorage");
|
|
96
|
+
if (!existsSync(path)) return null;
|
|
97
|
+
const query = (file: string, readonly: boolean): string | null => {
|
|
98
|
+
const db = new Database(file, readonly ? { readonly: true } : undefined);
|
|
99
|
+
try {
|
|
100
|
+
const row = db.query(
|
|
101
|
+
"SELECT e.value AS value, e.value_compression_type AS compression FROM map_entries e JOIN maps m ON m.row_id = e.map_id WHERE m.storage_key = ? AND e.key = ?",
|
|
102
|
+
).get(Buffer.from(`chrome-extension://${extensionId}/`, "latin1"), Buffer.from("\x01auth-token", "latin1")) as { value: Uint8Array; compression: number } | null;
|
|
103
|
+
if (!row || row.compression !== 0) return null;
|
|
104
|
+
const value = Buffer.from(row.value).toString("latin1");
|
|
105
|
+
return /^\x01[A-Za-z0-9_-]{43}$/.test(value) ? value.slice(1) : null;
|
|
106
|
+
} finally { db.close(); }
|
|
107
|
+
};
|
|
108
|
+
// Chrome keeps the file in WAL mode. While Chrome runs, a recent write (a freshly minted token)
|
|
109
|
+
// can sit in the -wal file only, and a read-only open sees it. Once Chrome has exited and
|
|
110
|
+
// removed the -shm file, a read-only open fails (SQLITE_CANTOPEN): then read a private copy of
|
|
111
|
+
// the file and its -wal, which never touches Chrome's own files. (No `file:` URIs: Bun's SQLite
|
|
112
|
+
// on Linux does not take them.)
|
|
113
|
+
try { return query(path, true); } catch {}
|
|
114
|
+
const tmp = mkdtempSync(join(tmpdir(), "rech-ls-"));
|
|
115
|
+
try {
|
|
116
|
+
copyFileSync(path, join(tmp, "LocalStorage"));
|
|
117
|
+
if (existsSync(`${path}-wal`)) copyFileSync(`${path}-wal`, join(tmp, "LocalStorage-wal"));
|
|
118
|
+
return query(join(tmp, "LocalStorage"), false);
|
|
119
|
+
} catch { return null; } finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
export function readExtensionTokenFromProfile(userDataDir: string, profileDir: string, extensionId: string): string | null {
|
|
123
|
+
return readExtensionTokenFromSqlite(userDataDir, profileDir, extensionId) ?? readExtensionTokenFromLeveldb(userDataDir, profileDir, extensionId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readExtensionTokenFromLeveldb(userDataDir: string, profileDir: string, extensionId: string): string | null {
|
|
91
127
|
const dir = join(userDataDir, profileDir, "Local Storage", "leveldb");
|
|
92
128
|
const wanted = Buffer.from(`_chrome-extension://${extensionId}\0\x01auth-token`, "latin1");
|
|
93
129
|
let newest = -1n, token: string | null = null;
|