AVANOAvano
Architecture

Encrypting what you say is solved. We work on the rest.

Message encryption is a finished problem and we did not try to re-solve it — Avano runs unmodified libsignal. Everything below is about the part nobody encrypts: who you talk to, when, how often, and from where.


01 — Identity Adversary: a court order served on us

There is no accounts table. Not an empty one — none.

Avano asks for no phone number, no email address, no username and no name. There is no registration step in the app, so there is nothing for us to store and nothing for a court order to name. Your identity is an Ed25519 account key derived on your own device from a BIP39 recovery phrase, and the phrase never leaves the phone.

That account key signs a per-device key inside a device credential, so a second device cannot be silently added to your identity by anyone holding a copy of one key. Your safety number is computed over the account key, and pairing verifies both that the credential is valid and that it vouches for exactly the device key in the bundle being offered — which is what stops a credential being transplanted onto somebody else's device.

The consequence worth stating plainly: there is no account recovery, because there is no account on our side to recover. That also means nobody impersonating our support can ever ask you for anything useful, and if you lose the recovery phrase we cannot help you.

Held by us

Nothing. There is no user record of any kind.

Live
Held by you

A recovery phrase and a key pair, on the device, sealed at rest.

Live
Recovery

The phrase restores the identity. It does not restore a single message. The restore screen is built on both platforms; that the restored account carries the typed phrase's identity is not yet independently verified.

Built

02 — Pairing Adversary: whoever can read the channel you sent the invite on

You add someone with a link that stops working once it is used.

An invitation is a sealed blob the relay stores under a random link id. The key that opens it rides in the URL fragment — the part after the #, which browsers and clients never transmit — so the relay holds ciphertext it cannot read. The relay serves that blob exactly once and deletes it.

That is the tripwire. If somebody intercepted your invitation and fetched it first, the person you actually sent it to finds it already gone and gets a loud failure rather than a silent downgrade. Our own end-to-end test asserts the acceptor fails loudly and that nobody is paired — not merely that a second fetch returns nothing, which would have been a proxy for the property rather than the property.

The queue addresses inside an invitation are signed by the account key, and an invitation arriving without its post-quantum identity fields is refused. That last part matters more than it sounds: an earlier version verified the signature only if the fields were present, so deleting two fields walked straight past a defence that called itself downgrade-protected.

Three limits on the tripwire

The serve-once store is in-process, so a relay restart forgets every pending link and every tombstone; across that boundary a consumed link reads as unknown and the tripwire fails quiet rather than loud. It cannot tell the interceptor from the acceptor — both hold the key and to the relay they are identical; what it detects is that two parties fetched, not which was honest. And it covers the link path only.

One-time link

Sealed, key in the fragment, served once, interception detectable.

Live
QR pairing

The engine can mint the bytes, but neither app can read a code: there is no camera permission in the Android manifest and no decoder in either app. The scan button is hidden in any build with a real engine.

Not shipped

03 — Content Adversary: someone recording today for a quantum computer later

Post-quantum at the handshake and on the ongoing ratchet.

Sessions open with PQXDH — X25519 alongside an ML-KEM-1024 prekey — and every message afterwards carries a Sparse Post-Quantum Ratchet component mixed into the Double Ratchet. The distinction between those two is the whole point: a great many products announce "post-quantum" and mean the handshake only, which fixes the quantum-resistant material once at setup and never refreshes it.

Our test drives fifteen alternating ratchet steps and asserts that no component is empty and no two are equal. That is what separates a continuous ratchet from one that stopped.

All of this is unmodified libsignal. We build on audited cryptography and we do not write our own — which is also why this is the one section of the architecture where we claim no advantage over Signal. We match them here, deliberately.

Never say "Avano is post-quantum" unqualified

The two claims above are true of message content and nothing else. Tor's circuit layer is classical — the shipped dependency offers four circuit handshakes and the strongest identifies itself as ntor3-curve25519-sha3_256-1 — so an adversary recording our traffic today could, with a future quantum computer, decrypt the transport. We closed the part of that which was ours: a hybrid X25519 + ML-KEM-1024 handshake now seals every frame inside the circuit, so the queue ids polled and their keys are no longer readable to a future machine. What remains open is Tor's own: that someone dialled the relay onion at time T. Nothing reaches backwards to recordings already made.


