Skip to content
rmwahidPublic

About

Senyum. Sapa. Scan. Privacy-first chat with no accounts and no cloud storage - one X25519 keypair, one QR handshake, end-to-end encrypted through a deliberately dumb relay.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TriSno

Flutter Dart Bun TypeScript Redis License: MIT

Senyum. Sapa. Scan. Satu identitas, komunikasi privat. · One identity, private communication.

Satu kali scan, seumur hidup kenal.

TriSno adalah aplikasi chat privacy-first tanpa akun, tanpa cloud storage, tanpa identitas tradisional. Tidak ada email, tidak ada nomor HP, tidak ada username. Yang ada hanya satu pasangan kunci X25519 yang lahir di perangkat Anda dan tidak pernah keluar dari sana. Temukan seseorang, senyum, sapa, lalu saling scan QR. Selesai, mulai chatting.

Nama TriSno punya tiga lapis makna, semuanya disengaja: Tri-S (Senyum, Sapa, Scan), "No No No" (no accounts, no cloud storage, no key leak), dan trisno dalam bahasa Jawa/Kawi yang berarti cinta, kasih sayang - mendekat tanpa identitas.

Dokumentasi lengkap (bahasa Inggris) ada di bawah.


English

What is TriSno?

TriSno is a privacy-first chat application built on one principle that overrides everything else: your identity belongs to you, and to no one else.

There are no accounts. No email signup. No phone number. Your identity is a cryptographic keypair generated on your device the first time you launch the app:

  • The private key never leaves your device. It lives in the hardware-backed keystore and cannot be exported.
  • The public key is who you are. Sharing it is a QR scan away - in person, face to face.
  • Messages are end-to-end encrypted. Sealed box (ephemeral X25519 + AES-256-GCM); the relay only ever sees opaque bytes.
  • Nothing lives in the cloud. Chat history stays in your local, encrypted database.
  • The relay is deliberately dumb. It holds encrypted envelopes in Redis for at most 24 hours, then delivers or drops them. It cannot read your messages even if compelled to.

To start a conversation you don't "add a friend" or "send an invite". You meet a person, face to face. You tap Show My QR. They tap Scan. The cryptographic handshake itself is the introduction. After that, you can talk for as long as either of you keeps the keypair.

Why "TriSno"

Reading Meaning
Tri = three The three founding "S" principles: Senyum (smile), Sapa (greet), Scan (scan). That is the entire social protocol.
"No No No" Three refusals that define the app: no accounts, no cloud storage, no key ever leaves your device.
Trisno (Javanese / Kawi) From Sanskrit tṛṣṇā, the word trisno means love, affection. You can be close to someone - really know them, really care - without surrendering your identity to a platform. TriSno rebuilds that old idea for the digital age: draw near without exposing who you are.

Features

  • No accounts - identity is a cryptographic keypair, not an email or phone number
  • End-to-end encryption - sealed box (ephemeral X25519 + AES-256-GCM) on every message
  • QR identity exchange - obfuscated, expiring QR codes (1-7 days) carry your public key and relay URL
  • Mandatory PIN - app lock on every launch; the local database master key is derived from it
  • Local-only history - messages stored encrypted (AES-256-GCM) in SQLite on your device
  • Deliberately dumb relay - stateless, Redis TTL only (24h max), zero persistence, nothing to seize
  • Self-hostable - anyone can run their own relay; the client honors whatever relay URL the QR carries
  • Encrypted backup - manual export/import of identity + history, protected by your password
  • Broadcast - one-to-many without group state (each recipient gets their own sealed copy)
  • ntfy push (Android) - self-hosted push with a silent flag, no FCM, no Google dependency
  • Disconnect & regenerate - per-contact soft disconnect, or regenerate your identity entirely

How it works

   Phone A                                     Phone B
  +---------------------+          +---------------------+
  | local encrypted DB  |          | local encrypted DB  |
  | private key (keystore)          | private key (keystore)
  +----------+----------+          +----------+----------+
             |        sealed box (X25519 + AES-256-GCM)       |
             +--------------------+-------------------+
                                  |
                      Relay (deliberately dumb)
                   WebSocket + Redis TTL (24h max)
           sees only recipient hash + opaque ciphertext
  1. Install, generate an X25519 keypair on the device
  2. The private key goes to the hardware-backed keystore - it cannot be exported
  3. The public key + a short ID (XXXX-XXXX) become your identity
  4. Show your QR (obfuscated, valid 1-7 days); your friend scans it, you scan theirs
  5. Chat: plaintext is encrypted on your device with their public key
  6. The relay forwards opaque bytes - instant when online, queued up to 24h when offline
  7. Your device decrypts and stores the message encrypted at rest (key derived from your PIN)

Plaintext exists only briefly in RAM. Never on disk unencrypted, never on the server.

Screenshots

Coming soon - app screenshots and a demo GIF.

Quick Start

Prerequisites: Flutter 3.22+, Bun 1.x, and Redis 7+ (Docker or local).

1. Run the relay

git clone https://github.com/rmwahid/TriSno.git
cd TriSno/backend
bun install

# Environment: defaults work for local development
cp .env.example .env
#   - REDIS_URL: redis://localhost:6379 (default)
#   - or decrypt the team's SOPS-encrypted env if you hold the age key:
#     bun run decrypt-env     (requires SOPS_AGE_KEY_FILE)

bun run dev                 # ws://localhost:8080/ws

With Docker Compose (relay + Redis together):

