1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
"""
Device linking transcripts (MNP).
Identity keys are per node, so a person who uses a browser and a desktop client
holds two keys on the same node. The node has to admit the second without
asking an operator for a code every time — and without letting the hub, or
itself, decide which key belongs to whom.
The authority is **a key the node already pinned**. An existing device
countersigns the new one, which the hub cannot do: it has stored no user keys
since 2026-08-14, so device linking adds nothing a hub can reach.
The approval is bound by a **one-time code the new device generates and
displays**, hashed together with its own keys:
code_hash = sha256(code ‖ pk_ed25519 ‖ pk_x25519)
That binding is the part worth understanding. The approver types the code, is
handed candidate keys, and recomputes the hash — so a node that returned
different keys produces no match and the client refuses before signing. Nothing
here rests on a human comparing digits, which is the ritual Phase 12.1 dropped
as "correct, unusable as the default"; reintroducing it through the back door
would be the same mistake.
Fields are length-prefixed and domain-separated, per L4 — the same rule as
`handshake.py`, `join.py` and `adminop.py`. `nonce_node` is the handshake nonce
of the connection carrying the message, so neither signature can be lifted onto
another connection, and `node_pk` binds an authorization to one node.
See `docs/desktop-client-v1.md` §4.
"""
from __future__ import annotations
import hashlib
DEVICE_REQUEST_PREFIX = b"meshbay:device_req:v1"
DEVICE_ADD_PREFIX = b"meshbay:device_add:v1"
# Same as the join and admin transcripts: interactive exchanges that complete in
# milliseconds, so anything older is a replay.
DEVICE_TTL = 120 # seconds
# 40 bits, single use, and bound to the keys it was generated beside. Guessing is
# bounded the same way an invitation is — a handful of attempts per connection
# and a node-wide lockout.
DEVICE_CODE_BITS = 40
def device_code_hash(code: str, pk_ed25519_b64: str, pk_x25519_b64: str) -> str:
"""
The lookup key for a pending device request.
Both keys go in, so the hash identifies *this device asking with this code*
rather than *this code*. A node cannot answer an approver with a substituted
key: the approver recomputes this from what it typed and what it was given,
and looks the request up by the result.
Normalized the same way pairing codes are (`roster.normalize_code`), which
is applied by the caller — this function hashes exactly what it is given, so
both ends have to agree on the normalized form and neither can quietly
differ.
"""
payload = "\x1f".join((code, pk_ed25519_b64, pk_x25519_b64))
return hashlib.sha256(payload.encode()).hexdigest()
def _pack(prefix: bytes, fields: list[bytes]) -> bytes:
out = bytearray(prefix)
for field in fields:
out += len(field).to_bytes(4, "big")
out += field
return bytes(out)
def device_request_transcript(
node_pk_b64: str,
user_id: str,
pk_ed25519_b64: str,
pk_x25519_b64: str,
code_hash: str,
nonce_node: bytes,
ts: int,
) -> bytes:
"""
Signed by the **new** device, proving it holds the keys it is presenting.
Proof of possession only: this establishes nothing about whose account the
keys belong to. That is what the countersignature below is for.
"""
return _pack(DEVICE_REQUEST_PREFIX, [
node_pk_b64.encode(),
user_id.encode(),
pk_ed25519_b64.encode(),
pk_x25519_b64.encode(),
code_hash.encode(),
nonce_node,
str(ts).encode(),
])
def device_add_transcript(
node_pk_b64: str,
user_id: str,
pk_ed25519_b64: str,
pk_x25519_b64: str,
nonce_node: bytes,
ts: int,
) -> bytes:
"""
Signed by an **already-pinned** device, admitting the new keys.
Deliberately does not include the code: the code is a bearer secret used to
find the request, never signed and never echoed. What is signed is the pair
of keys being admitted, so a signature collected for one device cannot admit
another.
"""
return _pack(DEVICE_ADD_PREFIX, [
node_pk_b64.encode(),
user_id.encode(),
pk_ed25519_b64.encode(),
pk_x25519_b64.encode(),
nonce_node,
str(ts).encode(),
])
|