aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_recovery_mailto.py
blob: 401a69f56d8015970f8853ef0ba47366aedd496f (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
"""
"E-mail it to myself" on the recovery screen (docs/MESHBAY_DESIGN.md §3.6).

The recovery key never reaches the hub; a `mailto:` link has the person's own
mail application send it to their own address. This runs the page's
`recoveryMailto` under Node, with the English strings, and reads the link back
the way a mail application would.
"""

import json
import shutil
import subprocess
from urllib.parse import parse_qs, unquote, urlsplit

import pytest
from spa_source import STATIC

pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="no node")

MNEMONIC = "ABCD EFGH JKLM NPQR STUV WXYZ 2345 6789 ABCD EFGH JKLM NPQR STUV"

HARNESS = r"""
const fs = require('fs');
const [page, locale, email, user, key] = process.argv.slice(1);
const src = fs.readFileSync(page, 'utf8');
const start = src.indexOf('export function recoveryMailto(');
if (start < 0) throw new Error('recoveryMailto not found');
const end = src.indexOf('\n}\n', start);
const fn = src.slice(start, end + 2).replace(/^export /, '');
(async () => {
  const strings = (await import(locale)).default;
  const t = (k, params) => {
    let s = strings[k];
    if (s == null) throw new Error('missing string ' + k);
    for (const [n, v] of Object.entries(params || {})) s = s.split(`{${n}}`).join(v);
    return s;
  };
  const recoveryMailto = new Function('t', fn + '\nreturn recoveryMailto;')(t);
  process.stdout.write(JSON.stringify(recoveryMailto(email, user, key)));
})();
"""


def _link(email: str, user: str = "alice_tester") -> str:
    out = subprocess.run(
        ["node", "-e", HARNESS, str(STATIC / "auth-page.js"),
         (STATIC / "locales" / "en.js").as_uri(), email, user, MNEMONIC],
        capture_output=True, text=True, check=True)
    return json.loads(out.stdout)


def test_the_link_sends_the_key_to_the_person_and_nowhere_else():
    link = _link("alice+mesh@example.org")
    parts = urlsplit(link)
    assert parts.scheme == "mailto"
    assert unquote(parts.path) == "alice+mesh@example.org"
    assert "@" in parts.path, "an encoded '@' is not decoded by every mail app"
    query = parse_qs(parts.query)
    assert set(query) == {"subject", "body"}, "no cc, no bcc, nothing else"
    body = query["body"][0]
    assert MNEMONIC in body and "alice_tester" in body
    assert "\n\n" in body, "line breaks survive the encoding"


def test_an_address_cannot_add_recipients_or_headers():
    # An address is the person's own, but it is still text put into a URL.
    link = _link("a@example.org?bcc=x@evil.example&body=")
    query = parse_qs(urlsplit(link).query)
    assert set(query) == {"subject", "body"}
    assert MNEMONIC in query["body"][0]