cd TriSno/backend
docker compose up

2. Run the mobile app

cd TriSno/mobile
flutter pub get
flutter run                 # Android device/emulator
# or build an APK for sideloading:
flutter build apk --release # build/app/outputs/flutter-apk/app-release.apk

3. First conversation

  1. Open the app, set your PIN - your identity keypair is generated on first launch
  2. Tap Show My QR and pick a lifetime (1-7 days)
  3. Your friend installs the app and taps Scan
  4. Chat. That's it - no signup, and no server keeps your messages

The web/ folder is a static landing page for APK distribution - serve it with any static file server.

Tests & load bench (backend):

cd TriSno/backend
bun test                    # unit + e2e; real-Redis integration runs when Redis is reachable
bun run bench:quick         # small load test

Tech Stack

Layer Tech
Mobile client Flutter, Riverpod, Go Router, cryptography (X25519, AES-GCM), sqflite, flutter_secure_storage, qr_flutter, mobile_scanner
Relay server Bun + TypeScript ESM, ws, ioredis - zero framework, zero database
Push (Android) ntfy self-hosted - no FCM, no Google dependency
Push (iOS) deferred until an Apple Developer account is available
Distribution APK sideload via the static landing page - no app store required

Project Structure

.
├── backend/                # Relay server (Bun + TypeScript): stateless, horizontally scalable
│   ├── src/
│   │   ├── constants/      # single source of truth: limits, error codes, protocol, redis keys
│   │   ├── lib/            # HTTP error/envelope helpers, stats
│   │   ├── modules/        # frames, queue (atomic Lua drain), presence, pub/sub, ratelimit, delivery
│   │   ├── routes/         # minimal REST (health, presence)
│   │   └── ws/             # WebSocket handler
│   ├── tests/              # bun test: unit + real-Redis integration + e2e (57 tests)
│   └── bench/              # load test harness (fanout / offline-flood / connections / broadcast)
├── mobile/                 # Flutter client
│   └── lib/
│       ├── core/           # crypto, db, keystore, network
│       └── features/       # onboarding, pin, identity, contacts, chat, broadcast, backup, settings
└── web/                    # static landing page for APK distribution

Security & Privacy Model

Two encryption layers, two different threats:

Layer When Key Protects against
E2E (in transit) Before the message leaves your device Recipient's X25519 public key (sealed box) The relay, network attackers, ISPs
At rest (local) After your device decrypts Master key derived from your PIN (PBKDF2 + salt) Device theft, forensic extraction

What the relay knows: a recipient hash and opaque bytes, for at most 24 hours. It never logs content, never persists to disk, and cannot decrypt - there is no "us" to reset anything. If a relay is seized, the seizure yields encrypted envelopes with TTL. If the ISP is tapped, the tap captures encrypted frames. If a phone is confiscated without the PIN, the database holds AES-256-GCM ciphertext.

That makes TriSno useful beyond casual chat: secure coordination for protests, demonstrations, and sensitive communication where contents must not be readable by third parties, including state actors. This is not about hiding wrongdoing - it is about treating privacy as a default right rather than a privilege you negotiate for.

Trade-offs, by design:

  • Losing your PIN with no backup means losing your identity - there is deliberately no recovery path, no "reset via email".
  • QR encryption is obfuscation against casual scanners, not secrecy; the real security boundary is the end-to-end encryption of the messages themselves.
  • Forward secrecy (Signal Protocol / double ratchet) is planned but not in the MVP - each message is independently sealed.

Roadmap

  • Identity keypair generation + hardware-backed keystore
  • Mandatory PIN (setup, unlock, cooldown, wipe after repeated failures)
  • QR identity exchange (obfuscated payload, 1-7 day expiry, duplicate-contact conflict resolution)
  • 1-on-1 end-to-end encrypted chat (sealed box)
  • Local encrypted database (AES-256-GCM message bodies)
  • Stateless relay: WebSocket + Redis TTL (24h), offline delivery
  • Atomic batched queue drain (Lua) - no duplicate delivery, even across instances
  • Horizontal scaling: cross-instance delivery via Redis pub/sub fanout
  • Rate limiting, backpressure, connection caps, graceful shutdown
  • Broadcast to up to 10 recipients
  • Soft disconnect (per contact) + regenerate identity (global reset)
  • Encrypted backup export/import
  • ntfy push on Android (silent flag; iOS deferred)
  • Test suite (unit + real-Redis integration + e2e) and a load-test harness
  • Signal Protocol (X3DH + Double Ratchet) for forward secrecy
  • Argon2id + SQLCipher hardening for the local database
  • Multi-device pairing
  • Key rotation
  • Multi-relay fallback
  • Typing indicator + read receipts (opt-in only)
  • Group chat via MLS
  • Voice notes (Opus + E2E)
  • File / media sharing (E2E attachments)
  • Web PWA companion
  • Cross-relay federation
  • iOS push (blocked on an Apple Developer account)
  • App Store / Play Store distribution (APK sideload via landing page for now)

Contributing

MIT-licensed and open to contributions. The relay is deliberately small and dependency-free - keep it that way. Ideas, issues, and pull requests are welcome; code comments and UI text are kept in English.

License

MIT - for both the relay and the client. (File added on first public release.)

About

Senyum. Sapa. Scan. Privacy-first chat with no accounts and no cloud storage - one X25519 keypair, one QR handshake, end-to-end encrypted through a deliberately dumb relay.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages