Raven — A Steganographic, Metadata-Resistant Messenger

Technical White Paper · Draft v0.1 Scope: Solana mainnet-beta deployment · Last updated 2026-08-15

This document describes the mechanism and use case of Raven for a technical audience. It is a layered read: Sections 1–2 and 10 are accessible to any reader; Sections 3–9 are the protocol in full detail; Section 11 is a candid security analysis. It is derived from an internal, sprint-by-sprint engineering specification that is not public; where this paper and the implementation disagree, the implementation is authoritative.


Abstract

End-to-end encryption protects message content, but conventional secure messengers still leak metadata: who is talking to whom, when, how often, and — most fundamentally — that a conversation exists at all. For many users that metadata, and the mere presence of a messaging app, is the threat.

Raven conceals the existence of communication itself. Messages are encrypted client-side and stored as opaque blobs on the Solana blockchain; the ordinary public text a sender shares ("carrier text") carries no payload at all. A descriptive encoding rule — a statistical fingerprint of that text — is what tells the intended recipient's client that a message is waiting, and, in stealth mode, where to look. There is no messaging server, no account graph, and no contact list on any server. A shared pool wallet pays all on-chain costs, so a sender never holds cryptocurrency or signs a transaction. Crucially, recipients never poll: a message is discovered only when the recipient deliberately scans text they encounter, or receives a contentless real-time nudge over an optional signaling overlay ("Flock"). The result is a messenger whose traffic pattern, account graph, and message presence are all designed to be unobservable.


1. Introduction & Motivation

Modern E2E messengers (Signal, WhatsApp, etc.) solve content confidentiality well. What they do not, and largely cannot, hide:

Raven takes a different posture, summarized as deniability over convenience. Its carrier is infrastructure an adversary cannot easily take down or correlate: ordinary posts on public platforms (X, Reddit, LinkedIn, …) plus a public, globally-replicated blockchain. A Raven message looks like a normal social-media post to anyone who does not hold the recipient key and the carrier text simultaneously. There is no Raven account to subpoena, no server log to seize, and — by design — no background network chatter that betrays a user is "checking for messages."

Raven is built as three client surfaces over one shared protocol core, and runs against Solana mainnet-beta.


2. Threat Model & Design Principles

2.1 Adversaries considered

Adversary Capability What Raven denies them
Passive platform/social-media observer Reads all public carrier posts; can intercept Flock broadcasts on a public frequency Steganography hides that any message exists; the carrier is plausibly an ordinary post. The Flock frequency is not among the denials: it is a stable pseudonymous identifier derived from the recipient's public key — the key they hand out to establish contact — so it names no account, but anyone holding that key can compute it and watch it (§7.3)
Network observer Sees relayer and RPC traffic Content is E2E encrypted before it reaches the relayer; the relayer never sees plaintext; Solana reads are public and unauthenticated, so a read reveals only interest in one opaque address
Solana node / chain analyst Sees every stored blob forever Blobs are opaque AES-256-GCM ciphertext; addresses are rotating or rule-derived and unlinkable without the counterparty key
Device seizure Full access to the unlocked device Vault is encrypted at rest; the in-memory session view is wiped on lock/close; a compromised key can be socially revoked (see §11 for honest limits)

2.2 Constitutional commitments

Four principles are treated as non-negotiable invariants in the codebase and the spec. They are quoted here because they define what Raven is:

  1. No ambient polling. "There is no ambient polling, no periodic background sync, no speculative reads against registered keys… 'Performance' and 'convenience' are not valid reasons to relax this." Every RPC read is the result of a user action or a Flock push for a frequency the user explicitly subscribed to.
  2. Public keys are not feeds. Broadcast channels expose exactly PUBLIC_KEY_SLOT_COUNT = 10 recent messages in a rolling window — a product decision, not a storage limit. No unbounded history retrieval.
  3. The webapp is a gateway, not a control panel. Administrative and settings surfaces live in the extension/iOS app; the webapp is intentionally limited to keys / compose / scan / tune / about.
  4. The session view is memory-only. Revealed messages live in memory for the app session in chronological order, then vanish on lock or close. "Nothing is written to disk. Nothing crosses session boundaries."

These commitments are what make Raven's traffic pattern unobservable; they are also why several "obvious" convenience features (inboxes, history, background refresh) are deliberately absent.


3. System Architecture

