diff options
Diffstat (limited to 'packages/meshbay-hub/src')
5 files changed, 99 insertions, 32 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, diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js index d5226fc..1a5a4c0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js @@ -32,9 +32,10 @@ extendTransport(class { const ts = Math.floor(Date.now() / 1000); // group_id is empty: operator authority is node-wide, not per group. - const transcript = C.joinTranscript( - this.nodePk, '', userId, pkEdB64, pkXB64, this._nonceNode, ts); - const sig = await this._identity.sign(transcript); + const sig = await this._identity.signAs('join', { + nodePk: this.nodePk, groupId: '', userId, + nonceNode: C.b64encode(this._nonceNode), ts, + }); const resp = await this._sendAndWait({ type: 'join_request', @@ -107,11 +108,12 @@ extendTransport(class { } if (!signFn) throw new Error('Admin challenge received but no signing key available'); - const transcript = window.MeshBayCrypto.adminTranscript( - challenge.op, challenge.node_pk, challenge.group_id, - challenge.subject, challenge.nonce, challenge.ts); - - const signature = await signFn(transcript); + // The fields, not the bytes: whatever holds the key builds the transcript + // from them (crypto.js, transcriptFor). + const signature = await signFn({ + op: challenge.op, nodePk: challenge.node_pk, groupId: challenge.group_id, + subject: challenge.subject, nonce: challenge.nonce, ts: challenge.ts, + }); console.log('[MeshBay] _authorizeAdminOp: signed', challenge.op, 'op_id=', challenge.op_id, '— sending admin_response'); const ack = await this._sendAndWait({ diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js index f0604ce..e49eb4c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js @@ -172,8 +172,11 @@ extendTransport(class { const { nonce, ct } = await C.sealChat( epochKey, gid, epoch, this.devicePk, plaintext); const device = C.b64decode(this.devicePk); - const sig = C.b64decode(await this._identity.sign( - C.chatSigningTranscript(gid, epoch, device, nonce, ct))); + // The transcript names the signing device as this identity's own key, + // which is what `devicePk` is once the node has been told (device_hello). + const sig = C.b64decode(await this._identity.signAs('chat', { + groupId: gid, epoch, nonce: C.b64encode(nonce), ct: C.b64encode(ct), + })); const msg = await this._sendAndWait({ type: 'chat_msg', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js index 1cb248a..1c6fc78 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js @@ -105,9 +105,10 @@ extendTransport(class { const { pkEdB64, pkXB64 } = this._identity; const ts = Math.floor(Date.now() / 1000); - const transcript = C.joinTranscript( - this.nodePk, groupId || '', userId, pkEdB64, pkXB64, this._nonceNode, ts); - const sig = await this._identity.sign(transcript); + const sig = await this._identity.signAs('join', { + nodePk: this.nodePk, groupId: groupId || '', userId, + nonceNode: C.b64encode(this._nonceNode), ts, + }); const resp = await this._sendAndWait({ type: 'join_request', @@ -171,9 +172,10 @@ extendTransport(class { const codeHash = await C.deviceCodeHash( C.normalizeCode(code), pkEdB64, pkXB64); const ts = Math.floor(Date.now() / 1000); - const transcript = C.deviceRequestTranscript( - this.nodePk, userId, pkEdB64, pkXB64, codeHash, this._nonceNode, ts); - const sig = await this._identity.sign(transcript); + const sig = await this._identity.signAs('device_request', { + nodePk: this.nodePk, userId, codeHash, + nonceNode: C.b64encode(this._nonceNode), ts, + }); const resp = await this._sendAndWait({ type: 'device_add_request', v: '0.1', @@ -225,9 +227,10 @@ extendTransport(class { async _countersign(userId, codeHash, pkEdB64, pkXB64) { const C = window.MeshBayCrypto; const ts = Math.floor(Date.now() / 1000); - const transcript = C.deviceAddTranscript( - this.nodePk, userId, pkEdB64, pkXB64, this._nonceNode, ts); - const sig = await this._identity.sign(transcript); + const sig = await this._identity.signAs('device_add', { + nodePk: this.nodePk, userId, pkEd: pkEdB64, pkX: pkXB64, + nonceNode: C.b64encode(this._nonceNode), ts, + }); const resp = await this._sendAndWait({ type: 'device_add', v: '0.1', pk_ed25519: pkEdB64, pk_x25519: pkXB64, code_hash: codeHash, ts, sig, @@ -246,9 +249,10 @@ extendTransport(class { async revokeDevice(userId, pkEdB64, pkXB64) { const C = window.MeshBayCrypto; const ts = Math.floor(Date.now() / 1000); - const transcript = C.deviceAddTranscript( - this.nodePk, userId, pkEdB64, pkXB64, this._nonceNode, ts); - const sig = await this._identity.sign(transcript); + const sig = await this._identity.signAs('device_revoke', { + nodePk: this.nodePk, userId, pkEd: pkEdB64, + nonceNode: C.b64encode(this._nonceNode), ts, + }); const resp = await this._sendAndWait({ type: 'device_revoke', v: '0.1', pk_ed25519: pkEdB64, ts, sig, }); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 17a8e5d..9b86921 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -52,10 +52,13 @@ async function _pkEdFromSk(skPkcs8B64) { */ async function _identityFromKeys(skEdB64, skXB64) { const skXRaw = Uint8Array.from(atob(skXB64), c => c.charCodeAt(0)); + const own = { pkEdB64: await _pkEdFromSk(skEdB64), pkXB64: await _pkFromSk(skXB64) }; return { - pkEdB64: await _pkEdFromSk(skEdB64), - pkXB64: await _pkFromSk(skXB64), - sign: (bytes) => window.MeshBayKeys.signBytes(skEdB64, bytes), + ...own, + // By kind and fields, never over bytes a caller chose (crypto.js, + // transcriptFor) — the same contract the desktop's main process keeps. + signAs: (kind, fields) => window.MeshBayKeys.signBytes( + skEdB64, window.MeshBayCrypto.transcriptFor(kind, fields, own)), async shared(peerPkRaw) { const sk = await crypto.subtle.importKey( 'pkcs8', skXRaw, { name: 'X25519' }, false, ['deriveBits']); @@ -77,7 +80,8 @@ function _nativeIdentityHandle(keys, userId, nodePk, pub) { pkXB64: pub.pkXB64, sealedWith: pub.sealedWith || null, native: true, - sign: (bytes) => keys.sign(userId, nodePk, b64(bytes)), + // The main process builds the bytes from the kind and the fields. + signAs: (kind, fields) => keys.sign(userId, nodePk, kind, fields), async shared(peerPkRaw) { const out = await keys.shared(userId, nodePk, b64(peerPkRaw)); return Uint8Array.from(atob(out), c => c.charCodeAt(0)).buffer; @@ -672,10 +676,10 @@ class MeshBayTransport { set onNeedToken(fn) { this._onNeedToken = fn; } get identity() { return this._identity; } - /** Signs with this node's identity, or null when there is none yet. */ + /** Signs an admin operation with this node's identity, or null when there is none yet. */ get signFn() { const id = this._identity; - return id ? (transcript) => id.sign(transcript) : null; + return id ? (fields) => id.signAs('admin', fields) : null; } /** Set on a first join: the identity created for this node, still to be left with it. */ @@ -1343,10 +1347,10 @@ class MeshBayTransport { // substitution this mechanism exists to close. const pkEdB64 = this._identity.pkEdB64; const ts = Math.floor(Date.now() / 1000); - const transcript = C.deviceHelloTranscript( - this.nodePk, this._groupId || '', this._userId, pkEdB64, - this._nonceNode, ts); - const sig = await this._identity.sign(transcript); + const sig = await this._identity.signAs('device_hello', { + nodePk: this.nodePk, groupId: this._groupId || '', userId: this._userId, + nonceNode: C.b64encode(this._nonceNode), ts, + }); const resp = await this._sendAndWait({ type: 'device_hello', v: '2.0', pk_ed25519: pkEdB64, ts, sig, |