aboutsummaryrefslogtreecommitdiff
path: root/crates/chat-crypto/tests/browser.mjs
blob: 8d2a9bbd7fbde29b7dc242c7ec0e47f3b83b4fbc (plain)
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
75
76
77
78
79
80
81
82
83
84
85
86
// Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import assert from 'node:assert/strict';
import { chromium } from '../../../.tools/browser/node_modules/playwright/index.mjs';

const assets = new Map([
    ['/wasi.mjs', [resolve('crates/chat-crypto/browser/wasi.mjs'), 'text/javascript']],
    ['/probe.wasm', [resolve('target/wasm32-wasip1/release/stack-chat-crypto.wasm'), 'application/wasm']],
]);
const server = createServer(async (request, response) => {
    response.setHeader('Cache-Control', 'no-store');
    if (request.url === '/') {
        response.setHeader('Content-Type', 'text/html');
        response.end('<!doctype html><title>Local cryptographic compatibility test</title>');
        return;
    }
    const asset = assets.get(request.url);
    if (!asset) { response.writeHead(404); response.end(); return; }
    response.setHeader('Content-Type', asset[1]);
    response.end(await readFile(asset[0]));
});
await new Promise(resolveReady => server.listen(0, '127.0.0.1', resolveReady));
const origin = `http://127.0.0.1:${server.address().port}`;
const browser = await chromium.launch({headless: true, channel: "chromium"});
const deadline = setTimeout(() => browser.close(), 120000);
try {
    const page = await browser.newPage();
    const external = [];
    page.on('request', request => {
        if (!request.url().startsWith(origin + '/')) external.push(request.url());
    });
    await page.goto(origin);
    const result = await page.evaluate(async () => {
        const { runProbe } = await import('/wasi.mjs');
        const bytes = await (await fetch('/probe.wasm', { cache: 'no-store' })).arrayBuffer();
        const valid = await runProbe(bytes);
        const again = await runProbe(bytes);
        const getRandomValues = crypto.getRandomValues;
        let rejected = false;
        crypto.getRandomValues = () => { throw new Error('Entropy unavailable'); };
        try { await runProbe(bytes); } catch (error) { rejected = error.message.includes("Entropy unavailable"); }
        finally { crypto.getRandomValues = getRandomValues; }
        const modified = new Uint8Array(bytes.slice(0));
        const pattern = new TextEncoder().encode('random_get');
        const offset = modified.findIndex((value, index) => pattern.every((byte, position) => modified[index + position] === byte));
        if (offset < 0) throw new Error('Expected entropy import was not found');
        modified.set(new TextEncoder().encode('reject_get'), offset);
        let unknownImportRejected = false;
        try { await runProbe(modified); }
        catch (error) { unknownImportRejected = error.message.includes('Unsupported WASI import: wasi_snapshot_preview1.reject_get'); }
        return { valid, again, rejected, unknownImportRejected };
    });
    assert.match(result.valid.output, /^PASS: backend=OpenSSL 3\.6\.5;/);
    assert.match(result.again.output, /^PASS: backend=OpenSSL 3\.6\.5;/);
    assert.ok(result.valid.randomCalls > 0 && result.valid.randomBytes > 0);
    assert.ok(result.rejected, 'Unavailable browser entropy must fail closed');
    assert.ok(result.unknownImportRejected, 'Unknown host imports must fail closed');
    assert.deepEqual(external, []);
    assert.deepEqual(await page.context().cookies(), []);
    assert.equal(await page.evaluate(() => localStorage.length + sessionStorage.length), 0);
    assert.deepEqual(await page.evaluate(async () => indexedDB.databases()), []);
    assert.equal(await page.evaluate(async () => (await navigator.serviceWorker.getRegistrations()).length), 0);
    console.log(result.valid.output.trim());
    console.log(`Browser CSPRNG: ${result.valid.randomCalls} calls, ${result.valid.randomBytes} bytes; missing entropy and unknown imports rejected.`);
    console.log('PASS: actual Chromium WASM execution; no external requests, cookies, web storage, or IndexedDB.');
} finally {
    clearTimeout(deadline);
    await browser.close();
    await new Promise(resolveClosed => server.close(resolveClosed));
}