summaryrefslogtreecommitdiff
path: root/docs/CHAT-APPLICATION.txt
diff options
context:
space:
mode:
Diffstat (limited to 'docs/CHAT-APPLICATION.txt')
-rw-r--r--docs/CHAT-APPLICATION.txt403
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.