Metadata-Version: 2.4
Name: dark-chat
Version: 3.2.0
Summary: Minimal encrypted terminal chat with private offline mailboxes
Project-URL: Repository, https://github.com/iZubayr/dark-terminal-chat
Project-URL: Issues, https://github.com/iZubayr/dark-terminal-chat/issues
Classifier: Programming Language :: Python :: 3
Classifier: Environment :: Console
Classifier: Operating System :: OS Independent
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: cryptography<50,>=44
Requires-Dist: websockets<18,>=15
Requires-Dist: prompt-toolkit<4,>=3.0.48
Requires-Dist: PyNaCl<2,>=1.6.2

# Dark Terminal Chat

Plain English terminal chat. No banner, logo, animation, or forced color. Python 3.10+.

~~~sh
pip install --upgrade dark-chat
dark-chat
~~~

[iPhone / iSH installation and login](docs/ish.md).

## Temporary chat

~~~sh
dark-chat --new
dark-chat --chat
~~~

The creator gets a secret invite code. Share it privately with the other person; they enter it at the prompt. Both commands use the hosted relay automatically. Each room has two slots. Temporary messages are never saved or queued. A disconnected participant has 60 seconds to reconnect. When both participants leave, or the server restarts, the old invite stops working.

## Chats and offline messages

Both people start on their own devices:

~~~sh
dark-chat
~~~

On the first launch, choose your account password (at least 12 characters) and a username. Usernames have 3-24 letters, numbers, dots, dashes or underscores and start with a letter. Later launches ask only for your account password. Existing accounts keep their keys and conversations.

The app gives you a separate **contact password**. Share your username and contact password privately with people who should be able to start a chat with you. `/profile` shows both again. Keep your account password private; it unlocks your identity and is never needed by a friend. If a username is taken, choose another with `/profile YOUR_USERNAME`.

To start a new conversation:

~~~text
/chat alice
Contact password for @alice: ************
Hello.
~~~

The contact password is hidden while typing. After the first introduction, `/chat alice` opens the saved conversation without asking again. Your friend must have opened the app at least once but can be offline when you write.

To open an existing conversation, type `/chat`, then enter its number:

~~~text
/chat
1. alice (2 new)
2. bob
Number or username: 1
~~~

`/chat 1` opens it directly. `/chat alice` also opens a received conversation when its displayed name is unique. If several chats share a name, choose a number. Incoming messages show the short command to open their conversation. Unread counts stay saved until you open the chat. Enter a new username in the same menu to start another chat, or use `/cancel` to go back. After typing `/chat `, the arrow keys can select a suggested saved contact.

Long IDs remain available through `/id` and `/chat FULL_ID [local_name]` for manual fingerprint verification and older contacts. They are no longer part of the normal chat flow. Names shown on incoming messages are display labels; verify a new contact using their privately shared username/password or full fingerprint. If moving your identity to another device, restore its backup first.

Outgoing messages are saved encrypted on your device before sending. `Queued.` means the local outbox accepted the message; it is not a read receipt. The outbox retries automatically in order. A full mailbox pauses that contact's queue while other conversations continue. The relay keeps encrypted messages until the recipient has saved and acknowledged them, for at most 30 days. Unconfirmed outgoing messages older than 30 days stop retrying and appear as `[unconfirmed: expired]` in local history; copy and resend their text if needed. They do not block newer messages. The last 50 locally saved messages appear when opening a conversation. `/history` shows them again.

The password unlocks this device's identity; it is never sent to the server. Logging in on another computer requires your encrypted identity backup as well as the password. Use one active device per identity; multi-device history synchronization is not provided.

## Commands

Type `/` to display the command menu. Choose with the arrow keys and press Enter. `/clean` is the first option; `/` followed by Enter selects it. Typing `clean` also works.

| Command | Action |
| --- | --- |
| `/clean` or `clean` | Close the chat and clear the terminal screen and supported scrollback |
| `/exit` or Ctrl+C | Close the chat |
| `/clear` | Delete the selected conversation's history for both people and stay in the chat |
| `/help` | Show commands |
| `/who` | Show temporary-room participants |
| `/chat ID [name]` | Open a permanent conversation; optionally name the contact locally |
| `/chat username` | Open a saved chat, or enter the contact password for a new one |
| `/chat` or `/chat number` | Choose a chat or open one by its number |
| `/contacts` | List chats and unread counts |
| `/profile [username]` | Show your username/contact password; optionally change the username |
| `/cancel` | Cancel chat selection or password entry |
| `/id` | Show your permanent ID |
| `/history` | Show the selected conversation's recent saved messages |
| `/backup` | Create an encrypted identity backup and print its path |

`/clean` exits dark-chat and clears the terminal; saved conversations remain. `/exit` closes without clearing the window. The command menu uses the terminal's normal colors and a simple selection highlight.

In permanent chat, `/clear` deletes the selected chat's saved history and queued messages on both clients. Contacts stay available, so you can keep chatting. If either person is offline, the signed deletion is saved and applied on reconnection before old messages can be retried. Both clients and the relay need version 3.2.0 or later. Older clients must update after a chat is cleared. Temporary chat has no saved history, so its `/clear` only clears the window.

