aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_bundle_key.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_bundle_key.py')
-rw-r--r--packages/meshbay-hub/tests/test_bundle_key.py48
1 files changed, 47 insertions, 1 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