Security whitepaper

Version 1.0 · 28 July 2026 · CC-BY 4.0

Version 1.0 · 2026-07-28 · CC-BY 4.0


Abstract

OpaqueShare is a file-transfer service where the operator provably cannot read the content it stores or forwards. Files are encrypted on the sender's device with a per-transfer key sealed to the recipient's long-lived identity key (app mode) or shared out-of-band via a URL fragment (link mode). The server sees ciphertext, a byte-count, and a hash; it never learns filename, MIME type, plaintext length, or a way to construct the decryption key. The client verifies content integrity via a rolling SHA-256 and sender authenticity via an Ed25519 signature over that hash, both bound to the recipient's declared trust in the sender's public keys. This paper describes the protocol, its threat model, and its explicit non-goals. It is written for readers who want to verify the cryptographic claims rather than take them on trust; client-side design decisions cite the corresponding architectural-decision records (ADRs), which are published alongside the client source.

1. Introduction and motivation

The word "encrypted" has been stretched thin in the file-transfer market. When WeTransfer or Dropbox advertise encryption, they mean their servers use disk-at-rest encryption and TLS on the wire - a provider running SELECT against a customer database can still read every file. In regulated contexts - legal discovery, medical records, journalism, incident response - that guarantee is often insufficient. What is needed is end-to-end: the file is unreadable to any party the sender did not intend to reach, including the operator, including under compelled disclosure.

End-to-end encryption is well understood for real-time messaging, where Signal's Double Ratchet [1] has become the reference design. It maps awkwardly to file transfer for three reasons. First, files are one-shot rather than continuous — forward secrecy per-message is not naturally per-file. Second, recipients frequently have no existing account with the sender's provider — link-mode delivery is a first-class use case, not an edge case. Third, files are larger than messages by orders of magnitude — streaming, integrity, and resumability need to work on multi-gigabyte payloads on consumer-grade devices.

Firefox Send addressed some of these (link-mode) with in-browser decryption via URL fragments and closed in 2020 after abuse-report handling proved too costly to sustain [2]. The lesson is not that end-to-end file transfer is unworkable; it is that abuse handling and content integrity are load-bearing pieces of the design that must be built in, not bolted on.

OpaqueShare is a zero-knowledge file-transfer protocol with app-mode and link-mode delivery, streaming send and receive, per-transfer ciphertext hashing, sender-signature verification, and a cooperative abuse-report mechanism that operates over ciphertext hashes without compromising the encryption of unrelated transfers. The design principle it optimises for is minimum server knowledge: every decision is measured against what it forces the server to learn.

This paper covers the v1 protocol. §2 states the threat model. §3 describes the cryptographic architecture. §4 walks the send and receive flows. §5 covers the trust model. §6 enumerates what the server can and cannot see. §7 describes the data lifecycle. §8 covers the additional protections. §9 compares the design to related work. §10 lists explicit non-goals and the v2 roadmap. §11 collects references.

2. Threat model

The adversaries below are listed in decreasing order of resources we expect the design to defeat.

2.1 Honest-but-curious server operator

Someone with legitimate root access to the server host, a sysadmin, a database administrator, a stack operator, who wants to read stored files or reconstruct who-sent-what-to-whom without overtly breaking rules. This is the primary adversary the protocol is designed against. We defeat this adversary by ensuring the server never possesses the material required to decrypt.

2.2 Compromised server or compelled disclosure

An attacker who has either taken control of the server host or compelled the operator to hand over everything the server has lawful access to. This is the same adversary as §2.1 with the gloves off. Our defence is the same: absence of decryption material in the server. What such an attacker gains is metadata — sender and recipient account identifiers, byte counts, timestamps, and ciphertext-object references. They do not gain the ability to decrypt in-flight or archived transfers.

2.3 Passive network adversary

An attacker who can observe TLS-encrypted traffic to and from the server but cannot decrypt it. This adversary sees connection timing, packet sizes, and hostnames via SNI. We do not defend against traffic-analysis attacks in v1 — size padding was considered and deferred. The threat model acknowledges this honestly: an attacker who logs enough sender ⇄ recipient pairs and file sizes can infer relationships that the plaintext would confirm.

2.4 Active network adversary and TLS MITM

