diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 21:04:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 21:04:39 +0200 |
| commit | 0378e8e0912a1a7e6cea4424e69d524e7afecbf8 (patch) | |
| tree | 4ae94e32d6638b4c2cc1ae4f74cbe5d00c940636 /packages/meshbay-hub/src/meshbay_hub/static/crypto.js | |
| parent | 0ed56d3a1b4f71cf622d3e27edc87a15ef33c185 (diff) | |
| download | meshbay-0378e8e0912a1a7e6cea4424e69d524e7afecbf8.tar.gz | |
fix: an identity signs a named kind, and a device approval answers a request
The desktop main process builds every transcript itself from fields
(transcripts.js) and signs no raw bytes; the page's identity has the same
contract (crypto.js transcriptFor). The keyring seals no bundle while browser
access is off. On the node, device_add must redeem a pending request filed by
the same keys, and device_revoke is signed under its own prefix
(meshbay:device_revoke:v1), so a retirement signature admits nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/crypto.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/crypto.js | 54 |
1 files changed, 54 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js index 27e97d3..c239cc8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js @@ -451,6 +451,7 @@ const JOIN_PREFIX = new TextEncoder().encode('meshbay:join:v1'); const DEVICE_REQ_PREFIX = new TextEncoder().encode('meshbay:device_req:v1'); const DEVICE_ADD_PREFIX = new TextEncoder().encode('meshbay:device_add:v1'); const DEVICE_HELLO_PREFIX = new TextEncoder().encode('meshbay:device_hello:v1'); +const DEVICE_REVOKE_PREFIX = new TextEncoder().encode('meshbay:device_revoke:v1'); function joinTranscript(nodePkB64, groupId, userId, pkEdB64, pkXB64, nonceNode, ts) { const enc = new TextEncoder(); @@ -503,6 +504,22 @@ function deviceAddTranscript(nodePkB64, userId, pkEdB64, pkXB64, nonceNode, ts) } /** + * Retiring one of the account's devices. A prefix of its own: were it the + * admission transcript, a signature given to retire a key would admit it. + */ +function deviceRevokeTranscript(nodePkB64, userId, pkEdB64, nonceNode, ts) { + const enc = new TextEncoder(); + const body = _lenPrefixed([ + enc.encode(nodePkB64), enc.encode(userId), enc.encode(pkEdB64), + nonceNode, enc.encode(String(ts)), + ]); + const out = new Uint8Array(DEVICE_REVOKE_PREFIX.length + body.length); + out.set(DEVICE_REVOKE_PREFIX, 0); + out.set(body, DEVICE_REVOKE_PREFIX.length); + return out; +} + +/** * "Which of this account's devices am I?", mirroring * `meshbay_common/device.py:device_hello_transcript`. * @@ -560,6 +577,42 @@ function constantTimeEqual(a, b) { return diff === 0; } +/** + * What an identity signs, by kind. The only way anything here gets signed with + * an identity: a caller names what it is signing and gives the fields, and the + * bytes are built from them — with the identity's own public keys wherever a + * transcript names them. The desktop application builds the same bytes in its + * main process (meshbay-client/src/transcripts.js) and signs nothing else, + * which is what makes a signature from it mean what its kind says. + * + * Bytes cross as base64: `nonceNode`, `nonce`, `ct`. + */ +function transcriptFor(kind, f, own) { + const nonceNode = () => b64decode(f.nonceNode); + switch (kind) { + case 'join': + return joinTranscript(f.nodePk, f.groupId || '', f.userId, own.pkEdB64, own.pkXB64, + nonceNode(), f.ts); + case 'device_hello': + return deviceHelloTranscript(f.nodePk, f.groupId || '', f.userId, own.pkEdB64, + nonceNode(), f.ts); + case 'device_request': + return deviceRequestTranscript(f.nodePk, f.userId, own.pkEdB64, own.pkXB64, + f.codeHash, nonceNode(), f.ts); + case 'device_add': + return deviceAddTranscript(f.nodePk, f.userId, f.pkEd, f.pkX, nonceNode(), f.ts); + case 'device_revoke': + return deviceRevokeTranscript(f.nodePk, f.userId, f.pkEd, nonceNode(), f.ts); + case 'chat': + return chatSigningTranscript(f.groupId || '', f.epoch, b64decode(own.pkEdB64), + b64decode(f.nonce), b64decode(f.ct)); + case 'admin': + return adminTranscript(f.op, f.nodePk, f.groupId || '', f.subject, f.nonce, f.ts); + default: + throw new Error(`Refused: nothing is signed as "${kind}"`); + } +} + /** Verify the node's Ed25519 signature over the handshake transcript (C3). */ async function verifyNodeSignature(nodePkB64, sigB64, transcript) { const raw = b64decode(nodePkB64); @@ -576,6 +629,7 @@ window.MeshBayCrypto = { inviteCreateSubject, tmdbConfigSubject, handshakeTranscript, handshakeProof, webrtcBinding, challengeTranscript, joinTranscript, verifyNodeSignature, constantTimeEqual, deviceRequestTranscript, deviceAddTranscript, deviceHelloTranscript, + deviceRevokeTranscript, transcriptFor, deviceCodeHash, sealChat, openChat, chatSigningTranscript, verifyChatSignature, normalizeCode, |