
Why another messenger
The usual answer is a slogan. Let me answer as an engineer instead — through what actually breaks in existing solutions.
One: the center. Telegram, WhatsApp and Signal all have an organization that can be served with a demand, and infrastructure that can be blocked as a whole. Even with encrypted content, the fact of "who talked to whom and when" sits in one place, and that place can be switched off. The problem isn't corporate malice — it's that such a point exists at all.
Two: the phone number. A number is an identity document tied to a SIM bought with your passport. A messenger that requires a number cannot be anonymous by construction.
Three: the transport. A perfectly encrypted messenger is useless if DPI can see it on the wire and cut the connection. Here is the detail people miss most often: encryption and invisibility are different problems. You can encrypt everything flawlessly and still be identified by the first eight bytes of a packet.
Prizrak (Russian for "ghost") is an attempt to close all three at once without giving up comfort: chats, groups, channels, calls, voice messages, video notes, reactions, gifts — everything people got used to in Telegram.
What it adds up to
| Capability | How it works |
|---|---|
| Identifier | name:domain — no phone number, no email |
| Encryption | OpenPGP passport + X3DH + Double Ratchet; MLS (RFC 9420) designed in for large groups |
| Federation | Your own homeserver on your own domain; servers find each other by themselves |
| Transport | Real TLS 1.3 to a real domain, multi-port, decoy page on probing |
| Censorship resistance | A network of "vaults" — relay nodes that cannot read what passes through |
| Calls | Custom native media stack, a custom STUN equivalent, P2P or relay |
| VPN | Built in, two hops, custom protocol on the wire (engine done, plumbing in progress) |
| Clients | Electron (macOS / Windows / Linux) and React Native (Android; iOS in progress) |
| License | AGPL-3.0 |

Three pillars
Everything stands on three things. None of them can be switched off on demand — they are built into the architecture.

1. Federation with no center
There is no main server. Anyone runs their own homeserver on their own domain, and their users immediately talk to users of every other server. Exactly like email: many servers, one network.
A user is addressed as alice:a.org — a name plus the domain of their homeserver. When alice:a.org writes to bob:b.org, server a.org looks at the recipient's domain, locates b.org and hands the envelope over.
2. End-to-end encryption
The server is a dumb relay of sealed envelopes. It sees the route (from, to, when) and ciphertext. Nothing else. Neither the server owner, nor the ISP, nor whoever seizes the machine can read the conversation: only the participants hold keys.
This is not a promise, it is a property of the code: the server stores a payload field containing ciphertext and holds no key that opens it.
3. Invisible to DPI
Messenger and call traffic must look like ordinary web browsing. Not "similar to HTTPS" — literally HTTPS: a real TLS 1.3 handshake with a real server certificate. There is nothing to detect, because nothing is being faked; the handshake is genuine.
Federation, in practice
The design follows Matrix, deliberately simplified.
Discovery. The classic route is GET https://domain/.well-known/prizrak/server, which returns a base URL. But I went further: you never have to list other servers in a config file. A server probes its peer's standard ports — 443, 8801, 80, 993, 995, 587, 465, 143, 110, 25 — finds a working entry point and caches the result for five minutes. The resolver config entry survives only as an optional pin for peers on non-standard addresses.
Why this matters: every mandatory config entry is a point of failure and manual labor. A node changes address and you are editing files on every server in the network. Live discovery removes that pain entirely.
Two APIs. Client-to-server and server-to-server:
| Level | Method | Path | Purpose |
|---|---|---|---|
| Client | POST | /_prizrak/client/v1/register |
publish profile and prekey bundle |
| Client | GET | /_prizrak/client/v1/bundle?userId= |
fetch a peer's keys (including from another domain) |
| Client | POST | /_prizrak/client/v1/send |
send a sealed envelope |
| Server | POST | /_prizrak/federation/v1/send |
accept an envelope for a local user |
| Server | GET | /_prizrak/federation/v1/bundle?userId= |
serve a local user's keys |
Where it is simpler than Matrix. Matrix replicates the full room state graph between servers with conflict resolution. That is powerful and heavy. For a Telegram-shaped messenger it is overkill: messages are a stream of sealed events delivered store-and-forward, and group state is resolved inside the E2E group rather than in open server state.
Storage is SQLite in WAL mode with real tables and indexes (it used to be a JSON file; the migration shipped with automatic conversion and 34 store tests).
Encryption: why a "pure PGP messenger" is a bad idea
The initial instinct was "just use PGP" — public key, private key, everyone understands it. That is the right instinct about asymmetric cryptography and the wrong decision for a message stream. Let me be precise about why.