An attacker who can intercept and alter TLS-encrypted traffic. In practice this requires either a rogue certificate authority in the user's trust store or a compromised endpoint. Our defence layers on top of standard TLS: certificate pinning at the client is planned (v2 roadmap) but not shipped in v1. The client does verify the sender's identity public key via an out-of-band fingerprint mechanism (§5); an active MITM cannot forge a valid sender signature over a substituted blob_sha256 without also holding the sender's Ed25519 signing key, which never leaves the sender's device.

2.5 Malicious sender or malicious recipient

A user who is themselves inside the cryptographic envelope. This is the weakest adversary and the one the design explicitly does not try to defeat: a recipient who receives a file can obviously read it, screenshot it, and share it further. A sender can send whatever they choose to send. What the protocol does provide, even here, is non-repudiation: the sender's Ed25519 signature over the ciphertext hash proves to the recipient which key material was used, and via the sender-public-key mechanism (§5) the recipient can bind that to a durable identity.

2.6 Endpoint compromise

Explicitly out of scope. If the sender's or recipient's device is compromised (hostile OS, keylogger with root, malicious browser extension) the file is compromised. The design does not attempt to defend the plaintext once it has left the sodium primitive on either end. This is documented as a limitation, not a defence-in-depth opportunity to be hardened around: attempting to would inflate complexity without meaningfully changing the adversary's economics.

2.7 Store-now-decrypt-later (quantum)

A resourced attacker who records ciphertext today with the intent of decrypting once large-scale fault-tolerant quantum computers become available. Curve25519 and Ed25519 are both broken by a sufficiently capable quantum adversary via Shor's algorithm. This risk is acknowledged: the v2 roadmap discusses the migration path to a hybrid classical-plus-post-quantum construction. v1 ships without post-quantum protection because the primitives available in stable libsodium do not yet include a settled Kyber/Dilithium implementation, and shipping a home-grown one would be worse than the status quo.

3. Cryptographic architecture

The protocol composes three layers of key material: (1) long-lived per-user identities; (2) per-transfer symmetric keys, and (3) per-chunk AEAD tags. Each of these addresses a distinct property.

3.1 Per-user long-lived keys

Every registered user has two keypairs generated on their device at first registration:

Storage on the device is platform-native: Keychain on iOS, Keystore-backed Encrypted SharedPreferences on Android, IndexedDB wrapped by an origin-scoped AES key held in SubtleCrypto on web. Each installed device slot is per-user: a device with multiple accounts holds separate keypairs under separate slots (client ADR-0011) rather than a single overwritable slot.

Key rotation is not automatic in v1. A device that loses its identity keypair loses the ability to decrypt any transfer sealed to it; there is no cloud backup. The client warns loudly on the verify-contact flow when a contact's public key differs from what was previously verified out-of-band (§5).

3.2 Per-transfer symmetric keys

Every transfer generates a fresh 32-byte key — K_file — via libsodium's crypto_secretstream_keygen. This key is used exactly once, for one transfer, and is discarded on the sender's device after the send completes.

In app mode, K_file is sealed to the recipient's identity public key via crypto_box_seal and travels through the server as the wrapped_key field on the transfer envelope. The server sees the sealed bytes; only the recipient's identity private key can unseal them.

In link mode, K_file is base64url-encoded and appended to the share URL as a fragment: https://opaqueshare.com/r/<id>#<K_file>. Browsers do not send URL fragments in HTTP requests, so the server never sees K_file for link-mode transfers. The recipient's browser extracts the fragment client-side and passes it into the decrypt routine locally.

Neither mode grants the server the material to decrypt.

3.3 Content encryption — the OS4S container

File bytes are encrypted with libsodium's crypto_secretstream_xchacha20poly1305 [3], which provides a streaming AEAD with a randomly chosen per-encryption 24-byte header and 17-byte AEAD tags on each 64 KiB chunk. The wire format is:

[4-byte magic prefix: 'O','S','4','S']
[24-byte secretstream header]
[chunk_1: up to 64 KiB plaintext + 17-byte AEAD tag]
[chunk_2: ...]
...
[chunk_N: last chunk, tag = FINAL]

Every chunk is authenticated with a Poly1305 tag over the ciphertext and its position in the stream, so truncation, reordering, and byte-level tampering are all detected. The FINAL tag on the last chunk defends against truncation attacks that would otherwise be undetectable at the AEAD layer.

The 64 KiB plaintext-chunk size (client ADR-0003) is chosen for two reasons. It is small enough that memory usage during encryption and decryption stays proportional to one chunk regardless of file size - multi-gigabyte transfers work on 4 GB mobile devices. It is large enough that per-chunk overhead (17 bytes) remains under 0.03% of plaintext even on maximally chunked payloads.

