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 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
- // secret.ts
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 Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(n))).toString("hex");
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 "./secret.ts";
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 Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(n))).toString("hex");
105
+ return Array.from(globalThis.crypto.getRandomValues(new Uint8Array(n)), b => b.toString(16).padStart(2, "0")).join("");
106
106
  }
@@ -1,6 +1,8 @@
1
1
  // @bun
2
2
  // extension-token.ts
3
- import { readFileSync, readdirSync } from "fs";
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;
@@ -1,4 +1,6 @@
1
- import { readFileSync, readdirSync } from "fs";
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;