
Terminal-based encrypted messenger with post-quantum cryptography, Double Ratchet protocol, and Tor anonymity. Features duress passphrase, deniable authentication, and OS-level hardening via seccomp-bpf and mlockall.
op4 — Secure Terminal Messenger
Op4 is a terminal-based encrypted messaging application written in Rust. It provides end-to-end encrypted private messaging with post-quantum cryptography, routed entirely through the Tor network so that neither the content of your messages nor your IP address is exposed to anyone — not even the person you are talking to.
op4 lets two people exchange private messages without either party revealing their IP address or real identity. Every message is:
op4 runs entirely in the terminal. It has no GUI, no browser component, and no cloud account. The only external process it contacts is the Tor daemon running on your own machine.
All download options — AppImage, source tarball, clone & build, and automated installer — are documented in the Downloads & Install guide. The latest release is available on the Releases page.
On Debian and Ubuntu, install/setup.sh handles everything in one command:
Rust toolchain, build dependencies, Tor, control port configuration, binary
compilation, system user, data directory, and AppArmor profile.
git clone https://github.com/Opfour/op4.git
cd op4
sudo bash install/setup.sh
After the script finishes, you must log out and log back in before running op4. The installer adds your user to the
debian-torgroup so it can read the Tor cookie file. Linux does not apply group changes to already-open sessions — a fresh login is required.Skipping this step will cause op4 to fail at startup with:
Permission denied reading /run/tor/control.authcookie
Then verify the source hash printed by the script matches the published release hash for your version before trusting the binary.
When op4 starts it prints a source hash covering all Rust source files,
Cargo.toml, Cargo.lock, and build.rs. Compare it against the value
below for the version you installed.
You can also check it without launching the full app:
op4 --print-hash
If the hash does not match, do not use the binary — it was either built from a different commit or has been tampered with.
Install dependencies first, then run the script:
Rust toolchain (pinned to 1.89.0 via rust-toolchain.toml):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
Tor and build dependencies:
# Fedora
sudo dnf install tor gcc pkg-config openssl-devel
# Arch
sudo pacman -S tor base-devel pkg-config openssl
Configure Tor control port — add to /etc/tor/torrc:
ControlPort 9051
CookieAuthentication 1
sudo systemctl restart tor
Add your user to the Tor group:
sudo usermod -aG tor $USER # Fedora / Arch
You must log out and log back in after this step. Group membership changes are not applied to active sessions. Until you do, op4 will fail with
Permission denied reading /run/tor/control.authcookie.To apply the change without a full logout, run:
newgrp tor
Build and install:
git clone https://github.com/Opfour/op4.git
cd op4
cargo build --release
sudo bash install/setup.sh
op4
# or, without system install:
./target/release/op4
On first launch op4 will guide you through setting a normal passphrase
and a duress passphrase, then generate your identity keys. Your vault is
stored at ~/.local/share/op4/vault.op4.
Minimum kernel: 4.15 (5.4+ recommended). Architecture: x86-64 (aarch64 should work but is untested).
Two people each need op4 installed, Tor running, and their vault unlocked. The exchange is asymmetric: one person sends their contact code first, the other adds it, then sends their first message, which arrives as a pending request that the first person accepts.
On first run op4 prompts for a normal passphrase and a duress
passphrase, then generates your identity keys. This only happens once.
Your vault is stored at ~/.local/share/op4/vault.op4.
$ op4
Your contact code contains your full public key bundle and your .onion
address. The other person needs this to reach you.
2 or →)e to export your contact codeop4:Your contact code is not secret. It is safe to share publicly. It contains only your public keys and onion address — no private material.
Once you have their contact code:
a to add a contactop4: contact code and press Enter↑/↓, then Enter)3 or →)This first message initiates the encrypted handshake and is delivered
to their .onion address over Tor. They will see it as a pending
contact request.
When someone sends you a first message, a badge appears on the Contacts tab showing how many requests are waiting.
2)p to review pending requestsEsc to reject and discard)Once accepted, the Double Ratchet is initialised and the conversation is immediately available in the Messages tab.
Before trusting a contact, confirm their fingerprint matches what they show on their own screen. This prevents a man-in-the-middle attack during the initial contact exchange.
A3F2:91BC:…If you believe your identity keys may be compromised:
4 or navigate right)r to rotate keysop4 uses a Double Ratchet protocol (similar to Signal Protocol) for forward secrecy. This means:
The KEM step combines X25519 and ML-KEM-768 as follows:
shared_secret = HKDF(X25519_ss || MLKEM_ss)
An attacker must break both algorithms to compromise the key exchange. This protects against a quantum adversary (ML-KEM-768) while remaining secure against classical attacks if ML-KEM-768 has an unknown flaw (X25519 fallback).
Messages are authenticated with HMAC-SHA256 using a key derived from the shared ratchet state. Because both parties hold the same HMAC key, either party could have produced any given MAC. This is the same deniability property used by OTR and Signal: messages cannot be cryptographically attributed to a specific sender in a court proceeding.
The vault file at ~/.local/share/op4/vault.op4 stores all contacts,
conversations, and identity keys. It is protected with two independent
Argon2id-derived keys:
op4 creates a Tor v3 hidden service for your inbox. Your .onion address
is derived deterministically from your identity key (via HKDF), so it is
stable across restarts without needing to store a separate key. Outbound
messages are sent through the Tor SOCKS5 proxy. Your real IP address never
appears in any network packet related to op4.
Cover traffic (Poisson-distributed dummy messages sent to self, mean interval 30 seconds) prevents a network observer from learning whether you are actively messaging someone by watching traffic volume.
apparmor/op4.profile) — restricts filesystem
access to only the vault directory, terminal devices, and Tor.┌─────────────────────────────────────────────────────────────┐
│ op4 process │
│ │
│ ┌──────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ TUI │ │ Double Ratchet │ │ Tor Transport │ │
│ │ (ratatui)│──▶│ + Hybrid PQ │──▶│ nym_client.rs │ │
│ │ │ │ Crypto │ │ │ │
│ └──────────┘ └──────────────────┘ └───────┬────────┘ │
│ │ │
│ ┌──────────────────────────────┐ │ │
│ │ Encrypted Vault │ SOCKS5 / control port │
│ │ ~/.local/share/op4/vault.op4│ │ │
│ └──────────────────────────────┘ │ │
└────────────────────────────────────────────────┼───────────┘
│
┌────────────▼────────────┐
│ Tor daemon │
│ 127.0.0.1:9050 (SOCKS) │
│ 127.0.0.1:9051 (ctrl) │
└────────────┬────────────┘
│
Tor network
│
┌────────────▼────────────┐
│ Peer's .onion address │
│ (their hidden service) │
└─────────────────────────┘
op4/
├── src/
│ ├── main.rs Entry point, startup sequence
│ ├── error.rs Unified error types
│ ├── crypto/
│ │ ├── keys.rs Hybrid KEM + signature keypairs
│ │ ├── primitives.rs AEAD, HKDF, HMAC, Argon2id
│ │ ├── ratchet.rs Double Ratchet implementation
│ │ ├── hmac_auth.rs Deniable authentication tags
│ │ └── handshake.rs Initial key agreement (X3DH-style)
│ ├── network/
│ │ ├── nym_client.rs Tor hidden-service transport
│ │ └── message.rs Wire message format + padding
│ ├── storage/
│ │ └── vault.rs Encrypted vault (Argon2id + AEAD)
│ ├── identity/
│ │ ├── profile.rs Contact codes, stored contacts
│ │ └── revocation.rs Key revocation records
│ ├── hardening/
│ │ ├── memory.rs mlockall, RLIMIT_CORE, dumpable
│ │ └── seccomp.rs seccomp-bpf syscall filter
│ └── ui/
│ ├── app.rs TUI event loop and state machine
│ ├── contacts.rs Contacts tab rendering
│ ├── conversation.rs Messages tab rendering
│ ├── settings.rs Settings tab rendering
│ ├── duress.rs Duress inbox rendering
│ ├── input.rs Input sanitization (CSI/OSC strip)
│ └── passphrase.rs Secure passphrase prompts
├── apparmor/
│ └── op4.profile AppArmor MAC profile
├── install/
│ └── setup.sh System installation script
├── build.rs Embeds source hash at compile time
├── deny.toml cargo-deny licence + advisory rules
├── rust-toolchain.toml Pins Rust 1.89.0
└── docs/ This documentation
Version: 0.2.0-dev (pre-release)
op4 is under active development. The following layers are complete and tested:
All layers are now wired end-to-end:
PublicKeyBundle
(your full public key set + onion address).All known limitations have been resolved. The application is now at feature-complete status for 0.2.0-dev:
HMAC deniable authentication is fully wired. Every outbound data
message carries an HMAC-SHA256 tag computed from the per-message ratchet
key over (conversation_id || message_counter || ciphertext). Inbound
messages are verified before being accepted; zero-filled tags from older
peers are tolerated for backward compatibility.
Message history persists across restarts. The full conversation log
is encrypted with a per-conversation HKDF-derived key and stored in the
vault's message_log_ct field. Messages are loaded from the vault when
a conversation is opened and written back after each send or receive.
Inbound contact requests from unknown parties are queued rather than
dropped. The Contacts tab shows a badge when requests are waiting.
Press [p] to review: you see the sender's fingerprint and their first
message, type a name, and press Enter to accept (or Esc to reject). On
acceptance the contact is added, the Double Ratchet is initialised, and
the initial message is saved to the vault.
Duress vault preserved across saves. The vault file format (v2) stores
exact ciphertext lengths in the header so AEAD decryption operates on the
real bytes rather than zero-padded sections. The encrypted duress section
is stored verbatim on every save() call, keeping the duress passphrase
valid indefinitely.
Dedicated Double Ratchet bootstrap key. A separate X25519 keypair
(identity_ratchet_secret) is generated on first run and included in the
contact code as ratchet_pub. Alice's ratchet is initialised with Bob's
ratchet_pub rather than his KEM identity key, separating key roles.
| Version | Source hash |
|---|
0.3.0 | 80820cb41a63575d2c139dadd425d13d1e87e62a9d60200ae7b894ae2e9ad8ed |
0.3.1 | 48115efb12747fa78b627ddbf7a56c46169f59e777d7d7508941bf89e4fe7521 |
0.2.0-dev | 35740577f6c4a4f19c5a08fe85b1f78a10347f2ba9dd7642d126552266bfa5a5 |
0.1.0 | e1a94761c7d3fa589ba892b47d5295aa417f95aee126809d51a7e7fb7e78982c |
| Distro | Status |
|---|
| Ubuntu 22.04 / 24.04 | Supported |
| Debian 12 | Supported |
| Fedora 39+ | Supported |
| Arch Linux (current) | Supported |
| Tails OS | Supported (setup guide) |
| macOS / Windows / WSL1 | Not supported |
| Key | Action |
|---|
1 / ← → | Switch tabs (Contacts / Messages / Settings) |
↑ ↓ | Move selection |
Enter | Open conversation / confirm |
Esc | Cancel / go back |
e | Export your contact code (Contacts tab) |
a | Add a contact (Contacts tab) |
p | Review pending requests (Contacts tab) |
d | Delete selected contact (Contacts tab) |
r | Rotate your keys (Settings tab) |
q | Quit |
| Purpose | Algorithm |
|---|
| Vault key derivation | Argon2id (m=64 MiB, t=3, p=1) |
| Vault encryption | ChaCha20-Poly1305 (256-bit key, 96-bit nonce) |
| Message encryption | ChaCha20-Poly1305 (per-message key from ratchet) |
| Key derivation (ratchet) | HKDF-SHA256 |
| Deniable authentication | HMAC-SHA256 |
| Classical key exchange | X25519 |
| Post-quantum key exchange | ML-KEM-768 (FIPS 203) |
| Classical signatures | Ed25519 |
| Post-quantum signatures | ML-DSA-65 (FIPS 204) |
| Transport anonymity | Tor v3 hidden services (.onion) |
Settings tab fully functional. Tor SOCKS5 address and auto-delete threshold can be edited inline. Key rotation (generates new keypair, broadcasts a signed revocation certificate to all contacts, refreshes the export code) and key retirement revocation are both wired and operational.
Key revocation wired end-to-end. RevocationCertificate structs are
signed with the hybrid Ed25519+ML-DSA-65 keypair and sent to all contacts
as WireMessageType::Revocation messages over Tor.