diff options
Diffstat (limited to 'docs/CHAT-APPLICATION.txt')
| -rw-r--r-- | docs/CHAT-APPLICATION.txt | 403 |
1 files changed, 403 insertions, 0 deletions
diff --git a/docs/CHAT-APPLICATION.txt b/docs/CHAT-APPLICATION.txt new file mode 100644 index 0000000..388c677 --- /dev/null +++ b/docs/CHAT-APPLICATION.txt @@ -0,0 +1,403 @@ +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/>. + + +EXPERIMENTAL PRIVATE CHAT + + Available on main: direct development and prebuilt Podman/HTTPS deployment. + See CHAT-PRODUCTION.txt for release, setup, and recovery. Feature work does + not merge main or activate the VPS. Experimental; no independent messenger audit. + +WHAT EXISTS + + Rust HTTP server and Rust/WASI cryptography. Strict TypeScript compiles the + interface and capability-limited WASI host to small, readable, free classic + JavaScript. See TYPESCRIPT.txt for build/licensing. Text uses textContent. + Existing services, including SearXNG, are unchanged. + + Crypto download shows an ASCII bar, percentage, and received/total MiB; + unknown totals show received MiB. A 45-second stall aborts with reload + instructions; slow transfers continue while bytes arrive. Chat connection has + a separate status. No browser cache or storage was added. + + CREATE NEW CHAT generates a purpose-specific identity locally and downloads + its protected file. Reopen that exact file with its password before first + authentication/message. OPEN CHAT FILE restores access on later visits. + No username/password account; messages never require updating the stable file. + The owner has a separate identity/inbox. The canonical RSA OpenPGP key does + not encrypt chat. + + History defaults to zero messages. Authenticated counters show total, sent, + and received messages. Choose 0 to N recent messages, where N is the current + conversation total, or page backwards for older messages. Counts refresh when + the history input is focused, history is requested, or a send is acknowledged. + Large selections are fetched and verified in batches of at most 50; that is + a per-request resource bound, not a user-visible 50-message selection limit. + 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 at the repository root on main. Native development needs Rust, libclang, + OpenSSL >=3.5 development libraries, C compiler, pkg-config, make, Perl, curl, + and tar. Checked OpenSSL: 3.6.5. WASI builds target x86_64 GNU/Linux and download + hash-verified WASI SDK 34.0/OpenSSL sources. Toolchains stay off the VPS. + + 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 privately creates/appends CHAT_SETUP_TOKEN and CHAT_ORIGIN + in the single root .env, preserving an existing token and SearXNG settings. + Root .env.example lists both; never commit .env or create crate-specific files. + CHAT_BIND and public navigation use the shared loader; see CONFIGURATION.txt. + + For Rust-only edits, sh infra/chat/build-browser.sh reuses verified OpenSSL/SDK + inputs. Source/version/configuration/flag changes require an OpenSSL rebuild. + WASI linear memory is capped at 256 MiB. + + Start: + + cargo stack chat + + Open http://127.0.0.1:9080/ and choose owner-only OWNER SETUP, + or use http://127.0.0.1:9080/#setup. + + Read CHAT_SETUP_TOKEN from root .env locally; paste into setup. Choose a + display name and strong unique password; save, reopen, and unlock the file. + This registers the owner public identity and disables token-based setup. + The token grants neither login nor key recovery. OWNER SETUP remains visible + with existing-file/reset instructions; it never silently replaces a key. + Never publish private file/password. Keep both in KeePassXC with a separately + protected database backup. + + Create a visitor chat in another window at http://127.0.0.1:9080/. + Identity stays in each page's memory; browser login is not shared. Control-C + stops development. Restart invalidates sessions, preserving identities, + conversations, counts, and ciphertext. + + Direct development requires loopback and an exactly matching bind/origin. + Production uses a private listener behind Caddy HTTPS, with explicit + CHAT_HOST/HTTPS_PORT origin; forwarded headers cannot choose it. Never expose + direct development through a public proxy. + +OWNER FILE LOSS AND BACKEND RESET + + Setup tokens grant registration, not private-key recovery. A new identity + cannot decrypt old messages if the owner file/backups are lost. Reset clears + all messages, encrypted profiles, conversations, counters, and owner registration. + Root .env and exports survive. This does not forensically erase backups, + snapshots, or OS copies. + + Run development commands at the stack root. Reset has no HTTP API; private + chat-directory access is the administrative boundary. Production runs + ./bin/stack chat --owner-fingerprint and ./bin/stack chat --reset-owner in an + isolated maintenance container. First stop with ./bin/stack down; restart + with ./bin/stack up --production. No VPS Rust required. Read CHAT-PRODUCTION.txt + before destructive reset. + + Read the current public fingerprint, even if you have lost the private file: + + cargo stack chat --owner-fingerprint + + Stop the chat server with Control-C in its terminal, then explicitly pass + that full fingerprint to the destructive reset command: + + cargo stack chat --reset-owner PASTE_CURRENT_OWNER_FINGERPRINT_HERE + + Mismatched fingerprints refuse without deletion. Server/reset share a lock; + direct development also requires a free loopback service port, protecting + older running processes without the lock. Online snapshots and fingerprint + reads remain available during operation. + + Restart: + + cargo stack chat + + Use OWNER SETUP and existing root .env CHAT_SETUP_TOKEN; generate, save, and + reopen a fresh file. Back up file/password in KeePassXC. Visitors also need + fresh files: old owner-key pins correctly reject replacement. Reset cannot + prove continuity with the lost key. + + An owner file found later may recover old conversations from an encrypted + backup in isolation. Reset retains exports and never substitutes new keys + into old ciphertext or visitor files. + +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. + + New identity JSON uses indentation and 80-character Base64 ciphertext chunks; + the last may be shorter. JSON punctuation/indentation adds width. Readers join + chunks before decoding. Private fields stay encrypted. This application envelope + is not OpenPGP ASCII armor. Its identifier is authenticated as associated data: + + stack-chat-file/v2;argon2id-m65536-t3-p1;aes256-gcm + + Existing compact v1 files with one ciphertext string remain supported: + + stack-chat-file/v1;argon2id-m65536-t3-p1;aes256-gcm + + v1 retains its original associated data. The tag binds versions: relabeling + or reshaping fields cannot convert files. Existing files need no rewrite; + old readers reject v2. Algorithms/KDF parameters stay fixed. + + 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. + + Unlock checks exact origin and owner pin. Owner creation pins its own public + certificate; visitors pin the certificate advertised over the trusted origin + connection. First visits still trust served application/TLS, without 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. + + Transactional sends deduplicate exact ciphertext/message-ID/sender/conversation + retries, returning the original sequence/counts. Reused IDs with changed data + are rejected. This handles lost responses after storage. Keep the page unlocked + to retry; reload loses pending ciphertext/draft, so inspect history before + resending. Retries are deliberate, never automatic. + + Limits: 8192 UTF-8 bytes per text, 16384 bytes per signed payload, 65536 base64 + characters per stored ciphertext, 50 history rows per API 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, in-place key rotation UI, blocklist, or password-change UI are + implemented. Lost-owner recovery uses the explicit backend reset above. + +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. + + Malicious servers can replace JavaScript/WASM and steal passwords/plaintext + on the next visit; key pins and LibreJS cannot prevent it. Shared-host/OS + compromise, extensions, browser bugs, dependency attacks, malicious friends, + and denial of service remain risks. Purpose labels cannot prevent key export + or reuse. This is not a SimpleX equivalent. + +BACKUP, EXPORT, AND RESTORE + + cargo stack backup uses SQLite online backups, not ordinary live database/ + journal copies. chat and chat-local get separate consistent snapshots while + running; chat stays unpaused. Other Podman services retain pause/copy/resume. + Chat snapshots are atomic, without a cross-service transaction. Exports + include root .env and must stay 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; destination must not exist. Standalone + snapshots contain ciphertext/public metadata, without full-stack GPG encryption. + Encrypt before off-machine storage. Browser private identities are absent; + server backups cannot recover lost identity files/passwords. + + 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 an encrypted backup off-VPS. After recovery, test owner/visitor identities, + counts, and decryption. Keep private files/passwords in KeePassXC with a protected + database backup, never source control/public archives. Local deletion or file + loss does not delete stored ciphertext, exports, or participants' copies. + +INTERFACE AND SITE STYLE + + web/styles.css supplies red ASCII logo, 80-column desktop width, black/white + light/dark themes, blue links, green prompts/actions, ASCII frames, and bracketed + controls. License links retain amber/muted green. Yellow LibreJS badge has a + keyboard-accessible warning; JavaScript licenses sit below it. Small screens + retain readable controls and fit ASCII rules without shrinking all text. + Root viewport scrolls at the screen edge; no inner logo scrolling. + No remote fonts, images, or additional scripts. + +LICENSES, SOURCE, AND VERIFICATION + + Bundled SQLite snapshots require a musl C compiler for portable CLI builds. + The root CLI ran as x86_64 static PIE; VPS SQLite packages are unnecessary. + Local checks compiled C with Alpine musl 1.2.6 headers/host compiler and + linked with Rust's self-contained musl runtime. + + The rendered dependency-license page is centralized on the main website at + https://doan.luuduct.ai/THIRD-PARTY-LICENSES.html. Chat links point there; its + previous local license-page URL redirects there. cgit keeps ordinary source + and raw-file views without a custom HTML renderer. + + 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, TypeScript sources/configurations/npm lock, 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" npm run test:crypto + PLAYWRIGHT_BROWSERS_PATH="$PWD/.tools/browsers" npm run test:chat + PLAYWRIGHT_BROWSERS_PATH="$PWD/.tools/browsers" npm run test:progress + + Automation needs Node/root locked Playwright, with browsers in .tools/browsers; + see TYPESCRIPT.txt for installation/strict builds. Releases exclude these tools. + Fixtures isolate roots with one temporary .env each, deleting private data on + completion. Default loopback 9081 (CHAT_TEST_PORT overrides) leaves preview 9080 + free. Packaged HTTPS tests retain disposable roots; see CHAT-PRODUCTION.txt. + Tests neither initialize the real owner nor touch the VPS. + + 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 + + LibreJS rejected a combined GNU prefix/or-later suffix. The table uses recognized + GNU-AGPL-3.0; prose/legal headers retain version-3-or-later. Readable classic + scripts/workers avoid module-script interception requirements. This tests + license recognition/operation, not delivered-code safety. + + 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, loading all + 54 fixture messages through 50+4 batches, pagination, + lost-response retry deduplication, reload, explicit lock, origin rejection, + live database snapshot restoration with both original identity files, owner + setup/access controls, refusing reset while running or with a mismatched + fingerprint, successful backend reset and fresh registration, rejecting old + visitor key pins, in-flight lock, back/forward restoration, and checks for + plaintext persistence in the database/journals or private credential uploads. + The packaged variant additionally verifies certificate trust/hostname through + curl, restrictive export permissions, and full encrypted recovery with saved + images and no build directories inside the recovered installation. + Native tests cover authorization, challenge reuse, expiry, locked sessions, + quotas, idempotency, and SQLite snapshot recovery. The implemented design and + limits are documented here. CHAT-OPENSSL-MIGRATION.txt records the backend review. |
