aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/keyderive.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/keyderive.js55
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,
};