@twinsearth/w2m-dsh-plugin 0.5.2 → 0.5.4
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 +57 -0
- package/PROTOCOL.md +122 -0
- package/docs/P2P-CRYPTO.md +225 -0
- package/lib/client.js +513 -0
- package/package.json +7 -3
- package/src/agent/agent.mjs +199 -5
- package/src/agent/crypto.mjs +1618 -0
- package/src/agent/p2p-node.mjs +276 -5
- package/src/agent/p2p.mjs +667 -40
- package/src/plugin/fleet-target.mjs +603 -0
- package/src/plugin/tools.mjs +348 -39
- package/src/relay/peers.mjs +56 -0
package/src/relay/peers.mjs
CHANGED
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
* * **Bounded.** Candidate count, address shape and TTL are all validated, because
|
|
21
21
|
* this is the one place where a peer supplies an address another peer will send
|
|
22
22
|
* UDP datagrams to.
|
|
23
|
+
* * **The static public key is a *distribution* channel, not an authority (v0.6.0).** A machine
|
|
24
|
+
* announces its own X25519 public key with its candidates, and a peer reads it back — which is
|
|
25
|
+
* what removes the `[Peer]` block nobody wants to fill in by hand. What the relay can and cannot
|
|
26
|
+
* do here is worth stating plainly: it can substitute a key, because a peer learns the key from
|
|
27
|
+
* the relay's answer (or from the dispatcher's offer, which the relay also carries verbatim). It
|
|
28
|
+
* cannot *use* a substituted key, because a handshake needs the matching private half, so a
|
|
29
|
+
* substituted key produces a refusal rather than a silent interception. §8 of
|
|
30
|
+
* `docs/P2P-CRYPTO.md` names the cheap improvement — a key-change notice — and leaves it for
|
|
31
|
+
* stage 2; until then a changed key is visible as `P2P_CRYPTO_BAD_MAC` rather than as silence.
|
|
23
32
|
*/
|
|
24
33
|
|
|
25
34
|
/** Most candidates a single machine may announce. Enough for several interfaces, not for a scan. */
|
|
@@ -31,6 +40,33 @@ export const DEFAULT_TTL_MS = 60_000;
|
|
|
31
40
|
/** Upper bound a caller may request, so an announcement cannot be pinned indefinitely. */
|
|
32
41
|
export const MAX_TTL_MS = 10 * 60_000;
|
|
33
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Bytes in an X25519 public key (v0.6.0).
|
|
45
|
+
*
|
|
46
|
+
* The key itself is base64, so this is what the validator decodes to. Kept as a literal rather than
|
|
47
|
+
* imported from `src/agent/crypto.mjs` for the same reason `P2P_MODES` is duplicated in
|
|
48
|
+
* `state.mjs`: the relay is a standalone process that must not depend on an agent module, and the
|
|
49
|
+
* value is a wire fact two independently deployed ends have to agree on.
|
|
50
|
+
*/
|
|
51
|
+
export const STATIC_KEY_BYTES = 32;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Is `value` a usable static public key?
|
|
55
|
+
*
|
|
56
|
+
* Strict about the encoding, because a key that "almost" decodes is the input to a MAC that fails for
|
|
57
|
+
* a reason indistinguishable from an attack. `Buffer.from(x, 'base64')` ignores characters it does
|
|
58
|
+
* not recognise, so the decode is only trusted after re-encoding it and getting the same string back.
|
|
59
|
+
*
|
|
60
|
+
* @param {unknown} value
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function isStaticKey(value) {
|
|
64
|
+
if (typeof value !== 'string' || value === '') return false;
|
|
65
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return false;
|
|
66
|
+
const decoded = Buffer.from(value, 'base64');
|
|
67
|
+
return decoded.length === STATIC_KEY_BYTES && decoded.toString('base64') === value;
|
|
68
|
+
}
|
|
69
|
+
|
|
34
70
|
const IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
35
71
|
|
|
36
72
|
/**
|
|
@@ -112,10 +148,26 @@ export class PeerRegistry {
|
|
|
112
148
|
const ttlMs = Math.max(1000, Math.min(requestedTtl, MAX_TTL_MS));
|
|
113
149
|
const now = this.nowMs();
|
|
114
150
|
|
|
151
|
+
// v0.6.0: the machine's static public key, when it sent one. Validated here because this is the
|
|
152
|
+
// one place a peer supplies a value another peer will use to authenticate it — a malformed key
|
|
153
|
+
// stored and served would make a handshake fail with a MAC error that looks exactly like an
|
|
154
|
+
// attack. Absent (a v0.5.x machine, or one with no key) stays absent rather than becoming `null`:
|
|
155
|
+
// "this machine has no key" and "this machine's key is unknown" must not be the same value.
|
|
156
|
+
let staticKey = null;
|
|
157
|
+
if (body.static_key !== undefined && body.static_key !== null) {
|
|
158
|
+
if (!isStaticKey(body.static_key)) {
|
|
159
|
+
throw Object.assign(new Error('static_key is not a 32-byte base64 X25519 public key'), {
|
|
160
|
+
code: 'BAD_STATIC_KEY',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
staticKey = body.static_key;
|
|
164
|
+
}
|
|
165
|
+
|
|
115
166
|
const entry = {
|
|
116
167
|
machine_id: machineId,
|
|
117
168
|
candidates,
|
|
118
169
|
nat: { mapping: typeof body.nat?.mapping === 'string' ? body.nat.mapping : null },
|
|
170
|
+
static_key: staticKey,
|
|
119
171
|
announced_at_ms: now,
|
|
120
172
|
expires_at_ms: now + ttlMs,
|
|
121
173
|
};
|
|
@@ -124,6 +176,7 @@ export class PeerRegistry {
|
|
|
124
176
|
return {
|
|
125
177
|
machine_id: machineId,
|
|
126
178
|
candidates,
|
|
179
|
+
...(staticKey === null ? {} : { static_key: staticKey }),
|
|
127
180
|
expires_at: new Date(entry.expires_at_ms).toISOString(),
|
|
128
181
|
ttl_ms: ttlMs,
|
|
129
182
|
};
|
|
@@ -148,6 +201,9 @@ export class PeerRegistry {
|
|
|
148
201
|
machine_id: entry.machine_id,
|
|
149
202
|
candidates: entry.candidates.map((c) => ({ ...c })),
|
|
150
203
|
nat: { ...entry.nat },
|
|
204
|
+
...(entry.static_key === null || entry.static_key === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { static_key: entry.static_key }),
|
|
151
207
|
announced_at: new Date(entry.announced_at_ms).toISOString(),
|
|
152
208
|
expires_at: new Date(entry.expires_at_ms).toISOString(),
|
|
153
209
|
};
|