diff options
Diffstat (limited to 'packages/meshbay-client/src')
| -rw-r--r-- | packages/meshbay-client/src/keyring.js | 34 |
1 files changed, 32 insertions, 2 deletions
diff --git a/packages/meshbay-client/src/keyring.js b/packages/meshbay-client/src/keyring.js index 4fb6eac..ec37fd4 100644 --- a/packages/meshbay-client/src/keyring.js +++ b/packages/meshbay-client/src/keyring.js @@ -24,6 +24,8 @@ const { transcriptFor } = require('./transcripts.js'); // keyderive.js: the same numbers, or no bundle opens across the two. const ARGON2 = { memory: 131072, passes: 3, parallelism: 1, tagLength: 32 }; const MAGIC = Buffer.from('MBK3'); +// TRANSITIONAL — the format before MBK3, read once to be replaced (keyderive.js). +const LEGACY_MAGIC = Buffer.from('MBK2'); const X25519_SPKI = Buffer.from('302a300506032b656e032100', 'hex'); const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; @@ -63,6 +65,17 @@ function seal(identity, key, userId, nodePk, pepperVersion) { return b64(Buffer.concat([MAGIC, Buffer.from([pepperVersion & 0xff]), nonce, ct])); } +/** TRANSITIONAL — MBK2: "MBK2" ‖ nonce ‖ AES-GCM under the Argon2 key, no AAD. */ +function openLegacy(bundleB64, key) { + const raw = unb64(bundleB64); + const nonce = raw.subarray(4, 16); + const body = raw.subarray(16, raw.length - 16); + const d = crypto.createDecipheriv('aes-256-gcm', key, nonce); + d.setAuthTag(raw.subarray(raw.length - 16)); + const plain = JSON.parse(Buffer.concat([d.update(body), d.final()]).toString()); + return { ed: plain.skEd, x: plain.skX }; +} + function open(bundleB64, key, userId, nodePk) { const raw = unb64(bundleB64); if (!raw.subarray(0, 4).equals(MAGIC)) { @@ -142,7 +155,11 @@ function createKeyring({ load, save, argon2 }) { const v = pepperVersion || 1; if (p) { pending.set(userId, { m, v }); return true; } const s = state(); - s.masters[userId] = { m: b64(m), v }; + // `legacy` (TRANSITIONAL): the Argon2 key itself, which MBK2 bundles + // were sealed under — kept beside `M`, in the same OS-protected store + // and for as long, so a node still holding one has it opened and + // replaced on the next connection. Remove once no MBK2 bundle is left. + s.masters[userId] = { m: b64(m), v, legacy: b64(a) }; save(s); return true; }, @@ -150,7 +167,10 @@ function createKeyring({ load, save, argon2 }) { const p = pending.get(userId); if (!p) return false; const s = state(); - s.masters[userId] = { m: b64(p.m), v: p.v }; + // The legacy key stays the old passphrase's: MBK2 bundles were sealed + // under that one, never under the new. + const legacy = (s.masters[userId] || {}).legacy; + s.masters[userId] = { m: b64(p.m), v: p.v, ...(legacy ? { legacy } : {}) }; save(s); pending.delete(userId); return true; @@ -177,6 +197,16 @@ function createKeyring({ load, save, argon2 }) { * was entered (a reset on a machine that had never held this identity). */ openBundle(userId, nodePk, { bundleEnc, recoveryEnc, recoveryMnemonic, username }) { + if (unb64(bundleEnc).subarray(0, 4).equals(LEGACY_MAGIC)) { + // TRANSITIONAL. Kept unsealed (`sealedWith: null`), so the next + // settle replaces the node's copy with MBK3, or withdraws it when the + // account has no browser access. + const legacy = (state().masters[userId] || {}).legacy; + if (!legacy) throw new Error('no_legacy_key'); + const id = openLegacy(bundleEnc, unb64(legacy)); + keep(userId, nodePk, { ...id, sealedWith: null }); + return publicOf(id); + } const { m } = master(userId); let id; try { |