diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-28 21:14:10 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-28 21:14:10 +0200 |
| commit | a421a03d2be16670dc8d9076d26f4a7eac669986 (patch) | |
| tree | ce8a0053497278086696a8f802e97605f80903f4 /packages/meshbay-hub/src/meshbay_hub/static/crypto.js | |
| parent | f63104b82da24ff3f406c53346300bd50788796f (diff) | |
| download | meshbay-a421a03d2be16670dc8d9076d26f4a7eac669986.tar.gz | |
fix: bound pending admin challenges and sign every value an op acts on
Any member could make a node hold unbounded challenge requests; a
connection now keeps at most 8, 64 KiB each. root_add, group_attach,
invite_create and tmdb_config signed less than they did; their subjects
are now canonical JSON of every value (the TMDB token by SHA-256).
MNP 5.0, floor kept at 4.0.
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 | 40 |
1 files changed, 39 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js index b74732c..0ca8ee5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js @@ -307,6 +307,43 @@ function b64encode(bytes) { return btoa(String.fromCharCode(...bytes)); } +// ── Admin operation subjects ──────────────────────────────────────────────── +// Mirrors meshbay_common/adminop.py. The subject is what the signature covers of +// a request, so an operation whose effect is several values names them all. +// Canonical JSON — sorted keys, no whitespace — so both sides build the same +// bytes, and `null`, `""` and a value stay distinct. + +function adminSubject(fields) { + const sorted = {}; + for (const k of Object.keys(fields).sort()) sorted[k] = fields[k]; + return JSON.stringify(sorted); +} + +// A secret named without being written: null (unchanged) and '' (clear) as +// themselves, anything else as its SHA-256. +async function secretDigest(value) { + if (!value) return value; + const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); + return 'sha256:' + Array.from(new Uint8Array(d)) + .map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +function rootAddSubject(path, name, kind, writable, removable) { + return adminSubject({ path, name, kind, writable, removable }); +} + +function groupAttachSubject(name, sharedDir, writable) { + return adminSubject({ name, shared_dir: sharedDir, writable }); +} + +function inviteCreateSubject(userId, username) { + return adminSubject({ user_id: userId, username }); +} + +async function tmdbConfigSubject(token, language) { + return adminSubject({ token: await secretDigest(token), language }); +} + // ── Admin operation transcript ─────────────────────────────────────────────── // Mirrors meshbay_common/adminop.py::admin_transcript(). Both sides build these // bytes independently; they are never taken off the wire. @@ -535,7 +572,8 @@ window.MeshBayCrypto = { importGEK, deriveChunkKey, decryptChunkBin, openGroup, sealGroup, unwrapGEK, b64encode, b64decode, - adminTranscript, handshakeTranscript, handshakeProof, webrtcBinding, + adminTranscript, adminSubject, rootAddSubject, groupAttachSubject, + inviteCreateSubject, tmdbConfigSubject, handshakeTranscript, handshakeProof, webrtcBinding, challengeTranscript, joinTranscript, verifyNodeSignature, constantTimeEqual, deviceRequestTranscript, deviceAddTranscript, deviceHelloTranscript, deviceCodeHash, |