3.4 The envelope

Each transfer carries a small AEAD-encrypted metadata blob, the envelope, or enc_header in code, containing:

The envelope is encrypted with K_file using the same AEAD primitive. The server sees the envelope only as an opaque byte string. The recipient decrypts it after unsealing K_file (or extracting it from the URL fragment in link mode), and uses the fields to name the saved file, verify the plaintext-length invariant, and cross-check the hash the server reported.

Alongside the envelope, the sender emits an Ed25519 signature over the ciphertext hash. That is, Sign(signing_priv, blob_sha256). The recipient verifies this against the sender's signing public key (which arrives on the download response, alongside the sender's identity public key, see §5). A recipient who trusts the sender's public keys can conclude both that the ciphertext bytes are the ones the sender emitted (authenticity) and that no intermediary substituted them (integrity).

3.5 Mode split summary

Property App mode Link mode
Recipient has account Yes No
K_file delivery Sealed to recipient identity_pub via crypto_box_seal, through server URL fragment, out-of-band
Recipient identity trust Enforced via fingerprint (§5) None — anyone with the URL can decrypt
Optional password gate No (recipient is authenticated) Yes — argon2id-hashed on server
Sender signature verification Yes Skipped — link-mode sender identity is not exposed [ADR-0010]

The link-mode password does not weaken the encryption; K_file stays in the URL fragment either way. It is a rate-limited server-side check that fires before the presigned download URL is issued, defeating opportunistic link sharing without granting the server decryption capability.

4. Protocol flow

The send and receive paths are described below. The server never touches file bytes. It issues time-bounded presigned URLs against the cloud storage, and files stream directly from client to the storage provider.

4.1 Send

sequenceDiagram autonumber participant S as Sender participant API as API participant OS as Object Store S->>API: POST /transfers/initiate
{ mode, byte_count, recipient_id | link_password_hash } API-->>S: { transfer_id, upload_url(s) } Note over S: Encrypt-stream(source, K_file):
emits ciphertext chunks loop for each part S->>OS: PUT part (presigned URL) OS-->>S: 200 + ETag end Note over S: On stream close:
compute blob_sha256,
build enc_header,
sign blob_sha256 S->>API: POST /transfers/{id}/commit
{ blob_sha256, enc_header, signature,
wrapped_key?, parts? } API->>OS: HEAD storage_key
(verify byte_count) API->>API: Check blocklist by blob_sha256 API-->>S: 200 + { status: uploaded } API->>API: Enqueue recipient notification

Two aspects are worth calling out. First, the per-transfer crypto metadata (blob_sha256, enc_header, signature, wrapped_key) lands at /commit, not at /initiate (client ADR-0014 amends client ADR-0013 on this point). This is because the streaming pipeline does not know the ciphertext hash until the stream drains, and forcing the client to buffer the whole file to pre-compute it would defeat streaming's memory guarantees.

Second, the ciphertext-hash blocklist (see §8.2) fires at /commit rather than at /initiate for the same reason. The trade-off is that a banned re-upload wastes storage briefly before rejection; the TTL sweeper (§7) reclaims it. Blocklist hits are rare in practice.

4.2 Receive

sequenceDiagram autonumber participant R as Recipient participant API as API participant OS as Object Store R->>API: POST /transfers/{id}/download API->>API: Verify recipient authorised API-->>R: { download_url, wrapped_key?,
enc_header, signature, blob_sha256,
sender_identity_pub, sender_signing_pub } R->>OS: GET download_url R-->>R: Stream ciphertext,
compute rolling SHA-256 Note over R: Verify: computed hash == server-reported blob_sha256
(truncation / substitution defence) Note over R: Verify: Ed25519(sender_signing_pub, blob_sha256, signature)
(authenticity — skipped in link mode) Note over R: Unseal K_file via crypto_box_seal_open(wrapped_key,
own identity_priv, own identity_pub)
OR extract from URL fragment (link mode) Note over R: Decrypt enc_header with K_file → filename, mime, plaintext_length Note over R: Stream-decrypt ciphertext with K_file → plaintext
Verify emitted length == plaintext_length R->>API: POST /transfers/{id}/ack API->>API: Enqueue burn (see §7.1) API-->>R: { status: deleted }