Raven is a monorepo of six packages plus an Anchor smart contract. Three client surfaces share one protocol core; two backend components mediate Solana.

Backend (never sees plaintext)

Client surfaces (hold keys, do all crypto)

encrypted blob + key id

signs store_* tx

close_* aged PDAs

free getAccountInfo read

optional broadcast / subscribe

Chrome MV3 extension
popup · service worker · content scripts · OCR

Webapp · sendraven.ink
(Vite SPA, Vercel)

iOS app
(SwiftUI + CryptoKit)

raven-core (TypeScript)
crypto · encoding · keys · vault · scanning · frequency

RavenCore (Swift)
byte-for-byte mirror

Relayer (Express, Railway)
holds pool-wallet keypair

Sweeper (cron, Railway)
reclaims rent

Anchor program (Solana mainnet-beta)
C9DUi6…B4nrWo · pool wallet = only signer

Flock signaling overlay
api.theflock.ink (no ciphertext)


4. Cryptographic Protocol

All cryptography happens client-side. Primitives are standard and conservative.

4.1 Primitive stack

Layer Algorithm Library Parameters
Key agreement X25519 ECDH tweetnacl (scalarMult) 32-byte keys
Key derivation (messages) HKDF-SHA256 Web Crypto salt = empty, info = "raven-v1", → 256-bit key
Message encryption AES-256-GCM Web Crypto random 12-byte IV, 16-byte auth tag
Broadcast signing Ed25519 detached tweetnacl (sign) 64-byte signature over canonical JSON
Vault KDF Argon2id (legacy PBKDF2) hash-wasm / Web Crypto see §8

Note on naming. Some internal comments loosely refer to a "nacl.box envelope." In the shipping code, tweetnacl is used only for X25519 scalar multiplication, keypair generation, and Ed25519 signatures. The actual authenticated encryption of every message and the vault is AES-256-GCM via Web Crypto over an HKDF-derived key. This paper describes the implemented primitive.

4.2 Pairwise message encryption

To send to a known recipient, the sender computes the shared secret X25519(sender_secret, recipient_public), runs it through HKDF-SHA256 with info = "raven-v1" to obtain a 256-bit AES key, and encrypts the plaintext with AES-256-GCM under a fresh random 12-byte IV. The wire blob is:

base64( 0x00 ‖ IV(12) ‖ ciphertext ‖ GCM_tag(16) )

