summaryrefslogtreecommitdiff
path: root/docs/CHAT-APPLICATION.txt
blob: 388c677509abf4ad1993e41c1b60a06cd108749f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
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.