Security whitepaper
Version 1.1 · 10 August 2026 · CC-BY 4.0
Version 1.1 · 2026-08-10 · CC-BY 4.0
Abstract
Nduzem 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, however a
provider running SELECT against a customer database can still read
every file. In regulated contexts, such as 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; precisely, forward secrecy per-message is not naturally per-file. Second, recipients frequently have no existing account with the sender's provider. This makes link-mode delivery a first-class use case, not an edge case. Third, files are larger than messages by orders of magnitude. Consequently, 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.
Nduzem 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 includes 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.
Where a user has opted in to key backup (§3.6), this adversary also
obtains that user's sealed key container. It is a crypto_secretbox
under a key derived from a recovery key that has never been
transmitted to the server and cannot be derived from anything the
server stores. A compelled-disclosure order can therefore compel the
container and still not produce a readable key. We state this
explicitly because "the operator stores an encrypted copy of your
private key" is a claim that deserves to be examined rather than
waved past.
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, however size padding was considered and deferred. The threat model acknowledges this: 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 the server never holds in usable form
(§3.6) and a network adversary never sees at all.
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.
Key backup (§3.6) extends this surface off the device. A recovery key is designed to be written down, which means it can also be photographed, synced to a cloud notes app, or stored in a password manager that is later breached — none of which involve compromising the device at all. Anyone holding both the recovery key and the account credentials can reconstruct the identity key and read everything sealed to it.
We consider this an acceptable trade because it is opt-in and
because the alternative it displaces is worse: users who are told
that key loss is unrecoverable improvise their own backups, and an
improvised backup of a private key is rarely a
crypto_secretbox. The mitigation is honesty at the point of
generation — the client tells the user, when it shows the recovery
key, that anyone holding it can read their files.
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:
- Identity keypair (X25519). Used with libsodium's
crypto_box_sealto receive per-transfer symmetric keys. The public half is uploaded to the server; the private half never leaves the device in a form the server can read (§3.6). - Signing keypair (Ed25519). Used to sign the SHA-256 hash of the ciphertext container so recipients can verify authenticity. Same asymmetry; public half on the server, private half subject to the same rule.
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 user-initiated, never automatic. A user may replace their identity keypair at any time, gated on password re-authentication plus a second factor where one is enrolled; the consequence — every transfer sealed to the old key becomes permanently unreadable — is acknowledged explicitly before the rotation proceeds.
A device that loses its identity keypair loses the ability to decrypt any transfer sealed to it. Unless the user has opted in to a key backup (§3.6), that loss is permanent by construction: the server has never held anything that could reconstruct the key. 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://nduzem.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 authenticated-encrypted metadata blob,
the envelope, or enc_header in code, containing:
filename(UTF-8, sanitised at receive)mime(best-effort, nullable)plaintext_length(int, cross-checked at decrypt)blob_sha256(hex of the ciphertext container's SHA-256)
The envelope is encrypted with K_file under libsodium's
crypto_secretbox (XSalsa20-Poly1305), laid out as
nonce (24 bytes) || crypto_secretbox_easy(header_json, nonce, K_file).
This is a different primitive from the crypto_secretstream_xchacha20poly1305
used for the file body in §3.3: the body needs chunked streaming over
gigabytes, while the envelope is a single small message encrypted and
decrypted in one shot, which is exactly what crypto_secretbox is
for. Both are standard, both are authenticated, and both are keyed by the
same per-transfer K_file.
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.
3.6 Optional key backup and recovery
A device that holds the only copy of an identity key is a single point of failure. Losing a phone means losing every transfer ever sealed to it. v1.0 offered no remedy; users were told to accept the risk. That is a defensible cryptographic position and a poor product one, and in practice it pushes users toward keeping a plaintext copy of something somewhere — which is worse than a construction designed for the purpose.
Key backup is opt-in. A user who never enables it is in exactly the position described in v1.0 of this paper, and every claim made there about their keys continues to hold unchanged.
Construction. The client generates a 128-bit recovery key
from the platform CSPRNG. From it, a wrapping key is derived with
keyed BLAKE2b (crypto_generichash), keyed by the recovery key over
the domain-separation string opaqueshare/recovery-key/v1. Both
keypairs are serialised and sealed with crypto_secretbox
(XSalsa20-Poly1305) under that wrapping key. The result is stored
server-side as an opaque container:
"OSRK" | version(1) | nonce(24) | secretbox(identity_priv, signing_priv)
The recovery key is displayed to the user once, as 26 characters of Crockford base32 in groups of five. The alphabet omits I, L, O and U to remove the transcription ambiguities that matter when a human copies a string onto paper.
No password KDF is used. The input is already 128 bits of uniform CSPRNG output, so there is nothing for argon2id to stretch; running one would cost seconds on a phone and buy nothing measurable. This is the one place where the usual "always use a password KDF" rule does not apply, and the reason is that the secret is not a password.
What the server holds. The ciphertext container and nothing else. The recovery key is never transmitted, never derived from anything the server knows, and cannot be recovered from the blob. An adversary who obtains the entire database — §2.1 or §2.2 — gains a sealed box they cannot open.
What changes in the threat model. Two things, stated plainly:
- The recovery key becomes a second credential capable of recovering the identity key, and it lives wherever the user put it. A recovery key photographed, emailed to oneself, or stored in a compromised password manager is a path to the account's key material that did not previously exist. This is a real transfer of risk from "lose the device, lose the data" to "leak the recovery key, leak the data," and it is the user's choice to make. §2.6 covers the endpoint side.
- Rotation and backup interact. Rotating the identity key deletes the stored backup server-side, because a backup of a superseded keypair is worse than none: it would restore a key that no longer matches the published public key, and the client would have to detect that after the fact. The client compares the fingerprint derived from a restored keypair against the fingerprint the server currently publishes, and refuses a restore that does not match rather than leaving the user with a silently useless key.
What does not change. The server's decryption capability, which remains nil. Key backup adds ciphertext to the server's storage; it adds no plaintext and no key material.
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
{ 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
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 Nduzem 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.
5.3 Link mode has no fingerprint
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
- Sender user id, recipient user id, and mode (app or link).
- Storage key: a random object-store key; not derived from content.
- Byte count: the ciphertext container's total size, as measured by the object store's HEAD after upload.
blob_sha256: the ciphertext container's SHA-256, sent at commit and used for the abuse blocklist.- Timestamps: created, first-downloaded, acked, expired.
enc_headerandsignatureas opaque byte strings.- The key-backup container (§3.6), for users who have opted in: an opaque byte string holding both private keys sealed under a key derived from a recovery key the server has never seen. One row per user, deleted on key rotation and on account erasure.
wrapped_key(app mode only) as an opaque byte string sealed and unopenable without the recipient's identity private key.- User account metadata: email and handle, both application-layer encrypted at rest with a per-column AEAD key above database-at-rest encryption. A blind index (HMAC of the plaintext email under a separate key) supports login lookups without plaintext storage.
- Access patterns, which recipient downloaded which transfer at which time.
6.2 What the server does not see
- File content. Encrypted with K_file, which the server never possesses.
- Filename, MIME type, plaintext length. Encrypted inside
enc_headerunder K_file. - Sender or recipient private keys in any usable form. They are generated on-device. For users who have not enabled key backup, they are never transmitted at all. For users who have, the server holds only the sealed container of §3.6 — the key that opens it is never transmitted, and the server cannot derive it. In neither case does the server possess a private key it can use.
- Link-mode K_file. Lives in the URL fragment; browsers do not transmit fragments in HTTP requests.
- Whether a recipient's device successfully decrypted a given
transfer. The server sees
/ackbut not verification outcomes.
6.3 What the server emits about itself
Three transparency mechanisms make server behaviour verifiable to external observers:
- Admin audit log. Every admin action, such as takedown, blocklist change, user action, DSAR export is written to an append-only, hash-chained table. Each row contains the SHA-256 hash of the previous row's payload plus its own. Tampering with the log breaks the chain.
- Transparency canary. A periodic operator-signed statement of "we have not received a gag order or compelled-disclosure request in this window" is published to a public log. Absence of a canary in a given window is itself the signal.
- External anchor. The root of the audit-log hash chain is periodically anchored to an external timestamping service. This prevents retroactive log rewriting even by an operator with root.
None of these prove that the server behaves correctly because 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:
- Transfers in UPLOADED state past their expiry window
(
transfer_ttl_days, default six hours) object-store DELETE, mark EXPIRED. - Multipart uploads that received
/initiatebut never/commitor/abortpastmultipart_orphan_grace_seconds- AbortMultipartUpload on the pending session.
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.
- On mobile, plaintext lands in a temp file under
getTemporaryDirectory()and is deleted after save-to-user-chosen location or ack, whichever comes first. - On web, plaintext lives in an in-memory Blob and is released on
save (via File System Access API on Chromium browsers, or
<a download>on Firefox and Safari). The browser reclaims the Blob on tab close.
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.
9. Comparison to related work
The following rubric compares Nduzem'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 |
|---|---|---|---|---|
| Nduzem | 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, Nduzem 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 Nduzem 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]. Nduzem'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. Nduzem 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.
Nduzem'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 "Nduzem cannot be broken." It is "Nduzem'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. Nduzem'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:
- No forward secrecy per-transfer beyond one-shot K_file deletion.
Identity keys are long-lived; an attacker who compromises both a
recipient's identity_priv and the server's stored
wrapped_keycopies can decrypt historical transfers. User-initiated identity-key rotation now ships (§3.1) and limits future exposure, but it does not retroactively protect transfers already sealed to the old key; nothing short of forward secrecy would. - No post-quantum crypto. Discussed in §2.7. The v2 target is a hybrid X25519 + Kyber KEM for K_file wrap and hybrid Ed25519 + Dilithium for the signature, once stable libsodium bindings ship.
- No group or broadcast transfers. One sender, one recipient (app mode) or one URL (link mode). Multi-recipient app-mode transfers are on the v2 roadmap; the current sealed-box construction extends naturally by sealing K_file separately to each recipient.
- No automatic cross-device key sync. First login on a new device is a fresh device from the identity-key perspective: logging in exchanges credentials for tokens and carries no key material. A user who has enabled key backup (§3.6) can restore their keypair onto the new device by entering their recovery key, which makes manual cross-device transfer available today. What remains absent is anything automatic — no key escrow, no silent sync, no transfer that happens without the user deliberately moving a secret. QR-code pairing, which would remove the transcription step without introducing escrow, is on the v2 roadmap (client ADR-0011 covers the per-device key architecture).
- No external cryptographic audit yet. Server ADR-0004 documents the deferral criteria and the intended engagement model. This paper is the intermediate artifact: a specification against which an audit can be scoped.
- No metadata-privacy protections beyond TLS. Size padding and cover traffic are v2+ considerations. In v1, an adversary who observes the network can learn transfer timings and sizes; the protocol prevents them from learning content.
11. References
Cryptographic primitives:
- Bernstein, D. J. (2006). Curve25519: New Diffie-Hellman Speed Records. Public Key Cryptography — PKC 2006.
- Bernstein, D. J., Duif, N., Lange, T., Schwabe, P., & Yang, B.-Y. (2012). High-speed high-security signatures. Journal of Cryptographic Engineering.
- RFC 7748 - Elliptic Curves for Security (X25519, X448).
- RFC 8032 - Edwards-Curve Digital Signature Algorithm (EdDSA).
- RFC 9106 - Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications.
- Bernstein, D. J. (2008). ChaCha, a variant of Salsa20.
- Bernstein, D. J. (2013). Extending the Salsa20 nonce. (XChaCha20 rationale.)
- Denis, F. (2013–2026). libsodium. https://libsodium.gitbook.io/
Prior art:
- [1] Marlinspike, M., & Perrin, T. (2013). The Double Ratchet Algorithm. Open Whisper Systems.
- [2] Mozilla (2020). Update on Firefox Send and Firefox Notes. Blog post announcing discontinuation.
- [3] Denis, F. crypto_secretstream_xchacha20poly1305. libsodium documentation.
- [4] Backendal, M., Haller, M., & Paterson, K. G. (2022). MEGA: Malleable Encryption Goes Awry. IEEE Symposium on Security and Privacy.
Nduzem ADRs cited in this paper:
Client repo (public):
- ADR-0003 - OS4S ciphertext container.
- ADR-0004 - Streaming send.
- ADR-0006 - Streaming receive.
- ADR-0010 - In-app link decrypt (link-mode receive on the Flutter app).
- ADR-0011 - Per-user secure storage.
- ADR-0013 - Web send/receive streaming.
- ADR-0014 - Metadata deferred to
/commit.
Server-side design records are internal to the project. The security-relevant behaviour of the server (typically what it sees, what it does not see, and 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
- v1.1 · 2026-08-10 - Documented optional key backup and
recovery, and user-initiated key rotation, both of which shipped after
v1.0.1. New §3.6 gives the construction (128-bit recovery key,
keyed-BLAKE2b wrapping key,
crypto_secretboxcontainer) and states what it changes in the threat model and what it does not.Four statements elsewhere in the paper were true at v1.0.1 and are no longer true without qualification, and are corrected here rather than left to be discovered:
- §3.1 said "there is no cloud backup." There is one now; it is opt-in, and the sentence now says so.
- §6.2 said private keys are "never transmitted," unqualified. For users who enable backup, a sealed container is transmitted. The bullet now distinguishes the two cases and states the property that actually holds in both — the server never possesses a private key it can use.
- §10 said a user on a new device "must re-register or accept that received transfers cannot be decrypted." Restore makes manual cross-device transfer available. The non-goal is now correctly stated as the absence of automatic sync.
- §10 listed cross-device key rotation as the intended v2 remediation. It shipped.
§6.1 gains the key-backup container, per that section's own rule that new storage requires it to be revisited. §2.2 records that compelled disclosure can now reach the container and still not produce a readable key; §2.6 records the new off-device exposure a written-down recovery key creates. No property claimed in v1.0.1 is weakened for a user who does not enable backup.
- v1.0.1 · 2026-08-02 - Corrected §3.4. The envelope was
described as using "the same AEAD primitive" as the file body; it does
not. The body uses
crypto_secretstream_xchacha20poly1305and the envelope usescrypto_secretbox(XSalsa20-Poly1305). Both are standard and both are authenticated, so no security property stated elsewhere in this paper changes — but the paper claims every statement is traceable to source, and this one was not. - v1.0 · 2026-07-28 - Initial publication covering the v1 protocol.