What is wrong with PGP as a conversation transport:
- No forward secrecy. In PGP every message to you is encrypted to the same long-term key. Leak that key once and years of history decrypt. A ratchet gives every message its own one-time key.
- No post-compromise security. After a device compromise PGP does not heal. Double Ratchet re-establishes secrecy at the next DH exchange, and an attacker holding an old key falls out of the conversation.
- Key management. Rotation, revocation and picking the right key have historically been where ordinary users make their worst mistakes.
What PGP does superbly — and why it stayed:
- An identity passport. A long-term key is a stable identity. Users verify its fingerprint out of band — by QR code in person, or over a video call.
- Signing the ephemeral keys. Every X25519 prekey is signed by the OpenPGP key. That signature is exactly what catches MITM: a server handing out somebody else's key in place of yours cannot sign it. In the test suite a swapped prekey bundle is rejected on the signature.
- Offline envelopes. Invites, push payloads, account backups — classic PGP encryption to a public key fits perfectly.
The resulting design:
OpenPGP key (long-term) ───signs───▶ X25519 identity + prekeys
│
X3DH: the first shared secret
│
Double Ratchet (1:1 stream)
forward secrecy + post-compromise security
Under the hood: X25519 for the DH ratchet, HKDF-SHA256 for the root chain, HMAC-SHA256 for the symmetric chains, ChaCha20-Poly1305 as the AEAD. Out-of-order delivery is supported through skipped message keys — without it a mobile network would shred the conversation at the first cell handover.
Pairwise sessions do not scale to large groups: O(n) encryptions per message. That is what MLS (RFC 9420) is for — one encryption per message regardless of group size, with O(log n) work per membership change. MLS assumes only an untrusted delivery service, which maps onto this model neatly. The caveat: MLS is designed in but not yet integrated — today groups run on per-device fan-out (see the caveats at the end).
On key substitution, honestly. The defense is layered: prekeys signed by the passport, fingerprints verified out of band, and — for production — a public key transparency log (append-only, CONIKS-style) so that a swapped key becomes publicly detectable. The KT log is planned, not written. I am not going to pretend otherwise.
Transport: why an ordinary call is caught in one packet
This is the most underestimated part of the problem. People assume that if everything is encrypted, DPI sees nothing. It sees plenty: DPI does not look at content; it looks at shape.

