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 | 79 |
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, +}; |