aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_bundle_key.py48
-rw-r--r--packages/meshbay-hub/tests/test_desktop_keyring.py34
2 files changed, 80 insertions, 2 deletions
diff --git a/packages/meshbay-hub/tests/test_bundle_key.py b/packages/meshbay-hub/tests/test_bundle_key.py
index 333f8f8..f6c99f8 100644
--- a/packages/meshbay-hub/tests/test_bundle_key.py
+++ b/packages/meshbay-hub/tests/test_bundle_key.py
@@ -156,7 +156,7 @@ def test_an_earlier_format_is_refused_by_name(tmp_path):
}
console.log(JSON.stringify(results));
""")
- assert out == [["retired", "bundle_format_retired"], ["retired", "bundle_format_retired"]]
+ assert out == [["legacy", "bundle_format_retired"], ["retired", "bundle_format_retired"]]
def test_two_devices_of_one_account_derive_the_same_playlist_key(tmp_path):
@@ -181,3 +181,49 @@ def test_the_playlist_key_is_no_node_key(tmp_path):
}));
""")
assert out["distinct"]
+
+
+def test_an_mbk2_bundle_is_opened_once_and_sealed_again_as_mbk3(tmp_path):
+ """
+ TRANSITIONAL. Nodes still hold bundles sealed under the passphrase's Argon2
+ key alone. The same Argon2 run that makes `M` makes that key, so the session
+ keeps it — decrypt only — and the identity is moved to MBK3 on the account's
+ next visit instead of the member being re-invited.
+ """
+ out = _run(tmp_path, """
+ argonCalls = 0;
+ const sk = await K().deriveBundleSessionKey('p', 'someone', 'uid-1', PEPPER, 1);
+ const calls = argonCalls;
+ // An MBK2 bundle as 0.16 wrote it: "MBK2" ‖ nonce ‖ AES-GCM(A), no AAD.
+ const a = await crypto.subtle.importKey('raw', await _bundleKeyBytes('p', 'someone'),
+ { name: 'AES-GCM' }, false, ['encrypt']);
+ const nonce = new Uint8Array(12).fill(3);
+ const plain = new TextEncoder().encode(JSON.stringify({ skEd: btoa('ED'), skX: btoa('XX') }));
+ const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, a, plain));
+ const raw = new Uint8Array(4 + 12 + ct.length);
+ raw.set(new TextEncoder().encode('MBK2')); raw.set(nonce, 4); raw.set(ct, 16);
+ const mbk2 = btoa(String.fromCharCode(...raw));
+
+ const keys = await K().decryptLegacyBundle(mbk2, sk.legacy);
+ const resealed = await K().resealLegacyIdentity(keys, sk, null, { userId: 'uid-1', nodePk: 'NODE' });
+ const back = await K().decryptBundle(resealed.bundleEnc, await K().nodeBundleKey(sk, 'NODE'),
+ { userId: 'uid-1', nodePk: 'NODE' });
+ let otherPassphrase = 'opened';
+ const sk2 = await K().deriveBundleSessionKey('another', 'someone', 'uid-1', PEPPER, 1);
+ try { await K().decryptLegacyBundle(mbk2, sk2.legacy); } catch { otherPassphrase = 'refused'; }
+ let sealsUnderLegacy = 'yes';
+ try { await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, sk.legacy, plain); }
+ catch { sealsUnderLegacy = 'no'; }
+ console.log(JSON.stringify({
+ calls, format: K().bundleFormat(mbk2), keys, newFormat: K().bundleFormat(resealed.bundleEnc),
+ back, otherPassphrase, sealsUnderLegacy, extractable: sk.legacy.extractable,
+ }));
+ """)
+ assert out["calls"] == 1, "keeping the legacy key must not cost a second Argon2 run"
+ assert out["format"] == "legacy"
+ assert out["keys"] == {"skEd": "RUQ=", "skX": "WFg="}
+ assert out["newFormat"] == "current"
+ assert out["back"] == out["keys"]
+ assert out["otherPassphrase"] == "refused"
+ assert out["sealsUnderLegacy"] == "no", "the legacy key opens; it never seals"
+ assert out["extractable"] is False
diff --git a/packages/meshbay-hub/tests/test_desktop_keyring.py b/packages/meshbay-hub/tests/test_desktop_keyring.py
index 963ee55..e55e6d0 100644
--- a/packages/meshbay-hub/tests/test_desktop_keyring.py
+++ b/packages/meshbay-hub/tests/test_desktop_keyring.py
@@ -119,10 +119,33 @@ const v = JSON.parse(fs.readFileSync(input, 'utf8'));
await K.nodeBundleKey(sk, 'NODE-P'), { userId: v.userId, nodePk: 'NODE-P' });
out.sealed_here_opens_in_page = back.skX === pageId.skXB64;
+ // 3b. TRANSITIONAL: an MBK2 bundle, sealed under the Argon2 key alone as
+ // 0.16 wrote it, is opened with the legacy key kept beside M.
+ {
+ const nc = require('crypto');
+ const salt = nc.createHash('sha256').update(`meshbay:bundle:v2:${v.user}`).digest().subarray(0, 16);
+ const a = await argon2(v.password, salt,
+ { memory: 131072, passes: 3, parallelism: 1, tagLength: 32 });
+ const ed = nc.generateKeyPairSync('ed25519').privateKey.export({ format: 'der', type: 'pkcs8' });
+ const x = nc.generateKeyPairSync('x25519').privateKey.export({ format: 'der', type: 'pkcs8' });
+ const nonce = nc.randomBytes(12);
+ const c = nc.createCipheriv('aes-256-gcm', Buffer.from(a), nonce);
+ const body = Buffer.concat([c.update(JSON.stringify({ skEd: ed.toString('base64'),
+ skX: x.toString('base64') })), c.final()]);
+ const mbk2 = Buffer.concat([Buffer.from('MBK2'), nonce, body, c.getAuthTag()]).toString('base64');
+ out.legacy_open = ring.openBundle(v.userId, 'NODE-L', { bundleEnc: mbk2 });
+ out.legacy_kept = ring.identity(v.userId, 'NODE-L');
+ const keptLegacy = store.masters[v.userId].legacy;
+ delete store.masters[v.userId].legacy;
+ try { ring.openBundle(v.userId, 'NODE-M', { bundleEnc: mbk2 }); out.legacy_missing = 'opened'; }
+ catch (e) { out.legacy_missing = e.message; }
+ store.masters[v.userId].legacy = keptLegacy;
+ }
+
// 4. nothing but public keys come out of the keyring's answers.
out.identity_answer = ring.identity(v.userId, v.node);
out.retired = (() => { try { ring.openBundle(v.userId, 'NODE-R',
- { bundleEnc: Buffer.from('MBK2' + 'x'.repeat(40)).toString('base64') }); }
+ { bundleEnc: Buffer.from('y'.repeat(44)).toString('base64') }); }
catch (e) { return e.code; } })();
out.access_default = ring.browserAccess('someone-else');
ring.setBrowserAccess(v.userId, false);
@@ -284,3 +307,12 @@ def test_the_application_never_asks_its_own_crypto_for_argon2():
source = (KEYRING.parent / name).read_text(encoding="utf-8")
assert "crypto.argon2" not in source, name
assert "wasmArgon2(" in (KEYRING.parent / "main.js").read_text(encoding="utf-8")
+
+
+def test_an_mbk2_bundle_is_opened_with_the_kept_legacy_key(out):
+ """TRANSITIONAL. Kept unsealed, so the next settle replaces the node's copy
+ with MBK3 or withdraws it; without the legacy key the passphrase is asked
+ for, rather than the identity being given up."""
+ assert set(out["legacy_open"]) == {"pkEdB64", "pkXB64"}
+ assert out["legacy_kept"]["sealedWith"] is None
+ assert out["legacy_missing"] == "no_legacy_key"