The leading flag byte discriminates wire formats. 0x00 = pairwise (the receiver must already know the sender's identity public key).

4.3 Anonymous first-contact (ephemeral) encryption

For the first message to a freshly imported key — where the receiver does not yet know the sender — Raven uses an ephemeral variant (flag 0x01). The sender generates a one-time X25519 keypair, derives the key from X25519(ephemeral_secret, recipient_public), and places the ephemeral public key in the clear on the wire:

base64( 0x01 ‖ ephemeral_pub(32) ‖ IV(12) ‖ ciphertext ‖ GCM_tag(16) )

The receiver decrypts with X25519(my_secret, ephemeral_pub) — no prior knowledge of the sender required. The sender's identity public key travels inside the encrypted body (as sender_public_key) so the two sides can bootstrap a stable channel afterward (§8.2). This buys sender anonymity for first contact; it is not a per-message forward-secrecy ratchet (§11).

4.4 Blob versions

A decrypted payload is interpreted by trying versions in order (decryptRavenBlob: v3 → v2 → v1):


5. Steganographic Encoding

5.1 Carrier text and encoding rules

Raven does not generate cover text. The user writes (or chooses) ordinary public text — the carrier. Raven derives a descriptive fingerprint of that text, the encoding rule, which both (a) selects which sentence in a post carries a message and (b) seeds stealth addressing (§7.4).

An encoding rule is a JSON object with a mandatory vowel backbone (total_vowels, a_count, e_count, i_count, o_count, u_count) plus a variable pool of features that are recorded only when present in the carrier: word_count, positional word_at[] checks, consonant_count, longest/shortest word lengths, words starting/ending with a vowel, and counts of uppercase, digits, punctuation, emoji, and double letters.

5.2 Descriptive, not prescriptive; iterate-present matching

Rules count what is in the text — they never instruct a user to write text a certain way. Matching is iterate-present-only: fields absent from the rule are not checked, and a sentence matches a rule iff every present condition holds. This makes carriers natural-looking while keeping matching deterministic.

5.3 Canonicalization (parity-critical)

Because the same rule must be computed identically on every platform, text analysis pins exact semantics: NFC normalization, counting by Unicode codepoint (not UTF-16 units), an enumerated punctuation set (not a locale-dependent class), and Intl.Segmenter with Extended_Pictographic for emoji. The canonical byte serialization of a rule (canonicalRuleBytes) is used directly as HKDF salt in stealth derivation, so its byte-exactness is load-bearing.


6. On-Chain Storage Model

6.1 The program and the pool wallet

The Anchor program (mainnet-beta ID C9DUi6mFpMxpjkCGw7oxw23Ty63jEg9nujmqgwB4nrWo) stores each message in a Program Derived Address (PDA). A single hard-coded pool wallet is the only authorized signer for every store_* and close_* instruction. The relayer holds that keypair; the sweeper reuses it. Reads are permissionless and free — anyone can getAccountInfo on a PDA — but they reveal only interest in one opaque address, not who can decrypt it.

Hard limits, enforced on-chain: MAX_BLOB_LEN = 1024 bytes, MAX_RECIPIENT_KEY_ID_LEN = 64 chars. Each account holds the 8-byte Anchor discriminator, its fields, and a blob of up to 1,024 bytes (≈1.05–1.1 KB total).

6.2 Three address families

Family PDA seeds Addressing model
Private ["raven", recipient_key_id] Rotating per message via a next_query_id chain; recipient_key_id is normalized to ≤32 bytes
Public broadcast ["raven-pk", encryption_pubkey, slot_index] Fixed 10-slot rolling window, slot_index ∈ [0, 10)
Stealth ["raven-stealth", recipient_encryption_pubkey, query_id] Rule-derived dead-drop, query_id = 16 bytes (§7.4)

Private addresses rotate: each delivered v2 blob carries the next address in the chain, so an observer cannot link two messages to the same conversation by address. Public channels are intentionally bounded to 10 slots. Stealth addresses are derived from a shared secret and the carrier's encoding rule, so they are unlinkable without both the counterparty key and the exact carrier.

6.3 Economics and lifecycle

The pool wallet funds the rent-exempt reserve when an account is created. The sweeper (cron-triggered, with a kill switch and a dry-run mode) later enumerates program accounts, classifies them by age, and closes the aged ones — returning the reserve to the pool wallet. Default age thresholds:

This makes write-cost recoverable and bounds Raven's permanent on-chain footprint.

6.4 The relayer

A stateless Express service exposes:

Endpoint Purpose
POST /submit Store a private raven
POST /submit-public Store a public-broadcast slot
POST /submit-stealth Store a stealth raven
GET /fetch Allow-listed (X/Reddit/LinkedIn) URL→plaintext proxy for scanning
GET /health Liveness + pool wallet pubkey

It validates blob size and identifier shape, then signs and submits. Two independent layers of rate limiting protect the pool wallet: an always-on in-memory per-IP limiter and a sliding-window per-recipient limiter backed by a shared store. An admin denylist refuses flagged identifiers with a 403 before anything touches the chain, logging no metadata.


7. Message Delivery: Two Modes Over One Store

A defining property: both delivery modes write the identical blob to the identical PDA. Flock is a signaling overlay, not a storage path — it never carries ciphertext. The sender chooses per message.

Recipient (client)Flock (optional)Solana PDARelayer (pool wallet)Sender (client)Recipient (client)Flock (optional)Solana PDARelayer (pool wallet)Sender (client)alt[Fast / social][Stealth / in the wild]derive encoding rule from carrier textencrypt blob (X25519 → HKDF → AES-256-GCM)POST /submit (blob + key id)store_* (signed by pool wallet)broadcast {frequency, carrier_text}real-time auto-tune pushgetAccountInfo + decryptpost carrier text publiclyencounter & scan carrier textgetAccountInfo + decrypt

7.1 Manual / stealth-in-the-wild

The sender simply posts the carrier text. The recipient discovers the message by scanning text they come across — no real-time signal, no Flock metadata, maximum deniability. Discovery is always pull, never push.

7.2 Fast / social (Flock)

The sender additionally broadcasts { frequency, carrier_text, timestamp } to api.theflock.ink. The recipient's auto-tune WebSocket — subscribed to the recipient's own derived frequency (§7.3) — receives a real-time nudge and runs a scan.

The overlay is not enabled uniformly across surfaces, and the catch-up behavior differs accordingly:

The automatic paths do not deliver everything. A discovery floor applies to every bulk scan — history replay, the offline catch-up, and a manual "scan all keys": a private raven younger than 7 days is skipped, and skipped without advancing its address cursor, so it remains retrievable later. Inside that window the raven is discoverable only by scanning the carrier directly, which is the deniable path §7.1 describes and the one the sender chose by posting in the wild. Scans the user initiates against a carrier — paste, screenshot, snip — are never gated, and neither is a live Flock push, which arrives at age ~0. The floor is set against the retention schedule in §6.3 so a raven cannot pass out of the skip window and into the close threshold between two catch-up scans.

None of this is polling: every read is triggered either by an explicit subscription the user opted into or by the user bringing the app to the foreground, and the overlay only ever says "go look."

7.3 Frequency derivation

A frequency is derived from a single public key — the recipient's — so any sender holding that key computes the same channel without coordination. The derivation takes one key, never a pair: a recipient has exactly one frequency, and every sender to that recipient broadcasts on it.

HKDF-SHA256( ikm = recipient_pubkey, salt = zeros(32), info = "flock-frequency-v1", L = 12 )
→ three big-endian 4-byte chunks → (u32 mod 36^4) → base36, zero-padded to 4
→ "@xxxx.xxxx.xxxx"

The 12-byte output yields a short, human-readable handle. This derivation must agree byte-for-byte across TypeScript, Swift, and the Flock SDK; a pinned fixture enforces it.

7.4 Stealth addressing

The stealth path lets a sender drop a message at an address only the intended recipient can compute, derived from the carrier itself:

query_id = HKDF-SHA256(
    ikm  = X25519(sender_secret, recipient_public),   // pairwise shared secret
    salt = canonicalRuleBytes(encoding_rule),         // raw canonical bytes, NOT hex
    info = "raven-stealth-v1",
    L    = 16 bytes )

The account is a PDA seeded with the recipient's own encryption public key and that tag — not the sender's — so the sender writes into an address in the recipient's key space, and the recipient looks only in their own.

Discovery is a probe, not a lookup. The X25519 agreement is symmetric, so both sides derive the same tag from the same carrier. But a pasted carrier arrives with no sender attribution: the recipient does not know whose key to use. On scanning, the client recomputes the encoding rule from the carrier and then walks its own key list — for each key, one candidate tag is derived and one account read is issued at the corresponding PDA. A miss returns nothing and is silent; a hit is decrypted and surfaced. A scan therefore costs one derivation and one read per key held, and the practical bound on stealth discovery is the size of the user's own key list — there is no directory, no index, and no server-side lookup to consult. Rotating chain addresses are tried first, so an ordinary delivery costs no stealth probes at all.

Stealth blobs sit at a fixed (non-rotating) address and can be re-queried later, enabling late discovery. An observer who sees the carrier post on social media cannot derive the address without the counterparty's key. (For comparison, ordinary private-channel addresses use a different primitive — SHA-256 truncated to 16 bytes — and rotate; the two are intentionally not interchangeable.)

A second variant serves public broadcast channels. A channel has no counterparty, so its tag is derived by a self-agreement — X25519 of the channel keypair with itself — which the channel owner and any follower holding the channel keys both compute identically. That variant seeds the PDA on the channel's public key and carries a v3 signed payload, verified on read, where the pairwise variant above carries v2 and is unsigned (§4.4). A scan probes both families in turn: the recipient's private keys first, then each followed channel.


8. Identity, Vault & Key Lifecycle

8.1 Vault at rest

Each client stores its keys in an encrypted vault. The vault JSON is sealed with AES-256-GCM under a key derived from the user's passphrase via Argon2id (memory = 64 MiB, iterations = 3, parallelism = 1, 32-byte output, 16-byte salt). This is CURRENT_KDF_VERSION = 2. Legacy vaults derived with PBKDF2-SHA256 (100,000 iterations) are version 1 and are transparently migrated on unlock (re-derived under Argon2id with a fresh salt, re-encrypted, verified, then written — legacy data is preserved on any failure). The stored payload tags its kdfVersion, so detection needs no heuristics. The same StorageAdapter interface backs chrome.storage.local (extension) and IndexedDB (webapp).

8.2 Identity, keys, and the handshake

An identity is an X25519 encryption keypair plus an Ed25519 signing keypair (backfilled on first unlock if a legacy vault lacks one). Each correspondent is a RavenKey record carrying rotating address state (initialQueryId/currentQueryId/previousQueryId), the last seen encoding rule, a connection state (pendingimportedconnected), a direction, and revocation status. First contact uses the ephemeral path (§4.3): the first blob on an imported key embeds the sender's sender_public_key, letting the receiver bootstrap a stable bidirectional channel.

8.3 Revocation

If a key is compromised, its owner can send a v2 blob with revoked: true. The counterparty's scanner then stops advancing the chain, marks the key revoked, and removes it from the compose picker. This is an authenticated social signal, not cryptographic proof of compromise (see §11).


9. Cross-Platform Parity

Raven's TypeScript core, the Swift RavenCore, and the Flock SDK implement the protocol byte-for-byte. This is enforced by checked-in JSON fixtures that pin canonical outputs for: frequency derivation, stealth query_id, channel derivation, ephemeral encryption, v3 canonical signing, and the vault KDF. Any change to canonicalization, signing, encoding-rule shape, or a derivation moves all surfaces and the fixtures together. For a white-paper reader the takeaway is that Raven's wire and address formats are specified and test-pinned, not incidental to one implementation.


10. Use Cases

Raven targets users for whom the existence of a conversation is the sensitive fact:

Two postures serve these users: fast-social (Flock notifications for active, casual exchange) and maximum-stealth (post in the wild, no real-time signal at all). Because the choice is per message, the same relationship can move between "chatty" and "dark" without changing keys or addresses.


11. Security Analysis & Limitations

Stated candidly, because honest limits are what make the strengths credible.

Strengths.

Limitations.


12. Conclusion & Future Work

Raven demonstrates that hiding that a conversation exists — not only its contents — is achievable by composing well-understood cryptography with a public blockchain dead-drop, steganographic carriers, and a strict no-polling discipline. The system is solo-built and running in production — relayer and sweeper on Railway, webapp on Vercel, iOS and browser-extension clients shipping on their own cadence — today against Solana mainnet-beta.

Natural next directions: a forward-secrecy ratchet for pairwise channels; decentralized or user-run relaying to remove the single-signer trust assumption; economic modeling for rent and sweeping at scale; and a formal steganographic analysis of the carrier scheme.


Appendix A — Parameters & Constants

Name Value
Program ID (mainnet-beta) C9DUi6mFpMxpjkCGw7oxw23Ty63jEg9nujmqgwB4nrWo
MAX_BLOB_LEN 1024 bytes
MAX_RECIPIENT_KEY_ID_LEN 64 chars
PUBLIC_KEY_SLOT_COUNT 10
Message KDF HKDF-SHA256, empty salt, info = "raven-v1"
Message AEAD AES-256-GCM, 12-byte IV, 16-byte tag
Wire flags 0x00 pairwise · 0x01 ephemeral
Vault KDF (current) Argon2id, 64 MiB, t=3, p=1, 32-byte key, 16-byte salt
Vault KDF (legacy v1) PBKDF2-SHA256, 100,000 iters
Stealth query_id (pairwise) HKDF-SHA256, ikm = X25519 pairwise secret, salt = canonical rule bytes, info = "raven-stealth-v1", 16 bytes
Stealth query_id (broadcast) as above, ikm = X25519 of the channel keypair with itself
Channel query_id SHA-256 truncated to 16 bytes (rotating)
Flock frequency HKDF-SHA256, ikm = recipient public key, salt = zeros(32), info = "flock-frequency-v1", 12 bytes → @xxxx.xxxx.xxxx
Broadcast signature Ed25519 detached over recursively key-sorted JSON
Sweeper thresholds (default) private ≥ 8 d (1 d grace) · public ≥ 60 d (all slots) · stealth ≥ 90 d
Auto-tune catch-up browser surfaces ≥ 48 h since last activity (on reconnect) · iOS ≥ 1 h (on foreground/unlock)

Appendix B — Notation


The protocol description in this paper reflects the implementation as of 2026-06-24; deployment facts (network and program ID) were updated 2026-07-26 for the mainnet-beta cutover; and the delivery, stealth-discovery, and relayer sections were corrected against the implementation on 2026-08-15.

Canonical source: github.com/OAraLabs/raven-whitepaper
Revision 47d61ac · page built 2026-08-20
This page is generated from the Markdown in that repository. If the two disagree, the repository is correct.
© 2026 OAra Labs, LLC · CC BY 4.0