04 — Transport Adversary: the relay operator, and your network

Tor for every byte, by default. Not a setting.

There is no "enable Tor" screen, no advanced mode, and no clearnet fallback in a shipped build. The relay is published only as an onion service, which produces a useful property: a bare socket cannot resolve a .onion name at all, so a successful connection is itself evidence that the circuit carried the traffic. The plain-TCP path exists behind a debug environment variable and is unreachable in a build you would install.

We ask Arti for full vanguards and counter-galois-onion explicitly, rather than accepting defaults. Full vanguard mode matters for a client that dials the same relay onion from the same device every day for as long as the app is installed: the default is sized for onion activity lasting weeks. We also request Counter-Galois-Onion, Tor's newer relay crypto that hardens a circuit against a tagging attack — but on our onion-only circuits it covers the guard and middle hops, not the end-to-end client↔relay hop, where Tor has not yet implemented it, and we have not confirmed at runtime which hops actually negotiate it. Do not read this as end-to-end tagging resistance.

The distinction we hold ourselves to here is one this project learned expensively. "Tor is in the build" and "the messages go over Tor" were two different truths in this codebase for four days, and only one of them was ours. So the transport is named in the assertion, never inferred from a build flag — and the embedded Tor stack was confirmed present in the shipped Android library by reading its symbols, not by reading a #[cfg].

0Settings a user must find and switch on for this to be true.
1Address the relay publishes, and it is an onion service.
8Isolated Tor legs your inbound queues are spread across.

05 — Relay Adversary: a warrant, a subpoena, and a seizure

Not a server that knows you. A shelf of numbered slots.

A relay holds blind queues: sealed, identical-size blocks filed under random codes. There is no sender field and deliberately no sender authentication — that is how anonymous mail works. It never learns who deposited a block, who owns a queue, or that two queues belong to one conversation.

Every envelope pads to one fixed plaintext size and is then sealed whole into the transport block under a per-contact symmetric secret, at exactly 4,096 bytes. No field of ours is readable on the wire at all: no length, no type tag, no padding boundary. A relay holding the bytes cannot tell a text message from a reaction, a receipt, a file manifest or a group message — and cannot tell a session opener from an ongoing one, which used to be trivially visible because the opener was about 1,700 bytes longer.

Each of your contacts sends to a dedicated queue of their own, so no contact can link two contacts as co-recipients, and those queues are spread across eight isolated Tor legs with each contact's poll time drawn independently. Before that work, a relay read your complete queue set, your exact contact count and a stable device pseudonym out of a single round trip.

What is written to the disk: nothing

Queues live in memory. Where durability across a restart is configured at all it must point at a RAM-backed filesystem, and the relay refuses to start if that directory turns out to be on a real disk. The override is a literal string an operator has to type out in full — i-accept-a-seizable-spool — which is the kind of thing you cannot do by accident and cannot claim you did.

So a warrant for the disk returns nothing and a subpoena for backups returns nothing. A copy of the machine's memory is a different question and it is answered on the limits page rather than here, because a page describing a defence is the wrong place to be trusted about that defence's edge.

Block size

4,096 bytes, every block, in both directions.

Live
On disk

Nothing. RAM-backed spool or the process refuses to start.

Live
Relays

One relay runs today, in Germany, onion-only. A second ran in Iceland — different company, network and jurisdiction — and was deliberately allowed to lapse in early August 2026; the app never dialled it, because it pins a single relay. A diverse-jurisdiction second relay, sited outside Europe, is planned but not deployed, and the split that would use it is not switched on.

One relay
The split

The routing that puts the two sides of a conversation on different relays is written and is not switched on. The app dials one relay. On Android it tells you, per conversation, whether the split is in force rather than letting you assume it; on iOS that indicator is not built.

Not on

06 — Cover Adversary: anyone watching your network link

A constant rate, whether you are writing or reading in silence.

One fixed-size block leaves per slot. A real message pre-empts a decoy, and a decoy is byte-identical on the wire and goes to a sink queue your own device owns and drains itself — never a contact's queue. So an observer counting your packets cannot read your volume off the link.