Every verification step is client-side. The server never learns whether hash-verification succeeded or failed — it sees only whether /ack was called. This is deliberate: a failed verification is a security event on the recipient's device, not a coordination problem the server needs to manage.

5. Trust model and verification

Cryptography verifies that a message came from some key. It does not by itself verify that the key belongs to the person the recipient thinks they are talking to. That binding is the recipient's responsibility, and OpaqueShare provides three mechanisms for it.

5.1 Fingerprints

The server surfaces the sender's identity and signing public keys on the download response. The recipient's client recomputes a fingerprint (a Signal-style five-group base-10 digest of both public keys concatenated) and displays it in the receive UI. The sender sees the same fingerprint in their own profile.

Two users who trust that they are talking to each other can compare these fingerprints out-of-band, for example by voice call, video, in person, and record a durable trust decision on their local devices. This is the same TOFU-plus-verification model Signal's Safety Numbers use; the mathematics are identical, only the display format differs.

5.2 Trust on first use, with a hard block on change

When a sender first sends to a recipient (or looks up a recipient by email or handle), the client soft-warns that the fingerprint has not been verified. The user may proceed. On any subsequent send, the client checks whether the recipient's current fingerprint matches what was previously used. A mismatch is a hard block. The user must go through the verify-contact flow again before the send proceeds.

The rationale is asymmetric: sending to a wrong key is silent (the intended recipient never receives; an attacker who owns the wrong key does). Detecting key change proactively is the only way the user learns something is off before content leaves their device.

Because link-mode transfers have no on-platform recipient identity and the server does not expose the sender's public keys on the link-mode download endpoint, the signature-verification step is skipped for link mode (client ADR-0010). The web decrypt page displays a "sender not verified" banner explicitly. Users who need sender authentication should use app mode.

6. What the server can and cannot see

The following is exhaustive as of v1. New endpoints or storage columns will require this section to be revisited.

6.1 What the server sees

6.2 What the server does not see

6.3 What the server emits about itself

Three transparency mechanisms make server behaviour verifiable to external observers:

None of these prove that the server behaves correctly — a compromised operator can lie about anything they emit. What they do provide is detectable misbehaviour: any user who cares can pull the audit log and verify its hash chain against the anchored root, and any observer can note a missing canary.

7. Data lifecycle

Files exist on the object store from /commit (successful upload) through /ack (recipient signals success) or TTL expiry, whichever comes first.

7.1 Burn after read

On /ack, the server appends the transfer's storage key to a queue. A dedicated worker process drains the queue and issues DELETE calls against the object store. The queue is idempotent in that a repeated ack does not re-enqueue.

The worker is a separate container from the API service so that API request-serving latency is unaffected by burn-queue backpressure.

7.2 TTL sweeper

A second worker runs periodically and reaps two categories of lingering state:

The TTL sweeper is the backstop: the burn worker handles the common case (recipient ack), the sweeper handles crashes, abandonment, and users who close the tab without acking.

7.3 Client-side plaintext handling

The client is responsible for its own plaintext lifecycle.

Neither platform holds plaintext on disk longer than the receive session.

8. Additional protections

8.1 Rate limiting

Both per-user and per-IP sliding windows (server ADR-0019). Unauthenticated endpoints (login, register, password-reset) are IP-limited; authenticated endpoints are user-limited. Limits are documented per-endpoint in the API surface and returned to the client via Retry-After on 429 responses.

8.2 Abuse handling

Reports are first-class: any authenticated recipient can report a transfer. Admins review reports and can take down a transfer, which additionally offers to add its blob_sha256 to the ciphertext blocklist. Blocked hashes reject future re-uploads of identical ciphertext at /commit, regardless of which account submits them.

The design constraint is that abuse handling must operate over information the server has such as timestamps, storage keys, hashes, report submissions. The server does not gain the ability to inspect content in the process. Blocklist decisions are made on user reports and human review, not on server-side content inspection.

8.3 Log hygiene

Application logs never contain plaintext PII. Structured logging redacts email, handle, and any other application-layer-encrypted field at the emit point. A dedicated test suite exercises the receive path with fixture PII and asserts none of the fixture strings appear in captured log output.

8.4 GDPR and DSAR

Right-to-erasure requests trigger a tombstone-plus-scrub flow. The user's account row is retained (foreign keys from historical transfers require it) but every PII field is overwritten with a per-row nonce. A signed erasure receipt is returned so the user has evidence the request was honoured.

