aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_android_keys.py
blob: a00b909c088ed7a5117a192c75d474df3c713ccf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"""
The Android keyring against the desktop's, where the shared vectors cannot see.

`tests/vectors/keyring.json` holds the bytes (and the Android unit tests read
it). What a vector cannot hold is a *list*: which admin operations may be
signed at all, and the Argon2 parameters a format change would move. Two
implementations of a list drift silently — an operation the desktop stopped
signing at MNP 6.0 and Android still signs is a script in the page driving an
older node into widening its sharing. So both are read from source and
compared.
"""

import re
from pathlib import Path

import pytest

PACKAGES = Path(__file__).resolve().parents[2]
MAIN = PACKAGES / "meshbay-android" / "app" / "src" / "main"
KEYS = MAIN / "kotlin" / "org" / "meshbay" / "client" / "keys"
CLIENT = PACKAGES / "meshbay-client" / "src"
SHIM = MAIN / "assets" / "bridge" / "meshbay-bridge.js"

pytestmark = pytest.mark.skipif(not KEYS.exists(), reason="android sources not present")


def _read(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def test_the_same_admin_operations_are_signed():
    js = _read(CLIENT / "transcripts.js")
    js = js.split("const ADMIN_OPS = new Set([", 1)[1].split("]", 1)[0]
    kt = _read(KEYS / "Transcripts.kt").split("val ADMIN_OPS = setOf(", 1)[1].split(")", 1)[0]
    desktop = set(re.findall(r"'([a-z_]+)'", js))
    android = set(re.findall(r'"([a-z_]+)"', kt))
    assert desktop and android == desktop, android ^ desktop
    # The ones MNP 6.0 took away must not come back on either side.
    assert not {"root_add", "root_update", "group_attach"} & android


def test_the_argon2_parameters_are_the_desktops():
    js = _read(CLIENT / "keyring.js")
    m = re.search(r"memory: (\d+), passes: (\d+), parallelism: (\d+), tagLength: (\d+)", js)
    kt = _read(KEYS / "Kdf.kt")
    want = dict(zip(("MEMORY_KIB", "PASSES", "PARALLELISM", "TAG"), m.groups()))
    for name, value in want.items():
        assert re.search(rf"const val ARGON2_{name} = {value}\b", kt), name


def test_the_shim_offers_the_desktops_key_surface():
    preload = _read(CLIENT / "preload.js")
    shim = _read(SHIM)

    def channels(text: str, call: str) -> set[str]:
        return set(re.findall(rf"{call}\('((?:keys|device|secrets):[\w:-]+)'", text))

    assert channels(shim, "call") == channels(preload, r"ipcRenderer\.invoke")


def test_signing_is_by_kind_and_no_private_key_is_returned():
    channels = _read(KEYS.parent / "bridge" / "KeyChannels.kt")
    assert '"keys:sign" -> keyring.signAs(' in channels
    # No channel hands the page a stored key: the store's slots are never a
    # return value, and identity/mint/open answer with public keys.
    assert "secrets.read()" in channels  # the keyring's load, and nothing else
    assert channels.count("secrets.read()") == 1
    assert '"pkEdB64"' in channels and '"skEd"' not in channels


def test_widening_browser_access_is_confirmed_natively():
    channels = _read(KEYS.parent / "bridge" / "KeyChannels.kt")
    branch = channels.split('"keys:set-browser-access" ->', 1)[1].split('"keys:created-here"', 1)[0]
    assert 'confirm("native.browser_access_confirm")' in branch