@totalreclaw/totalreclaw 3.3.1-rc.11 → 3.3.1-rc.12

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/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [3.3.1-rc.12] — 2026-04-23
8
+
9
+ **Ship-stopper fix for rc.11.** The relay-served pair page's submit
10
+ button threw `NotSupportedError: Failed to execute 'importKey' on
11
+ 'SubtleCrypto': Algorithm: Unrecognized name` when the user clicked
12
+ "Seal key and finish". Root cause: `ChaCha20-Poly1305` is NOT
13
+ implemented in the Web Crypto API of Chrome / Safari / Edge — the
14
+ spec exposes `AES-GCM` as the only AEAD. rc.10/rc.11 never worked
15
+ end-to-end for any user; every pair attempt failed silently and the
16
+ token expired without logging a failure — GH issue #79.
17
+
18
+ rc.12 swaps the cipher suite from ChaCha20-Poly1305 to AES-256-GCM on
19
+ both sides (browser + gateway). Wire shape unchanged — still 12-byte
20
+ nonce, 16-byte tag, sid-bound AAD, base64url encoding. HKDF info bumped
21
+ from `totalreclaw-pair-v1` to `totalreclaw-pair-v2` so rc.11 ciphertexts
22
+ cannot collide with rc.12 keys (fail-closed on any version skew).
23
+
24
+ ### Changed
25
+ - `skill/plugin/pair-crypto.ts`: `aeadDecrypt` / `aeadEncryptWithSessionKey`
26
+ switched from `chacha20-poly1305` to `aes-256-gcm`. `HKDF_INFO` bumped
27
+ to `totalreclaw-pair-v2`.
28
+ - `skill/plugin/pair-page.ts` (local-mode pair page): WebCrypto
29
+ `ChaCha20-Poly1305` calls swapped to `AES-GCM`. Capability probe
30
+ function renamed `chaChaSupported` → `aesGcmSupported`.
31
+
32
+ ### Observability
33
+ - The relay's `pair-html.ts` (user-facing page) now reports phase-labelled
34
+ error messages so a network / encrypt / submit failure no longer masks
35
+ as a silent "stuck on acknowledge screen". Relay PR (fix/pair-aes-gcm-rc12)
36
+ is the canonical fix for the issue reported in #79.
37
+
7
38
  ## [3.3.1-rc.11] — 2026-04-23
8
39
 
9
40
  OpenClaw-side universal pair reachability — the plugin's `totalreclaw_pair` tool now routes through the relay WebSocket by default, mirroring the Python `2.3.1rc10` pivot on the Hermes side. The URL returned to the user is `https://api-staging.totalreclaw.xyz/pair/p/<token>#pk=<gateway_pubkey>` instead of the previous `http://<gateway-host>:<port>/plugin/totalreclaw/pair/finish?sid=<sid>#pk=…`. Managed hosts, Docker-in-cloud setups, phone-scan-QR flows, and split-network operators can now complete pairing without the browser needing loopback or LAN access to the gateway.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.1-rc.11",
3
+ "version": "3.3.1-rc.12",
4
4
  "description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
5
5
  "type": "module",
