summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/client/keys.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 00:56:04 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 00:56:04 +0200
commit6f68bb6c1970e26de1f66c073ea5d7fecd21840d (patch)
tree859c74db5819f1d49046e71a53e12afe5eb4193a /packages/meshbay-cli/src/meshbay_cli/client/keys.py
parent16d92c96bde3a65fc54a93b4662df30cafc831e4 (diff)
downloadmeshbay-6f68bb6c1970e26de1f66c073ea5d7fecd21840d.tar.gz
feat(cli): invitations, join, decline, leave, devices, browser access
join accepts an invitation or redeems a link pasted as sent, then has the node admit a key pair made here, with the invitation's code (a link's code goes only to the node it names; one for a node that is away is kept for the next connection). decline, leave and devices revoke ask first, and refuse without --yes where there is no terminal. devices approve countersigns the one waiting request a code matches, none when several do. browser-access is the application's setting, kept per machine and never enabled from the hub's mirror: on leaves a copy sealed under M on each node, off withdraws it, unset touches nothing. A key pair made for a first contact is now kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/client/keys.py')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/client/keys.py33
1 files changed, 31 insertions, 2 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/client/keys.py b/packages/meshbay-cli/src/meshbay_cli/client/keys.py
index faca01e..ecf44a5 100644
--- a/packages/meshbay-cli/src/meshbay_cli/client/keys.py
+++ b/packages/meshbay-cli/src/meshbay_cli/client/keys.py
@@ -10,6 +10,7 @@ look like an error — it looks like an account nobody can open.
import base64
import hashlib
import json
+import os
from dataclasses import dataclass
from argon2.low_level import Type, hash_secret_raw
@@ -77,8 +78,9 @@ def open_bundle(bundle_b64: str, *, master: bytes | None, legacy: bytes | None,
`MBK3` opens under the node's key derived from `M`, with the account and the
node as associated data. `MBK2` opens under `A` alone; it is read so a member
- is not locked out of a node still holding one, and never rewritten here —
- the application or a browser replaces it. Anything older is refused by name.
+ is not locked out of a node still holding one, and never written — an `MBK3`
+ replaces it where this machine leaves bundles (`browser-access on`). Anything
+ older is refused by name.
"""
try:
raw = unb64(bundle_b64)
@@ -106,6 +108,33 @@ def open_bundle(bundle_b64: str, *, master: bytes | None, legacy: bytes | None,
return json.loads(plain)
+def seal_bundle(keys: dict, *, master: bytes, pepper_version: int, user_id: str,
+ node_pk: str) -> str:
+ """`{skEd, skX}` sealed as `MBK3`, as `keyring.js` `seal` writes it: the magic,
+ the pepper version, a 96-bit nonce, then AES-GCM under the node's bundle key
+ with the account and the node as associated data."""
+ nonce = os.urandom(12)
+ aad = f"meshbay:bundle:v3|{user_id}|{node_pk}".encode()
+ plain = json.dumps({"skEd": keys["skEd"], "skX": keys["skX"]}).encode()
+ ct = AESGCM(node_bundle_key(master, node_pk)).encrypt(nonce, plain, aad)
+ return b64(BUNDLE_MAGIC + bytes([pepper_version & 0xFF]) + nonce + ct)
+
+
+def bundle_is_current(bundle_b64: str, *, master: bytes, user_id: str,
+ node_pk: str) -> bool:
+ """Whether a bundle is `MBK3` and opens under this `M`: one sealed under an
+ earlier passphrase, or `MBK2`, is to be sealed again."""
+ try:
+ raw = unb64(bundle_b64)
+ if raw[:4] != BUNDLE_MAGIC:
+ return False
+ aad = f"meshbay:bundle:v3|{user_id}|{node_pk}".encode()
+ AESGCM(node_bundle_key(master, node_pk)).decrypt(raw[5:17], raw[17:], aad)
+ return True
+ except Exception:
+ return False
+
+
@dataclass
class Identity:
"""One account's keys on one node."""