From 556eaa133e9cfe7f4c8e542708119fdfd311357e Mon Sep 17 00:00:00 2001 From: Doan Luu Duc Tai Date: Fri, 30 Oct 2026 21:39:57 -0500 Subject: Implement experimental file-based encrypted private chat Add fixed-suite Rust/WASI identity files, signed challenge authentication, ciphertext-only SQLite storage, a minimal LibreJS-compatible interface, and consistent backup snapshots. Keep production unchanged and document the local workflow, limits, source distribution, and browser recovery tests. --- docs/CHAT-APPLICATION.txt | 336 ++++++++++++++++++++++++++++++++++++++++++++++ docs/CHAT-DESIGN.txt | 7 +- 2 files changed, 340 insertions(+), 3 deletions(-) create mode 100644 docs/CHAT-APPLICATION.txt (limited to 'docs') diff --git a/docs/CHAT-APPLICATION.txt b/docs/CHAT-APPLICATION.txt new file mode 100644 index 0000000..2a06649 --- /dev/null +++ b/docs/CHAT-APPLICATION.txt @@ -0,0 +1,336 @@ +Copyright (C) 2026 Doan Luu Duc Tai + +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 . + + +EXPERIMENTAL PRIVATE CHAT + + Branch: feature/private-chat. This application is for local testing. + Production remains on main with its under-construction Message Me page. + No public chat service, TLS route, or new production identity was deployed. + This is working experimental software, not an independently audited messenger. + +WHAT EXISTS + + A Rust HTTP server and a Rust cryptographic command compiled to WASI. + Small, readable, free JavaScript modules implement the browser interface and + a capability-limited WASI host. Text is displayed through textContent. + SearXNG and the other existing services are unchanged. + + Visitors choose CREATE NEW CHAT or OPEN CHAT FILE. Creation generates a + purpose-specific identity locally and downloads its protected file. Reopening + the exact downloaded file and entering its password is required before the + first authentication or message. No username/password account is created. + Returning users select their file and password again. The stable file does + not need updating as messages arrive. The owner has a separate chat identity + and inbox; the canonical RSA OpenPGP key is not used for chat encryption. + + History defaults to zero messages. Authenticated counters show total, sent, + and received messages. Load 0 to 50 recent messages, then page backwards. + Reload, lock, or leaving clears the application's in-memory references and + visible messages. A separate authenticated session expires after 30 minutes. + There is no automatic background polling, notification, or remote logout. + Refresh the owner inbox and load history to fetch newly arrived messages. + +LOCAL PREREQUISITES AND COMMANDS + + Start in the root of the stack repository on feature/private-chat. Native + development needs Rust, libclang, OpenSSL 3.5 or newer development libraries, + a C compiler, pkg-config, make, Perl, curl, and tar. The checked configuration + uses OpenSSL 3.6.5. The WASI build script currently targets an x86_64 GNU/Linux + development host and downloads the hash-verified WASI SDK 34.0 and OpenSSL + source. General-purpose toolchains remain development tools, not VPS tools. + + Build and initialize once: + + sh infra/chat/build-openssl-wasi.sh + cargo stack licenses + sh infra/chat/build-source.sh + cargo stack chat --init + + Initialization appends CHAT_SETUP_TOKEN and CHAT_ORIGIN to the single root + .env, creating it privately if necessary. It refuses to overwrite an existing + setup token. The root .env.example lists both settings. Never commit .env. + Existing SearXNG settings stay in the same file. No crate has its own .env. + + For subsequent Rust-only edits, sh infra/chat/build-browser.sh reuses the + previously built, hash-verified OpenSSL/SDK inputs. Rebuild OpenSSL itself + after changing its source, version, configuration, or build flags. The WASI + module has a 256 MiB maximum linear-memory limit. + + Start: + + cargo stack chat + + Open: + + http://127.0.0.1:9080/#setup + + Read CHAT_SETUP_TOKEN from your root .env locally and paste it into the owner + setup form. Choose an owner display name and a strong unique password. Save + the downloaded file, reopen it, and unlock it. That establishes the owner + public identity. The setup token stops authorizing setup once an owner exists. + It does not become an owner login or a key recovery mechanism. Never publish + the owner private file or its password. Keep both in your private KeePassXC + database, along with a separately protected backup of that database. + + Open another browser window at http://127.0.0.1:9080/ to create a visitor chat. + Every page holds its own identity in memory; there is no shared browser login. + Stop the development server with Control-C. A server restart invalidates all + sessions but preserves identities, conversations, counts, and ciphertext. + + The server currently refuses a non-loopback bind or an origin that does not + exactly match its development bind address. HTTP is allowed only for the + explicit loopback development origin. The intended production origin remains + https://chat.doan.luuduct.ai/. Production Podman packaging, TLS configuration, + operational review, and a main-branch merge remain separate work. Do not + expose this experimental development service through a public proxy. + +FILE AND MESSAGE CRYPTOGRAPHY + + OpenPGP v6, ML-KEM-1024 + X448 encryption, ML-DSA-87 + Ed448 signatures, + and AES-256 content encryption. There is no RSA, classical-only, or alternative + post-quantum fallback. Each certificate has exactly the expected two keys and + a fixed purpose-only user ID. Names are not placed in public certificate IDs. + The server rejects uploaded secret-key material and unsupported certificates. + + The JSON identity file is an authenticated encrypted envelope. Its fixed + format identifier is authenticated as associated data: + + stack-chat-file/v1;argon2id-m65536-t3-p1;aes256-gcm + + Argon2id version 0x13 uses 65536 KiB, three passes, one lane, and a 32-byte + derived key. A fresh CSPRNG salt of 16 bytes and AES-256-GCM nonce of 12 bytes + are generated for each file; the authentication tag is 16 bytes. These KDF + parameters are fixed by the format, not accepted from an untrusted file. + The outer JSON contains only format, salt, nonce, ciphertext, and tag, encoded + in base64 where applicable. The encrypted payload contains the origin, name, + pinned owner public certificate, and the secret OpenPGP certificate. + Secret key material inside that already encrypted envelope is unprotected + only in process memory. Never extract or save the decrypted payload. + + Passwords must be 12 to 1024 UTF-8 bytes. This is a length floor, not a claim + of sufficient entropy: choose a strong unique passphrase. File parsing and + parameter checks precede the expensive KDF. Import is limited to 256 KiB. + Wrong passwords, malformed files, authentication failures, and unsupported + formats return a generic error. Mutable KDF keys, plaintext envelopes, and + private serialization buffers are zeroized where practical. + + The browser checks the file's exact origin and pinned owner certificate on + unlock. Owner identity creation pins its own public certificate. Visitor + creation pins the public certificate currently advertised over the origin's + trusted connection. First-visit trust is consequently still trust in the + served application and TLS; it is not an out-of-band identity guarantee. + + Each text message is signed, then encrypted for both participants. A signed + plaintext JSON object binds v=1, kind=message, exact origin, conversation ID, + sender fingerprint, CSPRNG message ID, and UTF-8 text. Receivers compare the verified signer fingerprint to the conversation + record and expected sender, then verify every payload binding before displaying text. Encrypted profile + objects bind kind=profile, origin, conversation, sender, and display name. + The Rust encryptor also enforces the pinned recipient, sender, origin, and + conversation before signing; the server cannot select an arbitrary recipient + by changing a public-key field. Display names remain labels, not proof of a + unique person. The interface shows full conversation fingerprints. + Message and profile formats are distinct to prevent cross-purpose replay. + Compression, unsigned messages, an unexpected content cipher, and malformed + recipient sets are rejected. Private certificates are never sent to the API. + +AUTHENTICATION AND STORAGE + + One JSON API endpoint /api accepts same-origin POST requests only; no CORS, + cookie authentication, URL credentials, or URL conversation identifiers. + A fresh random challenge binds protocol, exact origin, full fingerprint, + random nonce, an expiry, and the pinned owner fingerprint. It expires after 60 seconds and is consumed on + the first verification attempt, including a failed signature attempt. + A detached purpose-specific signature proves possession of the identity. + A random bearer session token then authorizes API requests for 30 minutes. + Session tokens and pending challenges exist only in server and page memory. + The owner can access the inbox and any conversation; a visitor can access + only the conversation indexed by their own verified fingerprint. + + Data lives under var/data/chat/chat.sqlite, in a private 0700 directory with + a 0600 database. SQLite uses foreign keys, FULL synchronous transactions, and + DELETE journaling. Durable data consists of the owner public certificate, + visitor public certificates, an encrypted name/profile, encrypted messages, + sender fingerprints, random message IDs, sequence numbers, and counts derived + from rows. Names and message text are not stored as plaintext. There are no + application access logs or deliberately retained client IP addresses. + SQLite journals contain the same public metadata and ciphertext, not private + keys or message plaintext. OS diagnostics and a future reverse proxy need + their own logging review before deployment. + + Sends are transactional. Retrying exactly the same ciphertext, message ID, + sender, and conversation returns its existing sequence without incrementing + counts. Reusing an ID with different data is rejected. This handles a response + lost after the message was stored. Keep the page unlocked to retry; reload + discards the pending ciphertext and draft, so inspect history before resending. + Send retries are deliberate; messages are not automatically resent. + + Limits: 8192 UTF-8 bytes per text, 16384 bytes per signed payload, 65536 base64 + characters per stored ciphertext, 50 history rows per request, 1000 visitor + identities, 10000 messages per conversation, and a one-billion-character + total message-ciphertext budget. At most 60 authentication challenges per + global minute window, 128 pending challenges, 256 active sessions, and four + queued cryptographic/database tasks are admitted. API bodies are at most + 256 KiB. Database access is serialized. These are basic bounds, not robust + protection against determined traffic, account creation, CPU, or disk attacks. + Public-key and profile storage, indexes, journals, and backups add overhead to + the ciphertext budget. Monitor actual disk use. No public abuse controls, + deletion UI, rotation UI, blocklist, or password-change UI are implemented. + +BROWSER STATE AND LIMITATIONS + + No application cookies, localStorage, sessionStorage, IndexedDB, service + workers, analytics, external scripts, fonts, or third-party requests. Assets + and API responses use no-store. Browser fetches omit credentials. Private + file/password bytes travel only to a local worker and Rust/WASM memory. + Each crypto worker terminates after its operation or a timeout. Lock and + pagehide terminate remaining workers, clear drafts/messages/credentials, + clear input controls, and revoke download URLs. Restored pages clear state. + + This means no intentional persistent application state, not literally zero + traces. Browser password managers, downloads, history, caches outside the + application's control, memory copies, swap, extensions, OS crash dumps, + screenshots, and a restored process can retain data. JavaScript strings, + garbage collection, compiler/library copies, and WASM memory cannot be + guaranteed erased. A 90-second worker timeout and 256 MiB WASM linear-memory maximum bound + each cryptographic instance. Browser/host copies add memory overhead. A malformed file or message can still expose defects in + the Rust, C, WASM, or browser stack. OpenSSL uses C/FFI; Rust's unsafe-code + prohibition does not make dependencies memory-safe. Native server and WASI + OpenSSL updates must both be reviewed and rebuilt when advisories warrant. + + A stolen file enables offline password guessing. A stolen file and password, + or an unlocked identity, enable impersonation and decryption of accessible + historical ciphertext. There is no username recovery, forward secrecy, or + automatic key ratchet. Losing the owner file/password loses owner access to + every conversation encrypted to it. A server rollback can omit messages; + signatures do not prove a complete transcript. Encrypted messages reveal + metadata, lengths, ordering, and interaction timing. The server sees client + addresses during connections. Bearer-token theft permits API use until + expiry or lock, though decrypting or signing still requires the private key. + + A malicious server can replace JavaScript or WASM and steal passwords and + plaintext on the next visit. A pinned key and LibreJS do not prevent that. + Shared-host compromise, browser extensions, a compromised OS, browser bugs, + dependency attacks, malicious friends, and denial of service remain risks. + A purpose label advises separation; it cannot prevent exporting and reusing + a key elsewhere. This application is not a SimpleX equivalent. + +BACKUP, EXPORT, AND RESTORE + + The existing cargo stack backup creates a GPG-encrypted archive and now uses + SQLite's online backup API for chat.sqlite. It does not copy a live SQLite + file or journal with ordinary file-copy code. It can take a consistent chat + snapshot while the local chat server runs. Existing Podman data still follows + the stack's previous pause/copy/resume process. A database snapshot is atomic + for chat data; unrelated services do not share a global transaction with it. + The backup includes the single root .env and must remain private/encrypted. + + For a chat-only consistent database export: + + cargo run --locked -p stack-chat-server -- --root . --snapshot var/exports/chat-snapshot.sqlite + + Create var/exports privately first if it does not exist. The destination must + not already exist. This standalone snapshot is ciphertext plus public metadata, + not a GPG-encrypted full-stack archive; encrypt it before off-machine storage. + It does not contain browser private identities. A server-side backup cannot + recover a lost user or owner file/password. + + Restore a full stack using the existing README.txt backup/restore instructions. + For a chat-only restore, stop the chat server, put the verified snapshot at + var/data/chat/chat.sqlite under the destination root, set directory mode 0700 + and database mode 0600, restore the single root .env privately, and rebuild + the corresponding source/WASM on the development machine. Start locally and + open the unchanged owner or visitor file with its password. Changing the + origin deliberately makes old files fail their origin binding; there is no + origin migration UI yet. Do not edit an encrypted file by hand. + + Keep at least one encrypted backup away from the VPS. Test the owner identity, + visitor identity, correct counts, and decrypted messages after a restore. + Keep private identity files and passwords in KeePassXC with a protected + database backup. Never place them in source control or the public source + archive. Deleting a message locally or losing a file does not delete stored + ciphertext, old exports, or other participants' copies. + +LICENSES, SOURCE, AND VERIFICATION + + Portable root CLI builds now also need a musl C compiler because consistent + snapshots use bundled SQLite. The root CLI was built and executed as an + x86_64 static PIE with SQLite; no runtime SQLite package is needed on the VPS. + The local check used Alpine musl 1.2.6 headers with the host C compiler for + the C object build and Rust's self-contained musl runtime for final linking. + + First-party software is GNU AGPL version 3 or later. Legal license headers + are the exception to the no-comments convention. Documentation is plaintext. + JavaScript has GNU LibreJS @license/@license-end markers and a linked + jslicense-labels1 table, following GNU's JavaScript license web-label format: + + https://www.gnu.org/licenses/javascript-labels.html + https://www.gnu.org/software/librejs/free-your-javascript.html + + The yellow GNU LibreJS-compliant badge links to the GNU project and exposes + a hover/focus explanation distinguishing software freedom from security. + A source archive is built by infra/chat/build-source.sh from an explicit + source allowlist, Cargo-vendored dependencies, and the original verified + OpenSSL archive. It excludes .env, .git, keys, target files, runtime data, + downloaded browser tools, and exports. .env.example is public. The original + dependency license texts are preserved. GNU-friendly human-readable names + such as Expat License are used in the generated third-party license page; + machine-readable SPDX identifiers remain unchanged. + + Rebuild the source archive after any source, lockfile, or notice changes. + Do not publish a binary without its exact corresponding sources. Inspect + tar -tzf var/build/chat-source.tar.gz and the generated license page. + The SQLite bundled C source is provided by libsqlite3-sys in the vendor tree. + The complete unmodified OpenSSL release is included separately. + + Run the normal strict workspace check, then browser tests: + + cargo stack check + PLAYWRIGHT_BROWSERS_PATH="$PWD/.tools/browsers" node crates/chat-crypto/tests/browser.mjs + PLAYWRIGHT_BROWSERS_PATH="$PWD/.tools/browsers" node crates/chat-server/tests/browser.mjs + + Browser automation additionally needs Node, Playwright under .tools/browser, + and its browser downloads under .tools/browsers. These are development tools + excluded from releases. The fixtures use temporary isolated installation roots + with one temporary .env each, and remove their private data on completion. + They do not initialize your real owner identity or touch production. + + LibreJS verification uses the Mozilla-distributed GNU LibreJS 7.21.1 in + Playwright Firefox 157.0, without whitelisting the application. The full chat + workflow passes while an unlicensed external control script is blocked. Set + CHAT_LIBREJS=1 when running the application browser test to reproduce this + variant; it needs that signed extension at .tools/librejs.xpi and Firefox + downloaded into .tools/browsers. The extension download used was: + + https://addons.mozilla.org/firefox/downloads/file/4169076/librejs-7.21.1.xpi + + Earlier tests exposed a LibreJS label mismatch: a combined GNU prefix and + or-later suffix was not recognized by that release. The table now uses its + recognized GNU-AGPL-3.0 identifier and the surrounding prose and legal headers + explicitly retain the version-3-or-later grant. Readable classic scripts and + workers are used instead of requiring module-script interception support. + This tests license recognition and operation, not the safety of delivered code. + + The crypto browser probe fails closed without browser CSPRNG entropy and on + unknown WASI imports. The application browser test uses real WASI/OpenSSL + cryptography, protected file downloads and reopen, incorrect passwords, + signed sends/replies, safe text display, counts, zero-history, pagination, + lost-response retry deduplication, reload, explicit lock, origin rejection, + live database snapshot restoration with both original identity files, in-flight + lock, back/forward restoration, and checks for plaintext persistence or private + credential uploads. + Native tests cover authorization, challenge reuse, expiry, locked sessions, + quotas, idempotency, and SQLite snapshot recovery. See CHAT-DESIGN.txt and + CHAT-OPENSSL-MIGRATION.txt for the design and historical backend review. diff --git a/docs/CHAT-DESIGN.txt b/docs/CHAT-DESIGN.txt index 7b8849c..d85ad44 100644 --- a/docs/CHAT-DESIGN.txt +++ b/docs/CHAT-DESIGN.txt @@ -16,9 +16,10 @@ along with this program. If not, see . PRIVATE CHAT DESIGN AND LIMITATIONS - Status: planned application; only the cryptographic compatibility probe - currently exists. This document is a specification, not a completion claim. - Work remains on feature/private-chat. Nothing is deployed to production. + Status: experimental local implementation on feature/private-chat. + CHAT-APPLICATION.txt records the implemented formats, behavior, commands, + tests, and current limitations. This document preserves the original design. + Nothing is deployed to production. USER EXPERIENCE -- cgit v1.2.3