The following rubric compares OpaqueShare's design position against existing options. Ratings reflect the systems' stated designs, not audits.

System E2E for file bytes Open protocol Files-first UX Recipient identity trust model
OpaqueShare Yes Yes (this paper) Yes TOFU + fingerprint (§5)
Signal Yes Yes No (chat-first; file transfer is a Signal message) Safety numbers
WeTransfer / Dropbox Transfer No No Yes N/A (provider sees plaintext)
Firefox Send (2017-2020) Yes Yes Yes None — link-only
Tresorit / SpiderOak Yes (claimed) No Yes Proprietary
MEGA Yes Partial Yes Account-scoped
ProtonDrive share Yes Partial Yes Account-scoped

"Open protocol" here means the wire protocol and cryptographic construction are publicly specified — enough for a third party to build a compatible implementation or audit the design. Separately, OpaqueShare publishes its client source code; the server code is not open. Under a zero-knowledge design the client is where the audit-relevant work happens: the server is not trusted with content by construction, and its correctness is instead constrained by the transparency mechanisms in §6.3.

9.1 Signal

The design borrows Signal's Safety Number pattern for fingerprint display and the underlying primitives (X25519, Ed25519, XChaCha20 via secretstream). What OpaqueShare adds is files-first UX: per-transfer keys rather than session-persistent state, link-mode delivery for recipients without accounts, and integrity metadata sized for multi-gigabyte files. What it lacks compared to Signal is forward secrecy at the message level — each K_file is one-shot but identity keys are long-lived.

9.2 Firefox Send (2020, closed)

Firefox Send pioneered the K_file-in-URL-fragment pattern that link-mode inherits. Its closure was driven by abuse handling: the combination of easy sharing, provider-blind content, and no per-user rate limiting made it a preferred vector for malware distribution [2]. OpaqueShare's abuse-report and ciphertext-hash blocklist mechanisms (§8.2) are direct responses to this failure mode. Blocklist decisions are made on user reports, not server-side content inspection, so the zero-knowledge property is preserved.

9.3 Tresorit / SpiderOak

Both make strong end-to-end claims but ship closed-source clients, so users cannot independently verify that the code doing the encryption on their device matches the vendor's stated protocol. Under a zero-knowledge design, the client is the load-bearing surface for that claim, the server, by construction, is not trusted with content, so its source-code status is a lesser concern for confidentiality. OpaqueShare publishes its client source, so a user or auditor can read the exact encryption path that runs on their device against this paper's specification. The server is not open source; its role in the security argument is limited to what §6 enumerates, and the transparency mechanisms in §6.3 exist precisely to make server behaviour auditable without requiring source access.

9.4 MEGA

Backendal, Haller, and Paterson (2022) published a series of practical cryptanalytic attacks against MEGA's client-side crypto [4], including a malleability attack that could be turned into a plaintext-recovery attack under specific server behaviour. OpaqueShare's design differs in ways relevant to those attacks: it uses libsodium primitives rather than custom AES-CCM code, it signs blob_sha256 explicitly (not a derived value that admits malleability), and it does not depend on server-side unwrapping of per-file keys (crypto_box_seal is unseal-only on the recipient side).

The comparison is not "OpaqueShare cannot be broken." It is "OpaqueShare's construction avoids the specific classes of attack that broke MEGA in 2022, and its ADR trail makes future audit against similar attack classes possible."

9.5 ProtonDrive share

Proton's file-sharing product uses OpenPGP-based end-to-end encryption with account-scoped recipient identities. It is a credible alternative for users already inside the Proton ecosystem. OpaqueShare's differences are the streaming primitive (secretstream vs. per-message OpenPGP), link-mode support with no account required, and the open-protocol commitment (this paper).

10. Non-goals and future work

The following are explicit non-goals in v1, listed so users can make informed decisions:

11. References

Cryptographic primitives:

Prior art:

OpaqueShare ADRs cited in this paper:

Client repo (public):

Server-side design records are internal to the project. The security-relevant behaviour of the server — what it sees, what it does not see, the transparency mechanisms that make its actions auditable — is fully described in §6 of this paper; a reader who wants to verify a claim can do so against the paper itself. Access to the internal design records is available under NDA on request; contact the project.


License

This paper is licensed under Creative Commons Attribution 4.0 International. You may share and adapt it — including commercially — with attribution.

Changelog