Clearing cannot erase separate backups, terminal recordings, screenshots, modified third-party clients or operating-system memory. Scrollback clearing depends on the terminal; it is not secure forensic erasure.

## Identity backup and recovery

While logged in, run `/backup`. Move the resulting encrypted file to a safe location on another device. It contains private keys, encrypted with your existing password. It does not contain conversation history. Anyone with both the backup and password can use your identity and decrypt messages addressed to it.

On a new device:

~~~sh
dark-chat --restore /path/to/identity-backup.json
dark-chat --login
~~~

Restore refuses to overwrite an existing identity. There is no server-side password reset or private-key recovery. For a complete local backup, close all clients and copy the whole `~/.dark-chat` directory. History is encrypted and tied to the identity. `DARK_CHAT_HOME` selects a different profile directory.

## Security model

Permanent messages use [libsodium sealed boxes through PyNaCl](https://pynacl.readthedocs.io/en/latest/public/#nacl-public-sealedbox) and Ed25519 signatures. Clients verify the sender's key fingerprint and signature and the intended recipient. Authentication signs a fresh server challenge; old responses cannot log in again. Message IDs and durable acknowledgments suppress retries and replay. The server never receives private keys, account passwords, contact passwords, local contact aliases, or decrypted message text.

Each generated contact password has 72 random bits. The username and password derive an AES-256-GCM key that protects the public contact card; the relay stores the encrypted card and a one-way lookup token. Clients decrypt and authenticate the card before saving the keys. Share the contact password through a trusted channel. Anyone who has it can resolve that contact's keys; changing a username removes its old directory entry but does not revoke saved conversations. The relay sees usernames and directory lookups. Your own contact credentials are encrypted in the local history database; copy the entire profile directory to preserve them alongside history.

The identity file uses AES-256-GCM with a random salt/nonce and scrypt (`N=131072, r=8, p=1`) to protect against offline password guessing. Local message bodies and contact names use AES-256-GCM under an independent random storage key kept inside the encrypted identity. Files are owner-only on Unix; Windows uses the profile directory's inherited access permissions, plus encryption. Names/text received from peers are stripped of terminal control characters.

Temporary rooms continue to use AES-256-GCM with keys derived from their random 32-byte invite codes. A shared invite grants room access; it is not a permanent identity. Version 3 supports version 2 temporary clients. Permanent accounts require a version 3 server. Username introductions require both client and relay version 3.2.0 or later. Saved conversations and manual ID lookup continue to work with older version 3 relays.

Public Internet connections require verified WSS/TLS. Account mode refuses insecure remote/LAN connections even if `--allow-insecure` is supplied. Local loopback WS is available for development.

The relay sees IP addresses, public IDs/keys, sender/recipient relationships, timing, sizes, and delivery metadata. The local database exposes contact IDs and message metadata, although bodies and names are encrypted. There is no anonymity guarantee, forward secrecy against later compromise of a recipient's long-term private key, independent security audit, or protection from malware on an unlocked device. Sealed messages are signed, so they are not deniable. Terminal clearing does not change these limits.

## Hosting

The default relay is `wss://zubayr.alwaysdata.net/dark-chat/ws`. Advanced options remain available but are hidden from the short help: `--server`, `--name`, `--ca`, `--allow-insecure`, `--password-file`, and `--restore`. `DARK_CHAT_SERVER` overrides the relay; `DARK_CHAT_CODE` provides a temporary invite for automation. `--password-file` reads a protected local file for automation; do not put passwords directly on a command line.

~~~sh
dark-chat-server --database /persistent/private/path/mailbox.db
dark-chat --server ws://127.0.0.1:8080/ws --new
~~~

Run one relay process. Its default database is `~/.dark-chat-server/mailbox.db`, outside release directories; `DARK_CHAT_DATABASE` overrides it. Never put it in `.deploy/releases`. The server keeps ciphertext only and bounds storage to 1,000 identities, 1,000 pending messages overall, 100 pending messages per recipient, and 10,000 pending-message/delivery-receipt rows. A full mailbox returns an error while the encrypted outbox stays on the sender's device. Delivered ciphertext is deleted after acknowledgment; replay receipts expire after 30 days. Pending mail and public identities survive normal restarts and upgrades. Availability and backups of the server database remain the operator's responsibility.

[AlwaysData setup and automatic updates](docs/alwaysdata.md). [Package distribution](docs/distribution.md).

## Development

~~~sh
python -m venv .venv
python -m pip install -e . build twine
python -m unittest discover -s tests -v
python -m build
python -m twine check dist/*
~~~

Activate the virtual environment before installing. GitHub Actions tests installed wheels on Linux (Python 3.10 and 3.12) and Windows (Python 3.12). Only passing commits advance the deployment branch; AlwaysData tests the candidate again before activation. Tests cover offline delivery, database persistence, signatures, challenge replay, recipient isolation, key substitution, local encryption, CLI flows, and slash completion. A local test run does not establish live production behavior.

The earlier Node.js experiment is reference source only. The Python package is the application.
