@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 +31 -0
- package/package.json +1 -1
- package/pair-crypto.ts +34 -24
- package/pair-page.ts +17 -17
- package/pair-remote-client.ts +1 -1
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.
|
|
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.
|
|
3
|
-
*
|
|
2
|
+
* pair-crypto — gateway-side cryptographic primitives for the v3.3.x
|
|
3
|
+
* relay-brokered pair flow.
|
|
4
4
|
*
|
|
5
|
-
* Cipher suite (
|
|
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
|
-
* -
|
|
9
|
-
*
|
|
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
|
|
14
|
-
*
|
|
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 +
|
|
37
|
-
*
|
|
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
|
|
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
|
|
64
|
-
*
|
|
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-
|
|
76
|
+
export const HKDF_INFO = 'totalreclaw-pair-v2';
|
|
67
77
|
|
|
68
|
-
/** HKDF output length — 32 bytes = 256-bit
|
|
78
|
+
/** HKDF output length — 32 bytes = 256-bit AES-256-GCM key. */
|
|
69
79
|
export const AEAD_KEY_BYTES = 32;
|
|
70
80
|
|
|
71
|
-
/**
|
|
81
|
+
/** AES-GCM nonce length — 12 bytes (SP 800-38D recommendation). */
|
|
72
82
|
export const AEAD_NONCE_BYTES = 12;
|
|
73
83
|
|
|
74
|
-
/**
|
|
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
|
|
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
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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 +
|
|
477
|
-
//
|
|
478
|
-
// is self-contained
|
|
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:
|
|
509
|
-
//
|
|
510
|
-
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
530
|
-
await crypto.subtle.encrypt({ name: "
|
|
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
|
|
746
|
-
render(renderError("Your browser does not support
|
|
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
|
|
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
|
package/pair-remote-client.ts
CHANGED
|
@@ -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 +
|
|
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):
|