6
6
  "keywords": [
package/pair-crypto.ts CHANGED
@@ -1,19 +1,24 @@
1
1
  /**
2
- * pair-crypto — gateway-side cryptographic primitives for the v3.3.0
3
- * QR-pairing flow.
2
+ * pair-crypto — gateway-side cryptographic primitives for the v3.3.x
3
+ * relay-brokered pair flow.
4
4
  *
5
- * Cipher suite (per design doc section 3a-3b, ratified 2026-04-20):
5
+ * Cipher suite (design doc 3a-3b, cipher swap ratified 2026-04-23 / rc.12):
6
6
  * - ECDH on x25519 for key agreement.
7
7
  * - HKDF-SHA256 for symmetric-key derivation from the shared secret.
8
- * - ChaCha20-Poly1305 AEAD for the ciphertext payload, with the sid
9
- * bound as associated data (AD = sid UTF-8 bytes).
8
+ * - AES-256-GCM AEAD for the ciphertext payload, with the sid bound as
9
+ * associated data (AD = sid UTF-8 bytes, 12-byte nonce, 16-byte tag).
10
+ *
11
+ * rc.4..rc.11 used ChaCha20-Poly1305, but the Web Crypto API does NOT
12
+ * implement ChaCha20-Poly1305 in Chrome / Safari / Edge. The pair-page
13
+ * submit path silently threw `Algorithm: Unrecognized name` before
14
+ * reaching the network. rc.12 swaps the cipher suite to AES-256-GCM
15
+ * (universally supported in WebCrypto) and bumps HKDF_INFO to v2 so
16
+ * cross-version mis-pairs fail closed rather than garble.
10
17
  *
11
18
  * Every primitive is provided by the Node built-in `node:crypto` module
12
19
  * on Node 18.19+ and above. NO third-party crypto dependency is added
13
- * to the plugin for the gateway side. (The BROWSER side of the flow in
14
- * P2 uses WebCrypto with a `@noble/curves` + `@noble/ciphers` fallback
15
- * for older Safari — those ship as part of the served pairing page and
16
- * do NOT affect the plugin's server-side dep tree.)
20
+ * to the plugin for the gateway side. (The BROWSER side of the flow uses
21
+ * WebCrypto's AES-GCM directly no shim needed.)
17
22
  *
18
23
  * Scope and guarantees
19
24
  * --------------------
@@ -33,11 +38,11 @@
33
38
  *
34
39
  * Interoperability with browser WebCrypto
35
40
  * ---------------------------------------
36
- * The WebCrypto x25519 + HKDF + ChaCha20-Poly1305 APIs are bit-for-bit
37
- * compatible with Node's `crypto` as long as:
41
+ * The WebCrypto x25519 + HKDF + AES-GCM APIs are bit-for-bit compatible
42
+ * with Node's `crypto` as long as:
38
43
  * - Raw 32-byte public/private keys are used (not DER/SPKI).
39
44
  * - HKDF parameters are (hash=SHA-256, salt=sid bytes, info fixed
40
- * ASCII string, length=32 bytes → 256-bit AEAD key).
45
+ * ASCII string "totalreclaw-pair-v2", length=32 bytes).
41
46
  * - AEAD uses a 12-byte random nonce + 16-byte tag, AD = sid bytes.
42
47
  * See tests for fixed test vectors.
43
48
  */
@@ -60,18 +65,23 @@ import {
60
65
 
61
66
  /**
62
67
  * HKDF "info" parameter — fixes the domain separation for this protocol.
63
- * MUST match the browser-side constant in the pair-page bundle (P2).
64
- * Versioned so we can roll to a new KDF without breaking old ciphertexts.
68
+ * MUST match the browser-side constant in the pair-page bundle + the
69
+ * relay-served pair-html page.
70
+ *
71
+ * Versioned so we can roll to a new KDF or cipher suite without silently
72
+ * producing garbage with old ciphertexts. rc.12: bumped from v1 to v2
73
+ * after cipher-suite swap from ChaCha20-Poly1305 → AES-256-GCM (see
74
+ * module header comment for context).
65
75
  */
66
- export const HKDF_INFO = 'totalreclaw-pair-v1';
76
+ export const HKDF_INFO = 'totalreclaw-pair-v2';
67
77
 
68
- /** HKDF output length — 32 bytes = 256-bit ChaCha20-Poly1305 key. */
78
+ /** HKDF output length — 32 bytes = 256-bit AES-256-GCM key. */
69
79
  export const AEAD_KEY_BYTES = 32;
70
80
 
71
- /** ChaCha20-Poly1305 nonce length — 12 bytes per RFC 7539. */
81
+ /** AES-GCM nonce length — 12 bytes (SP 800-38D recommendation). */
72
82
  export const AEAD_NONCE_BYTES = 12;
73
83
 
74
- /** ChaCha20-Poly1305 auth tag length — 16 bytes standard. */
84
+ /** AES-GCM auth tag length — 16 bytes (128 bits, standard). */
75
85
  export const AEAD_TAG_BYTES = 16;
76
86
 
77
87
  /** Raw x25519 public/private key length — 32 bytes per RFC 7748. */
@@ -101,7 +111,7 @@ export interface GatewayKeypair {
101
111
 
102
112
  /** Fully-derived session keys — caller uses kEnc for AEAD ops. */
103
113
  export interface SessionKeys {
104
- /** 32-byte ChaCha20-Poly1305 key. */
114
+ /** 32-byte AES-256-GCM key. */
105
115
  kEnc: Buffer;
106
116
  }
107
117
 
@@ -323,9 +333,9 @@ export function deriveAeadKeyFromEcdh(opts: {
323
333
  // ---------------------------------------------------------------------------
324
334
 
325
335
  /**
326
- * Decrypt a ChaCha20-Poly1305 AEAD ciphertext. Returns the plaintext
327
- * on success; throws if the tag is invalid (which includes both
328
- * tampering and wrong-key attempts).
336
+ * Decrypt an AES-256-GCM AEAD ciphertext. Returns the plaintext on
337
+ * success; throws if the tag is invalid (which includes both tampering
338
+ * and wrong-key attempts).
329
339
  *
330
340
  * Ciphertext is expected in the combined form `plaintext || tag`, where
331
341
  * tag is the trailing 16 bytes. The caller MUST supply the same
@@ -351,7 +361,7 @@ export function aeadDecrypt(opts: {
351
361
  const ct = combined.subarray(0, combined.length - AEAD_TAG_BYTES);
352
362
  const tag = combined.subarray(combined.length - AEAD_TAG_BYTES);
353
363
 
354
- const decipher = createDecipheriv('chacha20-poly1305', opts.kEnc, nonce, {
364
+ const decipher = createDecipheriv('aes-256-gcm', opts.kEnc, nonce, {
355
365
  authTagLength: AEAD_TAG_BYTES,
356
366
  });
357
367
  decipher.setAAD(Buffer.from(opts.sid, 'utf-8'), { plaintextLength: ct.length });
@@ -409,7 +419,7 @@ export function aeadEncryptWithSessionKey(opts: {
409
419
  }
410
420
 
411
421
  const pt = Buffer.isBuffer(opts.plaintext) ? opts.plaintext : Buffer.from(opts.plaintext);
412
- const cipher = createCipheriv('chacha20-poly1305', opts.kEnc, nonceBuf, {
422
+ const cipher = createCipheriv('aes-256-gcm', opts.kEnc, nonceBuf, {
413
423
  authTagLength: AEAD_TAG_BYTES,
414
424
  });
415
425
  cipher.setAAD(Buffer.from(opts.sid, 'utf-8'), { plaintextLength: pt.length });
package/pair-page.ts CHANGED
@@ -364,7 +364,9 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
364
364
  // Client-observed time at page load (used to adjust for clock skew).
365
365
  const CLIENT_EPOCH_AT_LOAD = Date.now();
366
366
 
367
- const HKDF_INFO = "totalreclaw-pair-v1";
367
+ // v2: cipher-suite swap in rc.12 (see pair-crypto.ts header). Keep
368
+ // this constant in lockstep with the gateway-side pair-crypto.ts.
369
+ const HKDF_INFO = "totalreclaw-pair-v2";
368
370
 
369
371
  // ---------- Small utilities ----------
370
372
  function $(sel, root) { return (root || document).querySelector(sel); }
@@ -473,10 +475,9 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
473
475
  }
474
476
 
475
477
  // ---------- Crypto shims: prefer WebCrypto; fall back to JS path ----------
476
- // WebCrypto's x25519 + HKDF + ChaCha20-Poly1305 availability is Safari 17+
477
- // and modern Chromium / Firefox. If absent we render an error — the page
478
- // is self-contained, and we elect not to bundle @noble/curves + ciphers
479
- // for the MVP (tracked as Wave 3.1 polish follow-up).
478
+ // WebCrypto's x25519 + HKDF + AES-GCM availability is Safari 17+ and
479
+ // modern Chromium 133+ / Firefox 130+. If absent we render an error
480
+ // — the page is self-contained and we do not bundle any JS crypto shim.
480
481
  async function ensureWebCryptoSupport() {
481
482
  if (!window.crypto || !crypto.subtle) return false;
482
483
  try {
@@ -505,29 +506,28 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
505
506
  );
506
507
  return new Uint8Array(bits);
507
508
  }
508
- // AEAD: WebCrypto offers AES-GCM universally; ChaCha20-Poly1305 support is
509
- // newer. We attempt chacha first; if it throws, we abort (do NOT silently
510
- // swap ciphers that would mismatch the gateway).
511
- async function aeadEncryptChaCha(keyBytes, nonce, sid, plaintext) {
509
+ // AEAD: AES-256-GCM (universal in WebCrypto). Cipher swap rationale
510
+ // lives in pair-crypto.ts header comment (rc.12 changelog entry).
511
+ async function aeadEncryptAesGcm(keyBytes, nonce, sid, plaintext) {
512
512
  const key = await crypto.subtle.importKey(
513
513
  "raw", keyBytes,
514
- { name: "ChaCha20-Poly1305" },
514
+ { name: "AES-GCM" },
515
515
  false, ["encrypt"],
516
516
  );
517
517
  const adBytes = new TextEncoder().encode(sid);
518
518
  const ct = new Uint8Array(await crypto.subtle.encrypt(
519
- { name: "ChaCha20-Poly1305", iv: nonce, additionalData: adBytes, tagLength: 128 },
519
+ { name: "AES-GCM", iv: nonce, additionalData: adBytes, tagLength: 128 },
520
520
  key, plaintext,
521
521
  ));
522
522
  return ct;
523
523
  }
524
524
 
525
- async function chaChaSupported() {
525
+ async function aesGcmSupported() {
526
526
  try {
527
527
  const k = new Uint8Array(32);
528
528
  const n = new Uint8Array(12);
529
- const key = await crypto.subtle.importKey("raw", k, { name: "ChaCha20-Poly1305" }, false, ["encrypt"]);
530
- await crypto.subtle.encrypt({ name: "ChaCha20-Poly1305", iv: n, additionalData: new Uint8Array(0), tagLength: 128 }, key, new Uint8Array(0));
529
+ const key = await crypto.subtle.importKey("raw", k, { name: "AES-GCM" }, false, ["encrypt"]);
530
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: n, additionalData: new Uint8Array(0), tagLength: 128 }, key, new Uint8Array(0));
531
531
  return true;
532
532
  } catch (e) { return false; }
533
533
  }
@@ -742,8 +742,8 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
742
742
  render(renderError("Your browser does not support modern cryptographic APIs (X25519). Please update your browser and try again, or use a different device. Supported: Chrome 123+, Firefox 130+, Safari 17+."));
743
743
  return;
744
744
  }
745
- if (!(await chaChaSupported())) {
746
- render(renderError("Your browser does not support ChaCha20-Poly1305. Update your browser or use a different device."));
745
+ if (!(await aesGcmSupported())) {
746
+ render(renderError("Your browser does not support AES-GCM. Update your browser or use a different device."));
747
747
  return;
748
748
  }
749
749
 
@@ -762,7 +762,7 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
762
762
  const nonce = new Uint8Array(12);
763
763
  crypto.getRandomValues(nonce);
764
764
  const ptBytes = new TextEncoder().encode(mnemonic);
765
- const ct = await aeadEncryptChaCha(kEnc, nonce, SID, ptBytes);
765
+ const ct = await aeadEncryptAesGcm(kEnc, nonce, SID, ptBytes);
766
766
 
767
767
  // 6. Zero sensitive buffers BEFORE sending. The JS GC will run
768
768
  // whenever it runs; explicit zeroing is best-effort but honours
@@ -7,7 +7,7 @@
7
7
  * Python implementation byte-for-byte so either side can open a session that
8
8
  * the relay (`totalreclaw-relay`) + browser page (`pair-html.ts`) already
9
9
  * understand. Crypto primitives come from the shared ``pair-crypto.ts``
10
- * module — the same ECDH + HKDF + ChaCha20-Poly1305 stack the loopback HTTP
10
+ * module — the same ECDH + HKDF + AES-256-GCM stack the loopback HTTP
11
11
  * server uses.
12
12
  *
13
13
  * Flow (this file implements the gateway half):