aboutsummaryrefslogtreecommitdiff
path: root/docs/CHAT-APPLICATION.txt
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 21:39:57 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 21:39:57 -0500
commit556eaa133e9cfe7f4c8e542708119fdfd311357e (patch)
tree57ae2a9c5455f335e8c34b293f4f13975cb3d09e /docs/CHAT-APPLICATION.txt
parent143880db086c617b843340ca7023a879507761f8 (diff)
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.
Diffstat (limited to 'docs/CHAT-APPLICATION.txt')
-rw-r--r--docs/CHAT-APPLICATION.txt336
1 files changed, 336 insertions, 0 deletions
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 <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
+
+ 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.