diff options
| author | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-10-30 17:24:45 -0500 |
|---|---|---|
| committer | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-10-30 17:24:45 -0500 |
| commit | 7a0ce2805258e8ac061202904379692d53d5dc95 (patch) | |
| tree | a495aee0cbdc111cd9773f7d2d02753cd1d778d5 /crates/chat-crypto/browser | |
| parent | 436e217c7858888702d2b6d9e655597ccadb7281 (diff) | |
Prove OpenSSL PQ crypto in browser WASI and document the migrationfeature/private-chat
Diffstat (limited to 'crates/chat-crypto/browser')
| -rw-r--r-- | crates/chat-crypto/browser/wasi.mjs | 106 |
1 files changed, 106 insertions, 0 deletions
diff --git a/crates/chat-crypto/browser/wasi.mjs b/crates/chat-crypto/browser/wasi.mjs new file mode 100644 index 0000000..d27fe4b --- /dev/null +++ b/crates/chat-crypto/browser/wasi.mjs @@ -0,0 +1,106 @@ +// 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/>. + +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 + +export async function runProbe(bytes) { + let memory; + let output = ''; + let errors = ''; + let randomCalls = 0; + let randomBytes = 0; + const data = () => new DataView(memory.buffer); + const put32 = (address, value) => data().setUint32(address, value, true); + const noEntries = (count, size) => { put32(count, 0); put32(size, 0); return 0; }; + class Exit extends Error { + constructor(code) { super(`WASI exit ${code}`); this.code = code; } + } + const wasi = { + args_sizes_get: noEntries, + environ_sizes_get: noEntries, + args_get: () => 0, + environ_get: () => 0, + random_get(address, length) { + const buffer = new Uint8Array(memory.buffer, address, length); + for (let offset = 0; offset < length; offset += 65536) { + crypto.getRandomValues(buffer.subarray(offset, Math.min(offset + 65536, length))); + } + randomCalls += 1; + randomBytes += length; + return 0; + }, + clock_time_get(clock, precision, address) { + if (clock !== 0 && clock !== 1) return 52; + const milliseconds = clock === 0 ? Date.now() : performance.now(); + data().setBigUint64(address, BigInt(Math.floor(milliseconds * 1000000)), true); + return 0; + }, + clock_res_get(clock, address) { data().setBigUint64(address, 1000000n, true); return 0; }, + fd_write(fd, vectors, count, written) { + if (fd !== 1 && fd !== 2) return 8; + let total = 0; + for (let index = 0; index < count; index += 1) { + const address = data().getUint32(vectors + index * 8, true); + const length = data().getUint32(vectors + index * 8 + 4, true); + const text = new TextDecoder().decode(new Uint8Array(memory.buffer, address, length)); + if (fd === 1) output += text; else errors += text; + total += length; + if (output.length + errors.length > 65536) throw new Error('Probe output exceeded its limit'); + } + put32(written, total); + return 0; + }, + fd_read(fd, vectors, count, read) { if (fd !== 0) return 8; put32(read, 0); return 0; }, + fd_fdstat_get(fd, address) { + if (fd > 2) return 8; + new Uint8Array(memory.buffer, address, 24).fill(0); + data().setUint8(address, 2); + return 0; + }, + fd_fdstat_set_flags: fd => fd <= 2 ? 0 : 8, + fd_prestat_get: () => 8, + fd_prestat_dir_name: () => 8, + fd_close: fd => fd <= 2 ? 0 : 8, + fd_seek: () => 70, + fd_tell: () => 70, + fd_filestat_get: () => 8, + path_open: () => 76, + sched_yield: () => 0, + proc_exit(code) { throw new Exit(code); }, + }; + for (const name of [ + 'fd_readdir', 'fd_advise', 'fd_allocate', 'fd_datasync', 'fd_sync', + 'fd_filestat_set_size', 'fd_filestat_set_times', 'fd_fdstat_set_rights', + 'path_filestat_get', 'path_filestat_set_times', 'path_readlink', 'path_unlink_file', + 'path_rename', 'path_create_directory', 'path_remove_directory', 'path_link', 'path_symlink', + 'sock_accept', 'sock_recv', 'sock_send', 'sock_shutdown', 'poll_oneoff', + ]) wasi[name] = () => 76; + const module = await WebAssembly.compile(bytes); + for (const entry of WebAssembly.Module.imports(module)) { + if (entry.module !== 'wasi_snapshot_preview1' || typeof wasi[entry.name] !== 'function') { + throw new Error(`Unsupported WASI import: ${entry.module}.${entry.name}`); + } + } + const instance = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: wasi }); + memory = instance.exports.memory; + try { instance.exports._start(); } + catch (error) { if (!(error instanceof Exit) || error.code !== 0) throw new Error(`${error.message}: ${errors}`); } + if (!output.startsWith('PASS: backend=OpenSSL ') || randomCalls === 0) { + throw new Error(`OpenSSL probe did not pass: ${errors || output}`); + } + return { output, randomCalls, randomBytes }; +} + +// @license-end |