An ordinary WebRTC call carries two signatures, both in the clear:
- STUN/ICE: the magic cookie
0x2112A442sits at offset 4 in the packet. It is a constant from the standard; it has to be there. Instantly recognizable. - DTLS-SRTP: record type
22plus version0xFEFDat the start of the media stream. Another crisp signature.
The conclusion I had to accept early: you must disguise the entire transport, not "the signalling". Hiding the control channel is pointless when the media stream announces itself in its first bytes.
The fix: don't imitate HTTPS, be it
The best thing that currently works against modern filtering is the Reality/XTLS approach. The idea is counter-intuitive: do not fake TLS, perform a genuine TLS 1.3 handshake.
- On the wire there is a valid TLS record with a real certificate. (Today that certificate is placed by hand; a full Reality-style mode with a live cover site and a Chrome-shaped uTLS fingerprint is still on the roadmap.)
- The client proves its right to the tunnel with a hidden token inside the already-encrypted channel — invisible from outside.
- Probe resistance: if a censor knocks without a valid token, the server transparently serves an ordinary page and closes the connection. There is no anomaly, therefore nothing to blacklist.
- Media frame lengths are blurred with random padding so statistical analysis cannot latch onto the characteristic size of audio packets.
Multi-port, and "the client will find its way"
The server listens on a list of stable ports at once and silently skips those that are taken or need privileges. The client connects to an address with no port and scans, starting at 443. When a certificate is configured, 443, 993, 995 and 465 come up as genuine HTTPS/WSS.
The logic is simple: the more independent entry points, the more expensive the block. And the fewer questions for a user who just types name:domain and should never have to learn the word "port".
An honest caveat. I deliberately did not build "my own obfuscation on top of TLS" and then claim it is indistinguishable. Real TLS 1.3 on 443 with a real certificate gives a filter far less to match on than any home-made mimicry protocol would. That is not the same as unlinkable — timing and volume are still there. A custom mimicry protocol is a separate large R&D effort, and calling it "invisible" without that work would be dishonest.
Calls: how I lost a week to 2048 bytes
The most instructive engineering story in the project. Keep it as a checklist if you ever build your own media stack.
The initial diagnosis. Calls were slow and cooked the phone. Encryption was not to blame (the AEAD costs a fraction of a percent of CPU). Two other things were:
- Every video frame crossed the React Native bridge into JavaScript as base64. Monstrous overhead, per frame.
- All media went through the server, always.
The fix: media moves entirely into native code (Camera2 / MediaCodec / AudioRecord / Opus), while JS handles only signalling and UI. Zero base64 per frame.
A STUN of my own. Public STUN is unusable — see the magic cookie above. Instead the server acts as an address mirror itself: the client sends one UDP packet shaped like a QUIC initial (long header, random connection ID), and the server replies with the public ip:port it observed. That is precisely STUN's function with no recognizable byte on the wire. Candidates are then exchanged over the already-encrypted signalling channel, followed by simultaneous hole punching.
And here is where I got burned. Once the direct P2P path went live, audio in video calls was excellent while video fell apart into artifacts and freezes. The difference between audio and video is packet size: an Opus frame is hundreds of bytes, a VP8 frame is tens of kilobytes. The receive UDP buffer was 2048 bytes. Small audio packets fit; large video frames were truncated, the AEAD failed, the frame was lost.
What actually fixed it:
- receive buffer
2048 → 65536; - video temporarily routed over the relay (TCP keeps large frames whole and ordered), the direct path kept for audio until I finished MTU fragmentation in the next release;
- receive-side gating: after any loss, delta frames are dropped and a PLI is sent until a fresh keyframe arrives. Instead of a smear of artifacts you get a short freeze and a clean recovery;
- bitrate driven by channel backlog. Loss-based adaptation is blind on TCP (there is no loss), so the encoder is steered by the send queue length: the queue grows, drop the bitrate sharply; the queue drains, raise it carefully.
Topology. Direct P2P when NAT allows, relay otherwise. Mobile carriers are almost universally CGNAT, so the relay is the workhorse and the direct channel wins on Wi-Fi. The path indicator (🔗 / 📡) is visible during the call alongside loss and bitrate.
On codecs and iOS, honestly. Codec negotiation in the offer already exists: VP8 by default (for desktop compatibility), H.264 in test mode on Android. What remains is teaching the desktop to receive H.264 (WebCodecs) and porting the native stack to iOS — where there is a hard limit: iOS has no hardware VP8, VideoToolbox does H.264/HEVC. The iOS client itself is in the state "code written, never verified on a device": building it needs a Mac with Xcode, and the audio module was written blind and will almost certainly need a round of fixes.
And honestly about the current mobile state: video calls are switched off again. A later regression took audio down along with video (most likely drawing into a dead EGL context after the SurfaceView was destroyed), so I pulled the button until the video path can be rebuilt with a guarantee that video cannot kill audio. Audio calls work.
The vault network: delivery when the direct path is cut
The situation: servers on domains S1 and S2 cannot see each other — one's IP is banned at the other. But both can see some third node. That is enough.

