diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/keyderive.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/keyderive.js | 55 |
1 files changed, 52 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js index 918ade3..edfa109 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js @@ -142,7 +142,7 @@ async function deriveEncryptionKeyV1(password, username) { * passphrase in memory to re-derive it whenever a bundle turns up. It is unique * per account, so it does what a salt is for — no shared precomputation. */ -async function deriveEncryptionKey(password, username) { +async function _bundleKeyBytes(password, username) { const enc = new TextEncoder(); const salt = new Uint8Array(await crypto.subtle.digest( 'SHA-256', enc.encode(`meshbay:bundle:v2:${username}`))).slice(0, 16); @@ -151,8 +151,42 @@ async function deriveEncryptionKey(password, username) { time: ARGON2_TIME, mem: ARGON2_MEM_KIB, parallelism: ARGON2_LANES, hashLen: 32, type: _argon2().ArgonType.Argon2id, }); + return out.hash; +} + +async function deriveEncryptionKey(password, username) { return crypto.subtle.importKey( - 'raw', out.hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); + 'raw', await _bundleKeyBytes(password, username), + { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); +} + +/** + * The bundle key as **two handles over one Argon2 run**. + * + * `aes` is what has always been returned: the key that opens a node's identity + * bundle. `hkdf` is the same 32 bytes imported a second time as an HKDF key, + * from which purpose-separated subkeys can be derived — playlists are the + * first (docs/playlists.md §3.4). + * + * It has to be a second import of the same bytes, and not a derivation from + * `aes`: that one is imported non-extractably with `['encrypt','decrypt']`, so + * nothing can be derived from it at all. And it has to be one Argon2 run: a + * second call would put another ~650 ms on the sign-in path for a key that is + * mathematically identical. + * + * A subkey rather than the bundle key reused with a different AAD, for the + * reason `groupbox.py` already writes down for chunk keys — purpose separation + * is what stops one use's mistake becoming every use's. + */ +async function deriveBundleKeys(password, username) { + const raw = await _bundleKeyBytes(password, username); + return { + aes: await crypto.subtle.importKey( + 'raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']), + // HKDF keys are non-extractable by specification; `false` is the only + // value this accepts. + hkdf: await crypto.subtle.importKey('raw', raw, 'HKDF', false, ['deriveKey']), + }; } // ── Account recovery key ───────────────────────────────────────────────────── @@ -393,7 +427,7 @@ async function loginAndRecover(username, password) { // Both, so a bundle written before the KDF changed can still be opened — // and re-written with the new one on the next backup. bundleKey: { - v2: await deriveEncryptionKey(password, username), + ...(await _bundleKeyPairFields(password, username)), v1: await deriveEncryptionKeyV1(password, username), }, }; @@ -417,6 +451,17 @@ async function signBytes(skEdPkcs8B64, message) { return btoa(String.fromCharCode(...new Uint8Array(sig))); } +/** + * `{ v2, v2hkdf }` — the two fields every `session.bundleKey` carries for the + * current KDF. One helper because there are two places that build that object + * and they must not drift: a `v2hkdf` missing from one of them is a playlist + * store that silently does nothing on whichever sign-in path skipped it. + */ +async function _bundleKeyPairFields(password, username) { + const { aes, hkdf } = await deriveBundleKeys(password, username); + return { v2: aes, v2hkdf: hkdf }; +} + window.MeshBayKeys = { registerUser, loginAndRecover, generateNodeIdentity, generateKeypairs, signBytes, deriveAuthKey, decryptBundleWithKey, encryptBundleWithKey, bundleVersion, @@ -424,6 +469,10 @@ window.MeshBayKeys = { // node's identity bundle needs the old key (a {v2,v1} pair, since an old // bundle may be v1) to read it and the new v2 key to write it back. deriveEncryptionKey, deriveEncryptionKeyV1, + // One Argon2 run, an AES handle and an HKDF handle. Whatever builds a + // `session.bundleKey` uses this, so `v2hkdf` is never the field one sign-in + // path forgot (docs/playlists.md §3.4). + deriveBundleKeys, bundleKeyPairFields: _bundleKeyPairFields, // Account recovery key (docs/auth-confirm.md §4.3). generateRecoveryKey, deriveRecoveryKey, }; |