diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/crypto.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/crypto.js | 75 |
1 files changed, 75 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 69b8c7a..d2e19b7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js @@ -109,6 +109,80 @@ async function decryptChunkBin(gek, fileHashHex, chunkIndex, nonce, ct) { return new Uint8Array(plaintext); } +// ── Sealing a payload under the group key ──────────────────────────────────── +// +// Mirrors meshbay_common/groupbox.py. `index_sync`, `index_delta` and the +// `handshake_ack` config payload travel sealed under a GEK-derived subkey; the +// routing fields (type, v, group_id) and the ack's own authentication (node_pk, +// proof, sig) stay in clear, because a receiver must route, version-check and +// *authenticate* before it would trust a decryption. +// +// These take and return BYTES, not objects, and that is not an oversight: +// msgpack here is a minimal hand-written codec private to transport.js, exported +// to nothing (both files are classic scripts on globals, not ES modules). Making +// this layer take objects would mean duplicating that codec or reaching across a +// boundary that does not exist — both worse than one extra line at the call site. + +const GROUPBOX_INFO = { + index: new TextEncoder().encode('meshbay:index:v1'), + ack: new TextEncoder().encode('meshbay:ack:v1'), +}; + +/** + * Derive the AES-256-GCM subkey for one purpose. + * `salt: new Uint8Array(0)` matches Python's `salt=None` — RFC 5869 extracts with + * a zero key either way, which is what deriveChunkKey above already relies on. + */ +async function groupKey(gek, purpose, usages) { + const info = GROUPBOX_INFO[purpose]; + if (!info) throw new Error(`unknown groupbox purpose: ${purpose}`); + const gekKey = gek instanceof CryptoKey + ? gek + : await crypto.subtle.importKey('raw', gek, 'HKDF', false, ['deriveKey']); + return crypto.subtle.deriveKey( + { name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(0), info }, + gekKey, + { name: 'AES-GCM', length: 256 }, + false, + usages, + ); +} + +/** What the ciphertext is bound to: this message type, in this group. */ +function groupAad(msgType, groupId) { + return new TextEncoder().encode(`${msgType}|${groupId}`); +} + +/** + * Open a sealed payload. Throws on anything that does not open — a caller must + * never turn that into an empty index or an empty app list (groupbox.py's + * `unseal` says why at length). + * @returns {Promise<Uint8Array>} the msgpack bytes of the payload + */ +async function openGroup(gek, purpose, msgType, groupId, msg) { + if (!msg || !msg.nonce || !msg.ct) { + throw new Error(`${msgType}: not a sealed message`); + } + const key = await groupKey(gek, purpose, ['decrypt']); + const plain = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: msg.nonce, additionalData: groupAad(msgType, groupId) }, + key, msg.ct); + return new Uint8Array(plain); +} + +/** + * Seal payload bytes. Returns the `{nonce, ct}` pair to merge into a message. + */ +async function sealGroup(gek, purpose, msgType, groupId, plaintextBytes) { + const key = await groupKey(gek, purpose, ['encrypt']); + const nonce = crypto.getRandomValues(new Uint8Array(12)); + const ct = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: nonce, additionalData: groupAad(msgType, groupId) }, + key, plaintextBytes); + return { nonce, ct: new Uint8Array(ct) }; +} + + // ── GEK generation + ECIES wrapping ────────────────────────────────────────── function generateGEK() { @@ -372,6 +446,7 @@ async function verifyNodeSignature(nodePkB64, sigB64, transcript) { // Export for use in app.js window.MeshBayCrypto = { importGEK, deriveChunkKey, decryptChunkBin, + openGroup, sealGroup, generateGEK, wrapGEK, unwrapGEK, encryptChunk, b64encode, b64decode, adminTranscript, handshakeTranscript, handshakeProof, webrtcBinding, joinTranscript, verifyNodeSignature, constantTimeEqual, |