The key decision: delivery by pull. Under filtering it is far more robust when both ends make outbound connections to a shared node. S1 drops the envelope into a vault, S2 fetches it. Neither needs inbound reachability from the other — only outbound reachability to the vault.
The vault knows nothing. The mailbox address is a token — HKDF(recipient homeserver public key, epoch) — not a domain. The node does not know whose mailbox it is; it sees an opaque, rotating identifier and ciphertext. Deduplication is content-addressed (msgId = hash(ciphertext)).
Who actually delivers. The envelope is placed on four nodes at once (RF=4). The recipient takes whichever copy answers first and broadcasts a signed ACK to every replica. A node that receives the ACK deletes the blob. The ACK is the single source of truth about delivery: a node returning from an outage asks the live ones "is there an ACK?" and, if so, simply drops its stale copy.
Self-healing, borrowed from Ceph. Here I took the proven RADOS model and mapped it onto vaults:
| Ceph | Prizrak vaults |
|---|---|
| CRUSH — deterministic placement with no central table | Rendezvous hashing (HRW): anyone computes where a blob lives |
| Cluster map + epoch | Signed node registry with an epoch, spread by gossip |
size / min_size |
RF=4 / min_size=2 |
| Placement Group | A bucket of blobs; replicas compare Merkle roots per bucket |
| Primary OSD | Primary vault — first live node in the deterministic order |
| Backfill / recovery | Topping copies back up to RF when a node leaves |
| Peering on OSD return | Merkle resync: drop what was delivered, shed extras, pull what is missing |
An important adjustment for censorship: node-to-node links can be cut too. So the baseline guarantee is that the sender fans out to four nodes immediately (that path is known to work), self-healing is best-effort on top, and the backstop is the sender periodically re-fanning-out anything still unacknowledged.
Immutability makes life much simpler than in Ceph: blobs are write-once and deleted on ACK or TTL (7 days). No mutations means no versions, no strict write ordering, no quorums. Just "copy present / absent / acknowledged" plus anti-entropy.
Hardening. Length padding into buckets, proof-of-work admission against spam, anti-Sybil diversity by a signed operator-group label, epoch rotation, jitter. Plus private bridge nodes that never enter the public registry and are handed out by invitation — the Tor bridge model.
Tested. The vault network and federation over it are covered by 140 automated checks across node and server, including replication, self-healing, a node returning from an outage, and anti-enumeration of listings.
Discovery without "a list in the config"
A separate problem I had to solve: how do servers and nodes find each other when any static list is a gift to a censor? Publish a file listing your nodes and they all get blocked at once.
The principles:
- No static lists. The directory is live gossip state, not configuration.
- No single point. The directory is a set of signed objects that anyone can serve: another server, a vault, a mirror, a CDN, DNS. Blocking one domain means nothing.
- Kerckhoffs's principle. Assume the adversary knows the code and the entire public catalogue. Security comes from signatures (you cannot poison it), a multitude of entry points (you cannot block them all) and the stealth transport (traffic looks like ordinary HTTPS).
- One live contact is enough. Reach any node and you unfold the whole current catalogue. After that you never need a config again.
Cold-start channels, any single one of which suffices: live gossip from a known peer, baked-in rotatable seeds, DNS seeds over DoH (which routes around DNS tampering), a signed bootstrap bundle over a large CDN, peer exchange, and private bridges handed out by invitation.
On anti-enumeration: the registry is served in portions with rate limiting (BridgeDB style), and bridges are never gossiped publicly at all.
Honestly: cold start under a total ban is not solvable by magic — you need at least one out-of-band way to receive a bridge, exactly as with Tor. But the layers above mean that in practice very few users ever land in that case.
The built-in VPN: two hops and a protocol of my own
Users install a VPN separately anyway, so it makes sense to build one into the client — especially since all the stealth machinery already exists.

The requirements were strict and, I think, correct:
- No standard protocols on the wire. WireGuard, OpenVPN, L2TP, plain SOCKS — filters have known those signatures for years.
- Two hops: client → an intermediate node in the user's own country → an exit abroad. The entry node knows your IP but not your destination; the exit knows the destination but not who you are.
- The local SOCKS/tun lives only inside the device and never appears on the wire.
- The messenger's own messages and calls bypass the tunnel — otherwise you build a loop. On Android that is
addDisallowedApplicationfor itself; on desktop, route exclusions.
The layers (each with its own tests):
| Layer | What it does |
|---|---|
| Shadow | Encryption: Noise NK + X25519, ChaCha20-Poly1305, replay protection |
| Mask | From outside the node is a real website with a door for insiders |
| Relay chain | Two hops with onion-style layered encryption |
| Breath | Traffic shaping: DATA/PAD/KEEP frames, keepalive, automatic profile |
| Flock | Handing out node addresses without a public list |
| Health | Liveness checked from inside the session, no pings; automatic node replacement |
| Economy | Tickets, tariff, revenue sharing with operators |
Three decisions I am happy with:
- Liveness without pinging. Pinging nodes from outside means lighting up both them and you. Liveness is judged from inside an established session. A node dies — switch to another in the same country, or the neighboring one, make-before-break: the new exit comes up first, then the old one is torn down.
- Country from GeoIP of the observed address, not from the operator's claim. An operator can write anything; it is verified by observation.
- Node rating is Bayesian with 60-day recency weighting and anti-gaming, displayed as "4.35 out of 5". Below 2.5 a node is hidden.

