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
|
"""
This account's devices on one node: what the application's "Your devices on this
node" does (docs/MESHBAY_DESIGN.md §3.3).
Every message here is sent on an authenticated session and signed by this
machine's key on the node, over the connection's nonce: the node admits or
retires a key only on the word of a device it already pinned.
"""
import time
from meshbay_common import MNP_VERSION
from meshbay_common.device import (
device_add_transcript,
device_code_hash,
device_revoke_transcript,
)
from ..errors import NOT_FOUND, USAGE, CliError
from .account import Account
from .keys import b64
from .session import GroupSession
def normalize_code(code: str) -> str:
"""
A code as typed, folded onto what was generated — `crypto.js` `normalizeCode`
and the node's `roster.normalize_code`: case, dashes and spaces do not count,
and the letters left out of the alphabet read as the digits they resemble.
"""
fold = {"I": "1", "L": "1", "O": "0", "U": "V"}
return "".join(fold.get(c, c) for c in code.upper() if c not in "- \t")
async def list_devices(session: GroupSession) -> dict:
"""`{devices: [{pk_ed25519, label, pinned_at, pinned_via, is_this_one}], pending}`."""
r = await session.ch.request({"type": "device_list", "v": MNP_VERSION},
"device_list_result")
return {"devices": r.get("devices") or [], "pending": int(r.get("pending") or 0)}
async def approve(session: GroupSession, account: Account, code: str) -> str:
"""
Countersign the one pending request this code names; its Ed25519 key.
The code never leaves this machine: each pending request is hashed with it
and must match the hash its device filed. None, or more than one, approves
nothing — filing needs only the account's token, so somebody holding the
passphrase who saw the code could file their own keys under it.
"""
raw = normalize_code(code)
if len(raw) != 8:
raise CliError(USAGE, "A linking code has 8 characters, as XXXX-XXXX.")
listed = await session.ch.request({"type": "device_lookup", "v": MNP_VERSION},
"device_lookup_result")
matches = [r for r in listed.get("requests") or []
if device_code_hash(raw, r["pk_ed25519"], r["pk_x25519"]) == r["code_hash"]]
if not matches:
raise CliError(NOT_FOUND, "No device is waiting with that code on this node.")
if len(matches) > 1:
raise CliError(NOT_FOUND, "More than one device is waiting with that code, so "
"none was approved. Get a new code on the device you "
"are linking.")
match = matches[0]
ts = int(time.time())
sig = session.identity.sk_ed.sign(device_add_transcript(
node_pk_b64=session.node_pk, user_id=account.user_id,
pk_ed25519_b64=match["pk_ed25519"], pk_x25519_b64=match["pk_x25519"],
nonce_node=session.nonce_node, ts=ts))
await session.ch.request({"type": "device_add", "v": MNP_VERSION,
"pk_ed25519": match["pk_ed25519"],
"pk_x25519": match["pk_x25519"],
"code_hash": match["code_hash"], "ts": ts, "sig": b64(sig)},
"device_add_ack")
return match["pk_ed25519"]
def find_device(devices: list[dict], prefix: str) -> dict:
"""The one device whose key starts with `prefix` (8 characters at least)."""
if len(prefix) < 8:
raise CliError(USAGE, "Name the device by the first 8 characters of its key "
"at least, as `devices list` shows it.")
found = [d for d in devices if d["pk_ed25519"].startswith(prefix)]
if not found:
raise CliError(NOT_FOUND, f"No device of yours on this node has a key starting "
f"with {prefix}.")
if len(found) > 1:
raise CliError(USAGE, f"Several devices have a key starting with {prefix}; "
"give more of it.")
return found[0]
async def revoke(session: GroupSession, account: Account, pk_ed: str) -> None:
"""Retire a device of this account. The node refuses the last one."""
ts = int(time.time())
sig = session.identity.sk_ed.sign(device_revoke_transcript(
node_pk_b64=session.node_pk, user_id=account.user_id, pk_ed25519_b64=pk_ed,
nonce_node=session.nonce_node, ts=ts))
await session.ch.request({"type": "device_revoke", "v": MNP_VERSION,
"pk_ed25519": pk_ed, "ts": ts, "sig": b64(sig)},
"device_add_ack")
|