It is free and on by default for everyone, and that was a reversal: it had been decided as a premium feature six days earlier. The argument that overturned it is one sentence — a privacy lane you pay for marks the people who paid for it. A relay reads a subscriber's constant rate straight off the queue, and the people who buy anonymity are on average the people with most to hide. A crowd you buy your way into is not a crowd.

The switch still exists because the cost is real and measured rather than estimated: 748 MiB per device per month at a thirty-second slot, tapped at the socket. A loop decoy crosses the wire twice, up as a deposit and down as a poll.

It hides volume, not presence

The app locks and seals when it leaves the foreground, so a phone nobody has opened emits nothing. No amount of cover traffic makes a stopped stream look like a running one, and the edges of a session stay visible — softened by a warm-up ramp and a cool-down taper, not erased. This site claimed otherwise once and it was corrected rather than quietly dropped.


07 — Device Adversary: whoever is holding your phone

The lock is not a screen. It is the key leaving memory.

Every sensitive column is sealed with XChaCha20-Poly1305 under a fresh nonce, bound to its own row and column, with the master key in the platform keystore and never in the file. On top of that the whole store can be locked behind a passphrase: the seal key is an Argon2id entanglement of the keystore key and your passphrase, so extracting the keystore key from a seized device still yields nothing.

Every identifying row key — contact names, session recipients, message conversations, group senders — is stored under a keyed blind index rather than in the clear. That was not always true: the key–value table held natural keys in plaintext while every other table went through the blind index, so a single query returned a readable list of your contacts.

The engine locks itself. An idle timer runs inside the worker that owns the key, so the protection cannot be lost by a platform forgetting to call it, and it is identical on both platforms. The app should still lock on backgrounding — only the platform knows about that event — but that hook is the optimisation, not the floor.

And the claim is proved the way it should be. Our test records every allocation, frees nothing, and scans the heap for the seal key, with a positive control that finds the key before the lock. The test it replaced asserted only that a locked account refuses operations and that a wrong passphrase fails — both of which an implementation that quietly cached the key would satisfy perfectly.

What a seized database still shows

The whole-file encryption our own specification mandates is not in use — the vendored build fails on our target — so the schema, the table names, the indices and the row counts are cleartext, and per-column sealing can never fix that. Message bodies, identities, session state, the contact list and group membership are sealed. The shape and volume of the database are not. The full inventory of what remains readable is on the limits page.


08 — The path One message, end to end

Sarah writes “running late.” Here is every hop.

On the phone

The text is encrypted under the Double Ratchet with a fresh post-quantum component. The disappearing-message timer, the conversation id and — for a group — the group id all go inside the sealed envelope, which is why a group message is byte-indistinguishable from a one-to-one message.

Padding

The envelope is padded to one fixed plaintext size and sealed whole into a 4,096-byte block. Nothing about its length, type or boundary survives.

The wire

The block waits for the next cover-traffic slot and pre-empts a decoy, so its departure time carries no information the decoys did not already carry. It leaves over a Tor circuit to an onion address.

The relay

Accepts 4,096 opaque bytes and files them under the random code they were addressed to. No sender, no account, no link to any other queue. Held in memory only.

Collection

The recipient's phone polls that contact's own queue on its own independently drawn schedule, over its own isolated Tor leg, and acknowledges the block on the leg that served it.

Arrival

Decrypted on the device, checked against its franking commitment, and written to the sealed store. If a timer was set, the sweep later deletes it and truncates the write-ahead log — without that second step the message stayed readable in the log, which is precisely the one feature whose whole promise is that the content is gone.

What does not happen anywhere in that path: no push service is told a message arrived for your device. No push provider carries Avano's messages on either platform. It does post an arrival notification — a local one, raised by the app itself off a content-free wakeup over your own relay, carrying nothing but the app's name. What that costs is reach: nothing can be raised after iOS terminates the app, or while an Android phone is locked and sealed — though an optional "Stay connected" service keeps an unlocked, backgrounded Android receiving. The full argument is on the refusals page.


Next

Now read the two pages that cost us something.

Every mechanism above has an edge, and the edges are collected rather than scattered. If you only read one more page, read the one that says what this cannot do.