The engine is covered by 182 tests in the VPN package. What remains before field readiness: a real TLS front with Let's Encrypt and a Chrome-shaped ClientHello fingerprint, real sockets between relay and exit instead of in-memory simulation, the tun↔engine packet pump, and testing on live carrier networks. I am not hiding that either: the engine and its layers are done and tested; the production plumbing is not.
Multi-device without one shared key
One account on three devices is a classic trap. The easy answer — "put the same key on every device" — destroys the security model: steal the tablet and the whole account is compromised with nothing to revoke.

How it works: two levels of keys. The account root is the existing OpenPGP key (it already lives in the backup and is restored by the seed phrase). Each device has its own X25519 identity and prekeys, signed by the root and published to a device registry.
The sender fetches the recipient's device list and encrypts the message separately for each device in its own ratchet session. On top of that, self-sync distributes your own outgoing messages and read marks to your other devices.
What it buys, and what it costs:
- Revoke a device and it stops receiving envelopes; the rest keep working.
- A new device sees direct conversations "from now on"; old history is not pulled in. That is a consequence of forward secrecy, not an oversight — for groups and channels it is partly solved already; for direct chats an optional E2E-encrypted server-side history copy is on the roadmap.
- The cost is O(members × devices) encryptions. Sender keys / MLS fix that for large groups — the next big step.
The limit is 10 devices per account; publishing an eleventh evicts the oldest. The device list and a revoke button live in settings.
Groups, channels and search
The goal here was mundane: parity with Telegram, because people will not use something that lacks what they are used to.
- Types: public / private group, and channels where only admins post.
- Member permissions: send messages, send media, add members, pin messages, change group info, change your own tag — six independent switches plus per-user exceptions.
- Slow mode: 5 / 10 / 30 seconds, 1 / 5 / 15 minutes, 1 hour.
- Auto-delete: off / 1 day / 1 week / 1 month. The server drops its copies, and clients additionally sweep local copies older than the retention window — otherwise "deletion" would be a fiction.
- History for new members — switchable.
A registry for public group search. A separate service (tech.prizrak.im): servers publish only public rooms to it, records are signed by the homeserver's key, domains are accepted TOFU-style and cross-checked against .well-known, with a 60-requests-per-minute-per-IP rate limit, a 200-groups-per-domain cap and a 7-day TTL. The client queries the registry through its own server, so search works from behind the stealth transport too.
Bot API: like Telegram, only smaller
It installs on every homeserver. A bot is a real Prizrak account whose E2E keys live on the server, while the third-party developer works over plain HTTP with a token (if you expose that endpoint at all, put it behind your own TLS terminator):
POST http://<server>:8840/bot<TOKEN>/sendMessage
{"chat_id": "!roomid:example.org", "text": "Build is green ✅"}
The response envelope matches Telegram's: {"ok": true, "result": …} or {"ok": false, "error_code": …, "description": …}. The v0.1 methods are getMe, sendMessage and getUpdates (long-poll up to 30 seconds, acknowledged by offset).
Bots are created by PrizrakFather, a direct equivalent of BotFather that bootstraps itself on the service's first start. You DM it /newbot, answer two questions, get a token. There are also /mybots, /revoke and /deletebot.
The first target use case was down to earth: posting into channels from external scripts and websites. One end-to-end integration test drives the whole cycle: a real homeserver plus botapi plus a live user, a conversation with PrizrakFather, token revocation, group creation and posting into it through the API.
Economics: ghosts, and why money is centralized
The messenger has an internal currency — ghosts (👻): gifts, donations, reactions, VPN payments, rewards for node operators.
Which raises the question you should ask of any decentralized project with a currency: what stops the admin of some random server from crediting themselves a million? The code is open, after all.

The honest, unromantic answer: balances cannot live on self-hosted servers. So the boundary is drawn like this:
- Messages, groups, channels, calls, files, keys — fully decentralized. No central site is involved at all.
- Money — a single authoritative ledger (the "Ghost Bank"). That centralization is unavoidable: ghosts are bought with real money. Telegram Stars work the same way.
Three guarantees:
- You cannot mint. The balance exists only in the Bank, which increases it exclusively after a confirmed payment — a webhook with HMAC signature verification, idempotent on the transaction reference. A rogue admin can only buy ghosts with real money, which is perfectly legal.
- You cannot steal. Every operation is signed with the user's separate Ed25519 "ghost key" and verified by the Bank. The admin of a homeserver does not have that private key. Replay protection: a ±5-minute timestamp and a single-use nonce.
- You cannot hijack a name. The key is bound to
user:domainunder TOFU: whoever registers first sets the key, and any later change requires a signature from the current one.
A fork can of course issue "its own ghosts" — but that is a different currency of a different bank. Open source does not undermine this: security rests on the Bank's secret payment keys and users' private keys, not on secrecy of the source.
Operator rewards. A vault node earns ghosts for uptime, for deliveries and for gigabyte-hours of storage. Deliveries are currently counted from the node's own report — a cryptographic delivery proof from the recipient is on the roadmap, and until then the network owner adjusts the numbers by hand. The network owner sets the rates in an admin panel, sees the operator table and pays out the accrued amount. Uptime is capped per sample so a disconnect cannot be billed, and payouts are idempotent.
What it is built from

