aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-11 14:55:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-11 14:55:52 +0200
commitcc90dc943fcd0e7bbf52674fb3f95ff097026f4a (patch)
tree6315a5e966c027c50b92d59e60c4b506a418143e /packages/meshbay-hub/src/meshbay_hub/static/crypto.js
parentedde9e441fb6b84e9d56215d6e2a8d9338b8f962 (diff)
downloadmeshbay-cc90dc943fcd0e7bbf52674fb3f95ff097026f4a.tar.gz
feat: Phase 10b — Self-service UI (group create/join, upload, IndexedDB, search)
Six self-service features for the web SPA: - Group creation UI with GEK auto-generation (AES-256-GCM ECIES) - Member management + invite by username (GEK wrapping for invitee) - Open group self-join flow (POST /v1/groups/{id}/join) - File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging) - IndexedDB caching of group file indexes (instant display on revisit) - Cross-group file search (SearchPage, pure client-side on cached indexes) 11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap. Co-Authored-By: Claude Opus 4.6 <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.js79
1 files changed, 77 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
index ee442fb..eb96eef 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
@@ -56,7 +56,7 @@ async function deriveChunkKey(gek, fileHashHex, chunkIndex) {
gek,
{ name: 'AES-GCM', length: 256 },
false,
- ['decrypt'],
+ ['encrypt', 'decrypt'],
);
}
@@ -158,5 +158,80 @@ async function decryptChunkBin(gek, fileHashHex, chunkIndex, nonce, ct) {
return new Uint8Array(plaintext);
}
+// ── GEK generation + ECIES wrapping ──────────────────────────────────────────
+
+function generateGEK() {
+ return crypto.getRandomValues(new Uint8Array(32));
+}
+
+async function wrapGEK(gek, pkXRaw) {
+ const skEph = await crypto.subtle.generateKey({ name: 'X25519' }, true, ['deriveBits']);
+ const pkEphRaw = new Uint8Array(await crypto.subtle.exportKey('raw', skEph.publicKey));
+
+ const pkRecip = await crypto.subtle.importKey('raw', pkXRaw, { name: 'X25519' }, false, []);
+ const sharedBits = await crypto.subtle.deriveBits(
+ { name: 'X25519', public: pkRecip }, skEph.privateKey, 256);
+
+ const sharedKey = await crypto.subtle.importKey(
+ 'raw', sharedBits, 'HKDF', false, ['deriveKey']);
+ const wrapKey = await crypto.subtle.deriveKey(
+ { name: 'HKDF', hash: 'SHA-256', salt: pkEphRaw,
+ info: new TextEncoder().encode('meshbay:gek_wrap:v1:aes') },
+ sharedKey,
+ { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
+
+ const nonce = crypto.getRandomValues(new Uint8Array(12));
+ const ct = await crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv: nonce, additionalData: pkXRaw }, wrapKey, gek);
+
+ return {
+ pk_eph_b64: btoa(String.fromCharCode(...pkEphRaw)),
+ nonce_b64: btoa(String.fromCharCode(...nonce)),
+ wrapped_b64: btoa(String.fromCharCode(...new Uint8Array(ct))),
+ };
+}
+
+async function unwrapGEK(bundle, skXPkcs8, pkXRaw) {
+ const pkEphRaw = b64decode(bundle.pk_eph_b64);
+ const nonce = b64decode(bundle.nonce_b64);
+ const wrapped = b64decode(bundle.wrapped_b64);
+
+ const skX = await crypto.subtle.importKey(
+ 'pkcs8', skXPkcs8, { name: 'X25519' }, false, ['deriveBits']);
+ const pkEph = await crypto.subtle.importKey(
+ 'raw', pkEphRaw, { name: 'X25519' }, false, []);
+ const sharedBits = await crypto.subtle.deriveBits(
+ { name: 'X25519', public: pkEph }, skX, 256);
+
+ const sharedKey = await crypto.subtle.importKey(
+ 'raw', sharedBits, 'HKDF', false, ['deriveKey']);
+ const wrapKey = await crypto.subtle.deriveKey(
+ { name: 'HKDF', hash: 'SHA-256', salt: pkEphRaw,
+ info: new TextEncoder().encode('meshbay:gek_wrap:v1:aes') },
+ sharedKey,
+ { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
+
+ const plain = await crypto.subtle.decrypt(
+ { name: 'AES-GCM', iv: nonce, additionalData: pkXRaw }, wrapKey, wrapped);
+ return new Uint8Array(plain);
+}
+
+// ── Chunk encryption (for upload) ────────────────────────────────────────────
+
+async function encryptChunk(gek, fileHashHex, chunkIndex, plaintext) {
+ const chunkKey = await deriveChunkKey(gek, fileHashHex, chunkIndex);
+ const nonce = crypto.getRandomValues(new Uint8Array(12));
+ const ct = await crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv: nonce }, chunkKey, plaintext);
+ return { nonce, ct: new Uint8Array(ct) };
+}
+
+function b64encode(bytes) {
+ return btoa(String.fromCharCode(...bytes));
+}
+
// Export for use in app.js
-window.MeshBayCrypto = { importGEK, deriveChunkKey, decryptChunk, decryptChunkBin, decryptFile };
+window.MeshBayCrypto = {
+ importGEK, deriveChunkKey, decryptChunk, decryptChunkBin, decryptFile,
+ generateGEK, wrapGEK, unwrapGEK, encryptChunk, b64encode, b64decode,
+};