A monorepo. The JavaScript core is reused by desktop and mobile; native code appears only where nothing else will do (media, the Android VPN service).
Run your own server:
cd packages/server
npm install
./deploy/prizrak-deploy.sh init --domain chat.example.org \
--admin root --registration on --relay-url stealth://<IP>:8810
./deploy/prizrak-deploy.sh create-admin --password 'PASSWORD'
./deploy/prizrak-deploy.sh start
Run a vault node (any server or home machine with Node.js and a permanent connection will do):
cd packages/deaddrop
npm install
node src/node.js
# status: http://127.0.0.1:8820/status
Default ports: homeserver 8801, call relay 8810, rendezvous 8811/UDP, vault node 8820, group registry 8830, Bot API 8840 — plus 80/443 and the mail ports when TLS masquerading is on. UDP 8811 has to be opened in the firewall explicitly; without it the direct P2P path only forms inside a single Wi-Fi network.
Clients are on prizrak.im: an APK for Android, .exe for Windows, .dmg for macOS, an AppImage for Linux. Registration is a username and a password; the domain is filled in for you.

Among the small things that cost more than expected but without which a product isn't a product: an account backup file, a 17-word seed phrase with a checksum (a spare way in and a password reset), one-click auto-updates with Ed25519 signature verification, minimize-to-tray, an incoming-message sound (yes, the ICQ one), round video notes up to 60 seconds, voice messages with a waveform, and delivery/read ticks — where for voice and video notes the second tick only appears once the message has actually been played, as in Telegram.
What is missing, and where it is weak
The section that makes any security write-up worth reading.
- Metadata. E2E does not hide the fact of a conversation from your own homeserver: who, with whom, when. Log minimization and channel separation reduce it; nothing removes it entirely. Absolute invisibility does not exist — the goal is to make deanonymization and blocking disproportionately expensive.
- Home-grown crypto in the prototype. The ratchet implementation is educational, even if well tested. Production needs mature libraries and an independent audit. Rolling your own crypto is a bad idea and I will say so plainly.
- Key transparency is planned, not written.
- MLS is designed, not integrated. Groups currently work through per-device fan-out, which is correct but does not scale to very large groups.
- The VPN's "field" stage is open: it needs a real TLS front with automatic certificates, real relay↔exit sockets, the tun↔engine packet pump, and testing on live networks.
- One-time prekeys (OTKs) are reused by the server: X3DH degrades to the safe no-OTK variant, but strict forward secrecy wants single-use OTKs with replenishment. A separate task.
- Mobile video calls are paused after a regression that took audio down with video. Audio calls work.
- The iOS client has never been built on a device, and native video is blocked on the absence of hardware VP8.
- Let's Encrypt issuance is not yet automated — the certificate is placed by hand.
None of this stops you from using the messenger today, but it is worth knowing.
A legal note
Circumvention tools protect free speech and privacy, but their legality differs by jurisdiction. The project is developed as open source for privacy and freedom of communication; complying with the law that applies to you is your responsibility.
Closing
Prizrak is an attempt at an honest decentralized messenger that refuses to compromise in three places at once: no center, no identity binding, no recognizable trace on the wire. Plus a built-in VPN, because all three problems lean on the same stealth transport and it would be odd not to reuse it.
The project is open; forks and independent servers are welcome — the more independent nodes exist, the more expensive the network is to block.
- Code: https://github.com/Foxeevich/prizrak (AGPL-3.0)
- Site and clients: https://prizrak.im
- Docs:
docs/ARCHITECTURE.md,docs/VPN-DESIGN.md,docs/GHOST-BANK.mdin the repository
I would welcome a substantive teardown — especially from people who have worked on DPI, transports and group cryptography. Well-aimed criticism is more useful here than stars.