Security › Whitepaper

The full technical document, published as written and rebuilt from its source on every deploy. For the short version, see Security. Questions or findings: security@paperwise.app.

Paperwise Security Whitepaper

Version 1.6 · October 2026 · security@paperwise.app

Paperwise scans, reads and files your paperwork, and keeps it private. This document explains exactly how: what is encrypted, with which keys, where those keys live, what our servers can and cannot see, and where the limits are. We wrote it for security-minded users and researchers, so it is specific. If anything here is wrong or unclear, write to security@paperwise.app.

In one paragraph: your iPhone does the page work itself: clean-up, text recognition, search and PDFs stay on it. Everything Paperwise stores on your iPhone is encrypted with its own keys, on top of iOS Data Protection, and left out of iPhone device backups: Library pages, files, photos and thumbnails with a Library key kept in the Keychain on this device, the database with its own key, and Hidden items with keys behind Face ID (and your Hidden passcode, if you set one). Readable copies exist only for the moment you use them: while you scan, view or share (§3 lists each case). Anything Paperwise puts in the cloud for you (sync, the iCloud backup, share links) is encrypted on your device first, with keys we don’t have; forwarded email arrives as normal email and is sealed to your devices’ key on arrival. Readable content reaches our servers only when you choose it: an AI action you start or, if you turn it on (Pro), automatic AI naming of documents you add afterwards; or a Word, Excel or PowerPoint conversion. We use no analytics or advertising SDKs. What we can see and how long we keep it is in §9; the limits are in §11.

1. Design principles

Your keys stay on your devices; sync, share links and the backup leave them only as locked boxes.
  1. Your keys, your devices. Keys that decrypt your synced, backed-up and Hidden content exist only on your devices, protected by the Keychain, Face ID / Touch ID and, if you choose, your own Hidden passcode. Two exceptions by design: a share link carries its own key (§8), and the Inbox’s public key lets our server seal email to you (§7).
  2. Ciphertext only, plus listed metadata, on our servers. What we store for you is encrypted before it leaves your device or, for email, sealed the moment it arrives. We also store the account, device and usage records listed in §9, some of them readable (for example your approved-senders list).
  3. Process on the device. Page detection, clean-up, text recognition, file text extraction, PDF export and the PDF and capture tools run on your iPhone (Apple Vision, Core Image, PDFKit). They work offline and send nothing.
  4. Readable only when you choose. Nothing goes to AI until you allow it; the AI helper is off on new installs. Content reaches our servers in readable form only for an AI action you start, for automatic naming of documents you add after turning “Name and sort new scans” on (Pro), or for an Office conversion you start, each behind a notice (§9). Our servers process it in memory and don’t store it; our AI provider keeps AI requests under its own retention terms (§9).
  5. No tracking. Paperwise contains no analytics or advertising SDKs, and no crash reporter. In-app purchases use Apple and the RevenueCat SDK, which receive purchase data and identifiers (§9).
  6. Standard, well-studied cryptography. AES-256-GCM, X25519, HKDF-SHA-256, HMAC-SHA-256, Argon2id, SHA-256. AES-GCM and SHA-256 come from Apple CryptoKit (through expo-crypto in JavaScript, directly in Swift); HKDF and HMAC are short JavaScript implementations of RFC 5869 and RFC 2104 on top of that SHA-256, checked against the RFC test vectors; X25519 is TweetNaCl (tweetnacl-js); Argon2id is the reference C implementation (vendored, native). We use no home-made ciphers.

2. Cryptographic building blocks

Purpose Algorithm
Symmetric encryption AES-256-GCM, 96-bit random nonce, 128-bit tag; associated data binds each ciphertext to its record and position, so blobs can’t be swapped or reordered. An all-zero key (what a wiped key leaves behind) is refused for encryption, and never stored in a passcode slot
Large files Chunked AES-256-GCM (4 MiB chunks, per-chunk nonce, AAD = record id + chunk index, plus an “end” marker on the last chunk), so truncation and reordering are detected. Sync objects use the same format.
Key agreement / sealing X25519 → HKDF-SHA-256 → AES-256-GCM (“sealed box”) with an ephemeral sender key; all-zero shared secrets rejected. The current format (PWSEAL1) is framed, with AAD = frame index + “final” flag. No other sealed-box format is read.
Key derivation from secrets Argon2id (64 MiB, 3 passes, parallelism 1) for passcodes and passphrases; HKDF-SHA-256 for deriving sub-keys. There is no weaker fallback: if the phone can’t give Argon2id its memory, the Hidden passcode isn’t set and you see an error. Stored parameters below these floors are refused.
Library files One random 256-bit DEK per file, wrapped by the Library key in the file’s header; the file in the chunked AES-256-GCM format above, AAD bound to the file’s random id (PWLIB1, specs/18). A file counts as encrypted only when its header authenticates under this install’s Library key, never because of its first bytes or its name
Database SQLCipher (AES-256) with a 256-bit random key
Randomness The operating system’s CSPRNG (SecRandomCopyBytes; a failure stops the operation instead of using the buffer)

3. On your iPhone

The Library key and the database key never leave this iPhone; Hidden adds its own keys behind Face ID.
  • Everything Paperwise stores is encrypted with its own key. Library page images and originals, thumbnails, files of every kind, photos and videos, card photos, signatures, note attachments, email attachments, items from the share sheet and received share links are written encrypted (AES-256-GCM, one random key per file, wrapped by a random 256-bit Library key). The Library key is kept in the Keychain with WhenUnlockedThisDeviceOnly, like the database key: it never leaves this iPhone, isn’t in device backups or iCloud Keychain, and never reaches the app’s JavaScript (native code decrypts by path). It isn’t behind Face ID, so sync and the backup work with the app lock off; Hidden adds its own keys behind Face ID (§4). Library files are written atomically (a temporary file, flushed, then swapped into place), so an interruption never leaves a half-written or readable copy; other encrypted files (Hidden’s own) can be left truncated by an interruption, never readable. On top of that, the app’s default file protection is iOS Data Protection class Complete: unreadable whenever the phone is locked. A scan of the Library folders at launch and at every return to the app checks that nothing readable is there; anything it finds (which no writer should leave) is encrypted in place at once. What is encrypted is decided by where a file goes, never by its name: files are stored under internal names (data-…, att-…), and the name you gave a file lives only in the encrypted database or, for Hidden, in the item’s sealed record. Whether a file is already encrypted is read from its header, authenticated with the Library key, so a file named x.enc or one that begins with Paperwise’s own format marker is encrypted like any other. That check logs counts only, never file names. A release build without the native encryption module refuses to save into the vault rather than saving readable files.
  • Readable only while you use it. A readable copy exists only in these cases, each under Caches or tmp, deleted when you’re done and again at every launch: (1) while you scan: the scan in progress and the camera and picker copies while the app is open (the draft is encrypted as soon as the app goes to the background); (2) while you edit: filter previews, the erase-pen result, a card or signature photo before it is saved; (3) while you view a file other than a PDF in Quick Look (PDFs are shown from memory); (4) while you share or export: the share sheet, drag and drop and exported files; (5) for one on-device tool call that only takes a file (clean-up, text recognition, file text, PDF tools, thumbnails of Office files and videos); (6) while something arrives: a PDF or Office import being turned into pages, the file picker’s own copy (an email body is decrypted in memory only). When the app leaves the foreground, inside a background task, the scan in progress and the capture scratch (camera, picker, document-scanner and editing copies) are encrypted in place, including files still being written, for as long as iOS lets the app run, so a capture resumes from encrypted files. Exported files and import scratch are encrypted in place then too (exports also when the app locks) and made readable again when you come back, so a share sheet left open keeps working; if iOS ends the app instead, they are deleted at the next launch. Quick Look and share-sheet copies of encrypted files and filter previews are deleted when the app leaves and when it locks; Read with AI, Count and Office conversions build their upload in memory. Images on screen are decrypted in memory, kept only in a bounded memory cache that is emptied when the app goes to the background and when it locks, and never written to the image disk cache (Paperwise’s loader refuses it). Sync, the iCloud backup, share links, hiding and un-hiding re-encrypt from one key to another in memory, 4 MiB at a time, without a readable copy.
  • Other protection classes. Some files can’t use Complete because they’re written while the phone may be locked; they hold only ciphertext or public keys: photos from the in-app private camera are encrypted as they’re written (completeUnlessOpen), Lock Screen captures (completeUntilFirstUserAuthentication), Hidden photo-import ciphertext (completeUnlessOpen), and the capture public key in the App Group (completeUntilFirstUserAuthentication).
  • Device backups. The Library’s content folders (pages, files, attachments, thumbnails) are marked “excluded from backup”, at every launch and whenever such a folder is created, so iCloud and Finder device backups don’t contain your documents. The encrypted database itself is still in device backups, but its key is ThisDeviceOnly, so a backup’s copy can’t be opened on another phone; Hidden files are Paperwise-encrypted. If marking a folder fails, the app logs it and tries again a few seconds later and at each return to the app. The install identifier (also your RevenueCat app user id) is kept ThisDeviceOnly too: restoring onto a new iPhone, or reinstalling Paperwise, gives a new one (use Restore Purchases). To keep a copy of your documents, turn on Paperwise’s encrypted iCloud backup (offered during onboarding); restoring an iPhone from a device backup alone doesn’t bring them back.
  • Encrypted database. All titles, extracted text, notes, tags and metadata live in a SQLCipher database encrypted with a random 256-bit key. The key is stored in the Keychain with WhenUnlockedThisDeviceOnly, so it is never synced to iCloud Keychain and never usable from a device backup. Small app state that says something about you (settings, the account and device ids and the device’s name, AI choices per document, an in-progress scan) is kept in this database too, never in a plaintext file.
  • Exports and scratch files. Temporary files created for sharing are deleted when the share sheet closes and at every launch. Camera originals and image-editing scratch files are deleted once a page is saved, after hiding, and at launch. So are leftovers of an interrupted import or downloaded email, and the file picker’s copies beyond the 20 files imported at once. Library thumbnails are stored on disk next to the pages, encrypted like them; Hidden thumbnails exist only in memory. No screen puts a Library image in the image library’s disk cache. The pages of a scan going to Hidden are never put in an image cache, and the caches are cleared when Hidden locks, when such a scan is saved or discarded. The Office import preview blocks remote content.
  • Search index. Search uses SQLite FTS5 with its secure-delete option, so the words of a document you hide leave the index at once rather than at a later merge.
  • Screenshots (best effort). iOS can’t stop every capture (for example another camera pointed at the screen). Hidden screens (including a Hidden card’s “Show code”), the share-link screen and scanning into Hidden block screenshots and screen recording. The app switcher shows a cover instead of your content whenever Hidden is open (and everywhere when the app lock is on); the cover is native and set as the app resigns active, but iOS takes its snapshot at a moment we don’t control. The cover fails closed: while the app is locked it stays up until you unlock, never dropped on a timer.
  • App lock. The “Lock after” timer and the 60-second grace for protected documents use a clock that keeps counting while the phone sleeps and can’t be changed by setting the date back. The time the app left is recorded natively as it goes to the background, and when you come back the app-switcher cover stays up until the app has decided whether to lock. At first run the lock is offered switched on (it turns on only after a successful Face ID). Restoring from an iCloud backup at first run keeps that offer; turning the lock off asks for Face ID first; if the app can’t read its settings (for example the encrypted database can’t be opened) it treats itself as locked; and on iPad no keyboard shortcut works and no link opens while the app is locked (a link waits until you unlock). With the lock off, opening the app doesn’t ask for Face ID; Hidden always does. Protected documents. There is no per-document switch: a document whose type is ID, Medical, Bank statement or Tax is protected, whether or not the app lock is on. Every grid, list, cover, shelf, folder cover, search result and picker shows a lock instead of its page, thumbnail, note lines or card face, and search never shows a snippet of its text. Outside the 60-second grace below, search finds a protected document by its title only, never by its text, so typing a query can’t test an ID or account number one character at a time. Opening, sharing, exporting, creating a share link, Quick Look and the in-app pickers ask for Face ID (or the device passcode) first, with a 60-second grace after a successful Face ID; a protected document never leaves by drag and drop, and isn’t offered in pickers that would send it elsewhere. Copying the link of a protected item asks for Face ID. Changing a document’s type (••• → Change type…) updates its protection at once; taking a document out of a protected type asks for Face ID first. Copies (Duplicate, Merge, Split, “Make it a scan”) keep their source’s type, so they stay protected. When a document is, or becomes, a protected type, it offers once to move it to Hidden; “Not now” is remembered per document. Making “Lock after” longer asks for Face ID (shorter doesn’t). A notification tapped while the app is locked opens only after you unlock. If the app can’t read whether the phone can authenticate, the lock stays on and still asks; it never turns itself off. On iPad the lock screen is the key window, so a hardware keyboard and its shortcuts can’t reach the app underneath.
  • Clipboard. Recovery keys, the account key, card numbers, secure-note text, share-link URLs and pairing codes you copy stay on this device (no Universal Clipboard) and expire after 60 seconds. They aren’t selectable text (use the Copy button), and the share sheet for a link offers no system Copy or Add to Reading List. Hidden note editors have no system edit menu (whose Copy would use the normal, synced clipboard): a long press on a line copies it the local, expiring way, and Paste is on the toolbar. A card’s or a protected note’s share sheet offers no Copy; recognised text and Ask answers of a protected document aren’t selectable. Paperwise removes its own clipboard copies when the app locks, when Hidden locks and whenever a space opens (whichever passcode, so an emergency unlock looks like any other), and never removes something you copied since. Other text you copy uses the normal clipboard.
  • Keyboards. Paperwise refuses third-party keyboard extensions everywhere in the app (iOS asks once per launch, and a keyboard with Full Access could send what you type off the phone); Apple’s keyboard, dictation and accessibility input work as usual. Fields in Hidden (note title and text, space and album names, recovery keys, passcodes) turn off autocorrection, spell check, predictions, AutoFill and smart punctuation, so nothing typed there feeds the keyboard’s learning; space and album names use Paperwise’s own prompt instead of the system alert.
  • Deleting the app is a fresh start. iOS keeps an app’s Keychain items after the app is deleted. So on the first launch of a new install, before any of its code reads a key, Paperwise deletes every Keychain item an earlier install left, in all of its access groups (the app’s, the share extension’s and the App Group’s): the database and Library keys, the account key, device key and sign-in session, the backup key, Hidden’s device secret, key slots and vault key, the capture and share-extension keys, and the install identifier. A marker file in the app’s own container (excluded from device backups) says the check is done; an app update or an offloaded app keeps its data and keys (the database is still there, so it isn’t a new install). A database restored from a device backup that this install can’t open is deleted, and the next launch starts fresh. Two counters stay, because wiping them would weaken a protection: Hidden’s wrong-passcode counter (§4) and the backup’s anti-rollback floor for Hidden’s key-slot record (§11). Neither holds a key or content. So after a reinstall nothing comes back by itself: the iCloud backup is restored only with its recovery key, and each Hidden space only with its own recovery key.
  • Erasing this iPhone. Signing out can remove this account’s documents from the iPhone, and deleting the account can erase everything on it, Hidden included (§5). These reuse the normal delete paths: rows, the search index with secure-delete, the files, thumbnails, caches, local share-link records and a scan in progress. Afterwards the database is checkpointed. An erase is never sent to the account as deletions.
  • Pairing codes are accepted only from Paperwise’s own scanner: a paperwise://pair link opens “Add a device”, which asks you to scan the code.

4. Hidden vaults

The passcode you type decides which space opens; the app never lists or counts spaces.

Hidden is for documents, photos, videos and notes that should not appear anywhere else in the app.

  • Separation. Hidden items are absent from All items, folders, the vault’s search, AI features, notifications and widgets. Search inside Hidden uses text recognised on the iPhone from the decrypted page in memory; the text is stored only inside the item’s encrypted record.
  • Saving into Hidden. “Save to Hidden” on a scan and Hidden’s “+” (Scan, Files, Photos) encrypt the item as it is saved. No Library row is created first and nothing is queued for Library OCR or AI; like every Hidden item, it then syncs and is backed up only as ciphertext under its own Hidden keys. If the save fails, the app shows an error and keeps nothing in the Library. While you scan into Hidden, the pages exist only in that scan session: it is never offered for “Resume” in the Library, isn’t saved for a restart, and is deleted with its pages as soon as Hidden locks (after the app is killed, at the next launch). “Move from the vault…” and “Move to Hidden” on an existing document start from an item in the vault by definition: its plaintext Library files are deleted and its words leave the search index at once (§3).
  • Deleting a Hidden item deletes it at once; it doesn’t go to Recently deleted.
  • Per-item keys. Each Hidden item has its own random data key (DEK). DEKs are wrapped by the vault key K_vault.
  • Unlock: Face ID by default, plus an optional Hidden passcode.
    • Face ID / Touch ID releases a device secret from the Keychain, gated by biometrics (biometryCurrentSet: adding a new face or fingerprint invalidates it). By default this is the only factor, so anyone who can pass Face ID on the phone can open Hidden.
    • The Hidden passcode or pattern (optional, free) is stretched with Argon2id.
    • With the passcode on, both are combined with HKDF to unwrap K_vault. Neither alone is enough, so knowing the phone’s passcode does not open Hidden when the Hidden passcode is on. Extra spaces and the emergency passcode need a Hidden passcode (and Pro).
  • Brute-force resistance. Every place that accepts a Hidden passcode (unlock, confirmations, adding a device, creating or restoring a vault, changing a passcode, setting or testing a duress passcode) goes through one gate with escalating delays (5 free tries, then up to one hour per attempt). The counter lives in the Keychain, so restarting or reinstalling the app doesn’t reset it (the fresh-install wipe keeps it on purpose, §3). Opening another vault doesn’t refill the free tries: more than 10 failures in 24 hours keep the delays on. Waits are measured on a clock that can’t be set (time since boot); a restart begins an unfinished wait again. Setting the date doesn’t look like a restart, even where iOS doesn’t give a boot id.
  • No “in use” answers. The app never tells you that a passcode is used by another vault. If two vaults end up with the same passcode, the one set last opens; the other is reached with its recovery key. Setting an emergency passcode never reveals whether the typed passcode belongs to another space. Refusing such a passcode (with a generic message and a wrong attempt) while saving any other one for free would turn the screen into a free oracle: a miss would cost nothing and say “saved”, so someone holding one space could test candidates for another space’s passcode without ever touching the unlock delays. Charging a failed attempt for every save would still leave two different answers (“couldn’t save” vs “saved”), so it would only slow the oracle down. Paperwise accepts a foreign passcode silently without writing it, which removes the oracle and keeps deniability: every save gets the same answer, the same attempt accounting and the same work (the passcode stretched once, a slot sealed, one Keychain write, one state write). For a passcode that opens another space (or its emergency slot) the sealed slot is replaced with random bytes before that write, so the other space is never shadowed or erased, and nothing on the device records what happened; the earlier emergency passcode is replaced either way. Checking the result with “Test a passcode” costs a wrong attempt every time, exactly like typing the passcode on the unlock screen, so it adds nothing to what anyone holding the phone can already learn. The screen asks you to choose a passcode you don’t use for another space (that passcode keeps opening its own space).
  • Multiple invisible vaults.
    • Up to 10 spaces, each with its own passcode, keys and emergency passcode. Each space holds two of the 20 key slots: its own, and one reserved for its emergency passcode from the moment the space exists (random data until one is set, and again after it is removed).
    • A fixed set of 20 key slots always exists, with unused slots filled with random data. Every unlock does the same work against all slots, so the number of vaults can’t be read from the disk or from unlock timing. A key-slot record, table or backup in any other shape is refused as damaged, never converted.
    • The app never lists or counts vaults. “Hidden is full” is reported like any other failure to save a passcode, and counts as a failed attempt.
    • Creating a new space is limited to one per 24 hours, asks for Face ID again, and counts as a full failed attempt in the passcode gate. Restoring a space with its recovery key costs the same (it shares the 24-hour limit), so it can’t be used to probe for free slots either; a slot taken for an emergency passcode is recorded in the space’s sealed state before it is written, so an interrupted save never leaks a slot. Someone who can open one space can still learn, slowly (at most one try a day), whether all the slots are used (§11). The 24-hour answer comes only after the same Face ID prompt and passcode work as every other answer, so it can’t be told apart by its speed, and every space gets the same Face ID prompts whether or not it has a reserved emergency slot.
    • Switching spaces never mixes them. Work that writes into Hidden (sync, imports, the private camera, items shared to Hidden, conflict copies) takes a private copy of the open space’s key when it starts, seals with that copy only, and checks right before every write that the same space is still open (a counter changes whenever Hidden locks or another space opens). If not, it stops; the item stays waiting, as ciphertext or in its source, for the space it belongs to. The device secret B is copied for each unlock, so a timer can’t wipe it mid-unlock, and an all-zero B is refused. Hiding, queued photo imports and the Hidden lists follow the same rule: a hide is tied to the space open when you confirmed it and changes a document only if it is still visible, an import job is filed only under the space that started it, and a list loaded for one space is never shown in another. Every slot operation works on a private copy of B and stops if its space closes.
  • Duress (“emergency”) passcode (optional, Pro): opens its vault and, before it appears, irreversibly deletes the keys of the others on that device and the duress passcode itself (“crypto-shredding”); their files are deleted right after, in the background, so a duress unlock takes about as long as a normal one. If the app is killed before that clean-up ends, it resumes at the next unlock of the surviving space (a mark inside its sealed, fixed-size state, written before any key is erased; the resumed clean-up also finishes erasing the other spaces’ key slots, capture key and records if the kill came before that); until then, what’s left can’t be opened by anything on the phone. Setting an emergency passcode uses the space’s reserved slot, so it never tells whether Hidden is full; right after the clean-up, sync re-reads the server’s list so the erased spaces’ items wait here as ciphertext at once instead of showing as “haven’t arrived yet”. The settings look the same whether or not a duress passcode is set, and we have closed the tells we know of (for example the sync “items haven’t arrived” count and hidden links reappearing in the vault’s Shared links); §11 lists what remains. A duress unlock erases the other spaces on this iPhone only. It deletes nothing on our servers or in your iCloud backup: the synced and backed-up copies of the erased spaces stay there, encrypted. Nobody (us included) can open them without that space’s recovery key (or, from an iCloud backup uploaded before the erase, its passcode), but anyone who later obtains the recovery key can bring the space back; keep it where a coercer can’t reach it. Copies synced or kept in the iCloud backup remain encrypted and recoverable with their recovery keys: the backup keeps a frozen copy of erased vaults’ recovery records (also while the backup is paused, and the previous entry of a slot erased twice). That frozen copy starts as random fillers instead of a copy of the live records, and a real entry (itself sealed under its recovery key or its vault key) looks exactly like a filler, so someone holding the backup key can’t compare it with the live records to learn that a space was erased.
  • Recovery key. A recovery key, shown once, can restore access if a passcode is forgotten. Its “Save as PDF / Print” job has a neutral name.
  • Erase Hidden on this iPhone (on the recovery screen) deletes every space on this phone; synced and backed-up copies stay. It asks you to authenticate first. Signing in, signing out and deleting the account never erase Hidden, except “Also erase everything on this iPhone” in Delete account, which says it includes Hidden and asks for Face ID (§5). Deleting the app deletes Hidden’s keys on the next install’s first launch (§3).
  • Instant capture. Photos taken from the Lock Screen, Control Center or the Camera Control button go through a separate capture extension. It encrypts each photo immediately to a public key and cannot read anything already in Hidden. With the Hidden passcode on, that key is shared by all vaults on the phone, so it doesn’t reveal which vault is open; a capture is imported by the next vault opened that receives captures.

5. Accounts and devices

Your account key is made on your iPhone, reaches a new device when you scan its code, and must sign every sensitive change.
  • Accounts are optional. Scanning, Hidden and local features work without one. An account is needed only for sync and the email Inbox. Secure links (Pro) work without one: a link made while signed out belongs to the install that made it (§11).
  • Sign in with Apple.
    • We verify Apple’s identity token against Apple’s published keys.
    • We store the Apple account identifier only as an HMAC under a server secret, never in clear.
    • Session tokens are random, stored only as hashes, and expire after 90 days of inactivity.
    • Signing in again never takes over a device record. Each device record carries the device’s own public key, registered when it was created. A later sign-in keeps that record only when the phone proves it still holds the matching private key, with a one-time challenge tied to that sign-in. The install identifier the app sends (which is also its RevenueCat app user id, so not a secret) proves nothing on its own. Each install also holds a secret our server issued at its first request, kept in the Keychain on this device only (our server keeps only its hash); sign-in requires it, so someone who knows only your install id can’t sign it into their account. Without the device proof the sign-in gets a brand-new device record, the newest on the account, and no other record changes. So someone with your Apple ID and your phone’s install id and public key (none of them secret) can’t take over or retire your phone’s record; at worst they add a new record of their own, which you can remove. A phone that proves its key gets its own oldest record back.
  • Account Master Key (AMK). A random 256-bit key created on the device that creates the account, in the same step. It wraps the keys of everything you sync and never reaches our servers in readable form.
  • Recovery key escrow. We store your AMK only wrapped by your recovery key. Without the recovery key, that record is useless to us and to anyone who breaches us.
  • Destructive actions need your account key, not just a sign-in. Each device derives a signing key from the AMK; our server keeps only its public half, registered together with the recovery escrow by the sign-in that creates the account (signed with that key), so no account ever exists without it. Deleting your account, removing another device, replacing or removing the recovery data, every change to your synced items (uploads, edits and deletions), and changes to the Inbox (its key, address, settings, approved senders, and removing delivered mail) must be signed with it, bound to that exact request, your session and the time. Someone who signs in with your Apple ID on another phone, steals a session, or breaks into our servers can’t do these things. A deletion requested from a device without the key is only scheduled: it runs after 7 days, your devices are notified (when notifications are on, including devices removed in the last 30 days) and show it in Account, on the sign-in screen and right after signing in, and any device with the key can cancel it.
  • What someone who signs in with your Apple ID can and can’t do. Sign in with Apple is enough to open a session on your account, so we assume an attacker may have it. They can: add their own device (the newest, up to the 10-device limit) and sign it out; see your account’s metadata (device names, models and public keys, storage used, Pro status, whether a recovery escrow exists, the account’s public signing key, a pending deletion); download your encrypted data (the recovery escrow, synced items, waiting Inbox items, the encrypted index), which they can’t open without your account key or recovery key; use your account’s Pro (AI actions count against your limits); and schedule the account’s deletion, which runs after 7 days unless one of your devices cancels it (your devices are notified). They can’t: read any of it; delete the account at once; remove your devices or sign them out; take over a device record or its seniority; change or remove the recovery data, your synced items or the Inbox; or register their own signing key.
  • Signed requests are accepted once. The server remembers a hash of each accepted signature until it leaves the ±5-minute window, so a captured signed request can’t be replayed. Devices adopt the server’s clock only within ±10 minutes of their own.
  • One subscription, one account. A device’s Pro also covers the other devices of the account it is currently signed into: the account of that install’s most recent session, as long as that session is live. Signing that iPhone into another account moves its Pro there; signing it out ends it for every account. Signing in doesn’t sign the install out of other accounts, because the install id is chosen by the app and that would let anyone sign your device out. Pro without a session (on an install that isn’t signed in), managing an install’s share links, and signing in all need that install’s server-issued secret, so an install id alone grants nothing.
  • A break-in on our servers (or at our providers) can delete or hold back what we store (synced copies, Inbox items, links, the account itself) and see the metadata in §9, but can’t read any of it, or forge your signed changes; replaying old versions is detected by your devices (§6; a brand-new device is the exception, §11). The copies on your devices are unaffected.
  • The signing key is set once, at creation. Before signing in, the app makes a candidate account key, recovery key, recovery escrow and signing key, and signs them. Our server keeps them only if this sign-in creates the account, in the same database write as the account itself; for an existing account they are ignored and discarded on the phone. There is no later “register a key” step that someone else’s phone could use, and no account without a key. If you lose every device and your recovery key, nobody can read your synced data or sign for the account: you can still delete it (scheduled, 7 days) and start again. A device that already holds an account’s key never replaces it when it signs in to that same account again, whatever the server answers, so a compromised server can’t make your phone drop its key.
  • Your documents and the account. Documents on the iPhone belong to whoever is signed in; there is no per-document owner. When someone signs in with documents already on the iPhone (signed out before, or signed in to another account), the app asks “You have N documents on this iPhone. Add them to your account?” before anything syncs (also when the only things on the iPhone are Recently deleted documents, folders or saved signatures): Add to my account keeps them (with Pro sync on they’re uploaded, encrypted on the iPhone as always), Erase them deletes them from the iPhone (after Face ID) before the account’s own documents arrive. Until the question is answered, sync and the email Inbox wait. Signing out offers Keep them on this iPhone or Remove this account’s documents from this iPhone (Face ID). The app counts them as safe in the cloud only when Pro sync is on, nothing is waiting to upload and the last sync finished. Otherwise it says why and that erasing deletes them for good, and Keep is the default. Hidden isn’t touched by either. Deleting the account is one screen: “This deletes your account and everything on our servers,” with two choices: Also erase everything on this iPhone (Hidden included, Face ID; the app then starts like a new install) and Also delete your iCloud backup (the encrypted backup in your iCloud, and the backup turned off). Without either, documents stay on the iPhone and the backup stays in iCloud. A kept backup is restored only with its recovery key, which still opens it after the account is gone. When this iPhone doesn’t have the account key, the server deletion is scheduled (7 days, as above); the two choices still apply to the iPhone and the backup at once. Erasing on the iPhone is never sent to the account as deletions. The iCloud backup is this iPhone’s copy: documents erased at sign-in or sign-out leave it at its next run. “Erase everything” stops the backup first, so it stays as it was.
  • Adding a device: 0. The existing device asks for Face ID first, and for the Hidden passcode when Hidden has one.
    1. The new device shows a QR code containing a one-time public key and a secret.
    2. Your existing device shows the new device’s name and asks before it seals the AMK to that key and authenticates it with the secret.
    3. Our server only relays the sealed box. Because the secret travels only in the QR code, a compromised server cannot substitute its own key.
    4. The new device checks the key against your account’s registered signing key before keeping it.

6. End-to-end encrypted sync (Pro)

Only your devices can open a synced item; our servers see the outside of the box.
  • Every item has its own key, wrapped by the AMK. Titles, text, tags, folder structure and page lists are encrypted together as a manifest. Images and files are encrypted as separate objects.
  • Your device uploads ciphertext directly to storage (Cloudflare R2) through short-lived signed URLs (5 minutes, each bound to the exact size reserved), under random object names.
  • Hidden items stay double-encrypted: their vault encryption sits inside the sync encryption. Each vault syncs under its own key, derived from that vault’s recovery key, never from the account key; so the account key alone (for example on a newly added device) opens no Hidden item. On another device a vault is restored with its recovery key. A newer visible version from another device never un-hides an item: the hidden version is kept and pushed back.
  • Deleting removes the stored objects. Destroying an item’s key makes any remaining copy unreadable.
  • Integrity. Authenticated encryption detects any modification; conflicting edits are kept as copies rather than silently overwritten, with one exception: when a document is hidden on one device while it is edited, still visible, on another, the hide wins and that visible edit is discarded (no copy, so the hidden content never comes back to the main Library). A conflict copy is made like any copy: it keeps the document’s type (so a protected document stays protected) and its “Never send to AI” mark, and its files are stored encrypted under internal names.
    • Every version and every deletion carries a tag made with a key derived from your AMK. Your devices apply a deletion only when its tag checks out; anything else is ignored and the copy on the device is uploaded again.
    • A deletion from another device moves a document to Recently deleted first (30 days), not straight to deletion.
    • Each device remembers the newest version it has verified, and an encrypted account index (whose counter only grows) lists every item’s latest version. So the server can’t serve an older version or an old deletion as if it were new, and it can’t roll an item back by stripping its tag: a version or deletion without a valid tag is refused, and your copy (if you have one) is pushed back on top. If the server stops serving an index the device has already seen, the app reports it. Items listed in the newest index a device has verified that don’t arrive are reported in the app. A server that freezes a device’s view (keeps serving it the same index and no new changes) can hide newer changes from that device without it noticing, until the device sees a newer index (§11).
    • Every upload and change is signed with your account key, so a stolen session can’t replace an item’s stored copy either. If the server ever serves a version your account key can’t open (or a bad tag), your device keeps its own copy and uploads it again (or, without a copy, reports the item as not arrived yet) instead of retrying forever.
  • Cloudflare additionally encrypts stored data at rest with its own keys. That’s a second layer; we don’t rely on it.

7. Paperwise Inbox: email (Pro)

Email is readable on its way to us; once it is sealed on arrival, only your devices can open it.
  • Your address. You get a private address such as amberk3x9qm7d@paperwise.app. You can change it at any time; the old one stops working immediately. Mail to an unknown or old address is accepted and silently discarded, just like mail to a live one that isn’t delivered (not from an approved sender, over the daily limit of 200 messages, or more than 20 attachments), so addresses can’t be discovered by probing. The daily limit counts only messages that are stored or held for your approval: the mail handler reads just the message headers (at most 256 KB) to apply your approved-senders list before it counts anything, so mail it discards never reads the body, is never stored and can’t use up the day’s 200 and crowd out mail you want. Discarded messages have their own, larger daily counter (2000); past it they are still discarded, and wanted mail is still delivered. (With “anyone” or “hold unknown senders”, unknown senders are held, so they do count toward the 200.) Bounces a live address still sends: when the Inbox is full (1 GB waiting to be downloaded), when the message can’t be read, and on a temporary storage error; a sender who triggers one of those can tell the address exists. A message over 25 MB bounces for every address alike.

  • Sealed on arrival. When an email arrives, our mail handler:

    1. checks the address and your optional approved-senders list. An approved sender counts only when the message is verified, as recorded by Cloudflare’s own mail servers: when they report a DMARC result, only a DMARC pass for the From domain counts (a failing or missing DMARC policy means unverified, whatever DKIM and SPF say); only when they report no DMARC result at all, a DKIM signature or SPF check for exactly the From domain. Results a sender writes into the message are ignored, and the result header is parsed strictly (a “pass” hidden in a quoted value or a comment doesn’t count; a header with two DMARC or SPF results, a second result header claiming to come from Cloudflare’s servers, or anything malformed, counts as unverified)
    2. seals every attachment, the message text and the sender, subject and file names to your Inbox public key
    3. stores only the sealed result
    4. drops the plaintext from memory

    The private key exists only on your devices.

  • Dangerous attachments (executables, scripts, disk images, OneNote files, shortcuts, web pages and SVG images) are not stored. Archives are kept, but their contents aren’t checked: open one only if you trust the sender. Our server marks such a part as an archive in the sealed item, but current builds don’t show that mark yet. Each sealed part is bound to its message: the encrypted manifest records the part’s ephemeral sealing key, and the app refuses a part that doesn’t match or isn’t bound. Sender authentication results (SPF/DKIM/DMARC) are passed along, and a sender is marked verified only under the rule above, so the app can flag unverified senders.

  • Notifications say only “New item in your Inbox” unless you choose to show the sender. Even then they show the sender’s name or address only, never the subject.

  • Changing the Inbox needs your account key: a new or turned-off address, settings, the approved-senders list and removing delivered mail are signed like other destructive actions (§5).

  • Honest limit: email is not end-to-end encrypted. The sender, their email provider and the moment of arrival see the message. Our promise is that after arrival nobody but your devices can open it, including us.

The key travels inside the link you send, after the #, so our server only ever holds a file it can’t open.
  • The key stays in the link. The document is encrypted on your device with a random key, which is placed in the part of the link after #. Browsers never send that part to any server.
  • Optional passphrase.
    • The passphrase is stretched with Argon2id, in the browser or app, and combined with the link key. Neither the link nor the passphrase alone is enough.
    • Our server stores only a hash of a verifier derived from the passphrase and the link key, so it can refuse wrong passphrases without learning the passphrase, and its records can’t be used to guess the passphrase offline (the link key never reaches us).
    • Ten wrong attempts lock the link permanently.
    • The link itself says which verifier version it uses (#k=…&v=2). The viewer and the app use only that version, treat a link without it as incomplete, and stop if our server claims another, so a compromised server can’t switch a link to a different check. Our server accepts only this version when a link is created.
  • Expiry is mandatory: at most 7 days, with no permanent links.
  • Optional limit on the number of opens. Opens are counted atomically, so a limit of 3 means 3.
  • Revoking a link, or deleting your account, deletes the encrypted file immediately. Account deletion also removes the links your devices made while signed out.
  • The link is built on your device from the share address built into the app, not from what our server answers. The app uploads and downloads link files only from our storage host (Cloudflare R2), whatever address our server sends.
  • Managing your links (listing, revoking, renewing) needs your account, or, on an install that isn’t signed in, that install’s server-issued secret (§5); knowing an install id isn’t enough.
  • The viewer is a static page with a strict Content Security Policy, no third-party scripts, no cookies and no analytics. If Paperwise is installed, links open directly in the app.
  • Rate limits. Opening links is limited per network address across all links, and per address and link.

9. What our servers can see

What our servers see depends on the path: nothing, the outside of a locked box, or a request you chose to send.
We can see We cannot see
Page processing (detection, clean-up, text recognition, file text, PDF export, PDF and capture tools) Nothing. It all runs on your iPhone The pages, the text, the files
AI features (Pro: Ask, Extract, Translate, Read with AI, “Name and file with AI” (naming and sorting), “Keep the layout” for Word/Excel, Count; and automatic naming of documents added after you turn on “Name and sort new scans”) The text of that document (naming, Ask, Translate, suggested questions), or a page image: Read with AI uploads the stored page (up to about 3000 px on its long side), which our server reduces to 2000 px for the provider; Extract and “Keep the layout” send 1600 px pages; Count sends the photo (reduced to 2000 px). Processed in memory and forwarded to our AI provider; no user or install identifier is sent to the provider. Titles, summaries, your locale and the Count object are passed like the document itself: as tagged data the model is told never to follow as instructions Hidden items and documents marked “Never send to AI” (never sent, no request made). IDs, medical, bank and tax documents are never sent by automatic naming or by a request for several documents (Account → AI helper → “Name N existing documents with AI…”, Select → “Name and file with AI”): those skip them, and only an action on the document’s own screen, after Face ID, sends one. We don’t store the content after the response
Office conversion (Word, Excel, PowerPoint export) Word/Excel: the document’s title, your locale (Word), the recognised text lines of each page, plus a 1600 px page image only for a page without recognised text, which our server reads with its own (non-AI) OCR (without Pro, on at most 5 such pages per conversion). PowerPoint: the title and the full page images. Processed in memory (seconds) and discarded Anything after the response is sent
Account An HMAC of your Apple identifier (we don’t ask Apple for your name or email); Apple’s refresh token for your sign-in, stored sealed under a server key and revoked with Apple when you delete the account (production requires the key that makes this possible, so every account has one; if the revocation keeps failing, the sealed token waits at most 3 days for a manual retry, below); devices (the name: your iPhone’s model by default, e.g. “iPhone 17 Pro”, and set to it again at each sign-in, replacing a name you chose; platform; public keys; install ids; push tokens, kept 30 days after a device is removed, for account-security notifications only; when it was created; last-seen time, updated at most once a day; removed devices); session hashes; a hash of each install’s server-issued secret (§5); your account’s public signing key; Pro status (cached as yes/no for 10 minutes; the last known value, with its time, for 30 days, used only within an hour of RevenueCat’s answer) Your Apple email; your keys
Purchases (RevenueCat) Apple and RevenueCat, our subscription provider, see your purchases and the app user id (your install id); the RevenueCat SDK also sends the IDFV, device model, OS version, locale and App Store storefront. Our server asks RevenueCat only “is this install Pro?”. Deleting your Paperwise account doesn’t delete RevenueCat’s records Your documents
AI usage counts How many AI actions (per day and month) and automatic namings (per day) each paying install used, and its AI cost per day (the cost of what our AI provider actually processed, at list price), keyed by a hash of its id; kept 35 days. Daily counts of Inbox mails per account (stored or held, and discarded, counted separately), also 35 days What the actions were about
Sync Number, sizes, versions and timestamps of encrypted items and objects, hidden ones counted with the rest: our server can’t tell which items are hidden, only that an encrypted item exists; which objects belong to which item (so roughly how many pages a document has); deletions and when they happen (deletion records are kept for the life of the account); which device made each change; the size of the encrypted index. For hidden items, the part of the record readable with your account key (not by us) holds the item’s kind, sizes and page dimensions (§11) Titles, text, folder names, images, file names
Inbox That a message arrived for an address, its size and its number of parts; per item, whether it is held for your approval; your Inbox settings (who may send, whether notifications show the sender) and your Inbox public key; your approved-senders list, in readable form (so the server can check it); your current and past Inbox addresses (past ones are kept, retired, so they’re never given to someone else, also after account deletion) Sender, subject, file names, contents (sealed on arrival, §7)
Share links Sizes, expiry, open counts, failed-attempt counts, which install or account made the link; IP addresses transiently for rate limiting (not stored) The document, its title, the link key, the passphrase
Logs (Cloudflare Workers Logs and the engine’s log) Per request: a request id, method, route template (ids removed), status, timing, a 12-character prefix of a hash of the install id, limiter, quota and AI-cost outcomes, and outcome flags (for example whether a sign-in created an account or a new device, whether a proof was valid, a share’s part count); per Inbox email: its size, part count, result and drop reason; error names; a few error lines also include the error’s message Request bodies, headers with credentials, document contents, email addresses, names or subjects

How long we keep things (server side, current code):

Kept
Inbox items (sealed) Until the app downloads them, at most 30 days (removed by a daily job, so up to about a day more)
AI usage counts, Inbox daily count 35 days (also after account deletion)
Pro status cache 10 minutes; last known value 30 days (used for at most 1 hour after RevenueCat answered)
Sessions Until 90 days without use; revoked sessions are deleted 30 days after revocation
Sync deletion records (“tombstones”) For the life of the account
Past Inbox addresses Indefinitely, without the account link after deletion
Share-link files Deleted at once when you revoke a link; about 3 minutes after the last allowed open or a lock; up to about 24 hours after expiry
Account deletion Immediate when your device signs it with the account key; otherwise scheduled for 7 days later and run by a daily job (up to about a day more). Documents on your iPhone and your iCloud backup are erased only when you choose so on the same screen (§5)
Your iCloud backup In your own iCloud, until you delete it (Account → Backup → Delete backup, or “Also delete your iCloud backup” when deleting the account). Deleting the app doesn’t delete it; restoring it needs its recovery key
Push tokens of removed devices 30 days after removal, for account-security notifications only
Failed background jobs (“dead letters”) 30 days for a manual retry; a failed Apple-token revocation, which holds the sealed refresh token of a deleted account, at most 3 days
Database history (Cloudflare D1 Time Travel) About 30 days of point-in-time history of all of the above (§11)
Logs Cloudflare Workers Logs’ retention for our plan
RevenueCat Under RevenueCat’s policy; not deleted with your account
Our AI provider See “AI provider” below

Nothing goes to AI until you allow it. The AI features (Account → AI helper) start off, and “Name and sort new scans” (automatic naming and sorting, Pro) is off. When you turn it on, an alert says: “The text of each scan, note, file, shared item and email you add on this iPhone from now on is sent to our AI provider to name it and put it on a shelf. What’s already in your vault isn’t sent. Our AI provider (OpenAI) doesn’t train on it, but may keep it for up to 30 days to check for abuse. It isn’t fully private while AI reads it. Paperwise learns what a document is (an ID, a bank statement) only from this, so new ones of every kind are sent.” Documents set to “Never send to AI”, and Hidden ones, are never sent. Automatic naming skips a document already of a protected type (ID, medical, bank, tax); only an explicit AI action on it, with its own consent sheet, sends it. AI naming can only add protection: it never moves a document out of a protected type (only you can, with ••• → Change type…, after Face ID). It applies only to items added on this iPhone after you turned it on (scans, notes, files, items shared to Paperwise and forwarded email that arrives), until you turn it off; it never names older documents, items synced from your other devices, items you un-hide later, or a document where you cancelled an AI consent sheet (that choice is remembered per document). To name existing documents, use an AI action on a document, or Account → AI helper → “Name N existing documents with AI…”, which shows its own consent sheet with the number of documents. Right before each request the app checks again that the document is still waiting for a name and isn’t Hidden or “Never send to AI”. “Name N existing documents” never includes protected documents (IDs, medical, bank and tax), and checks this again before each request; only an explicit AI action on the document itself can send a protected document. Ask checks the same conditions right before each question is sent, and a running Read with AI, Translate or Extract re-checks before each request and stops when you leave the screen. AI results (names, page text, Extract fields, translations, chat answers) are written only while the document may still use AI: not Hidden, not “Never send to AI”, not in the trash, and with AI features on; a result that arrives after one of these changes is discarded. A hide in progress blocks new AI requests for its documents, and a hide refuses while a request is running. A copy made from other items (Duplicate, Merge, Split, a file copy, “Make it a scan”, a sync conflict copy) inherits the strictest marks of its sources: “Never send to AI”, and a protected type, so a copy of a protected document stays protected. There is no automatic AI text recognition: low-confidence pages keep the on-device result, and AI reads a page only when you tap “Read with AI”.

You are told before you send anything readable. The first AI action on a document in an app session shows: “AI is on for this document. Its text is sent to our AI provider to answer you. Our AI provider (OpenAI) doesn’t train on it, but may keep it for up to 30 days to check for abuse. It isn’t fully private while AI reads it.” (Continue · Cancel · “Don’t ask again for this document”). After Continue, further AI actions on that document don’t ask again until the app restarts; “Don’t ask again” makes that permanent for the document. Automatic naming with “Name and sort new scans” doesn’t show this sheet per document: the alert you accepted when turning it on is the notice. The first Word, Excel or PowerPoint conversion of a document shows a similar notice: the pages are sent to our server to build the file and deleted right after; they aren’t end-to-end encrypted while that happens. Office files are imported on the iPhone only; they never go to our server. A document that has used AI shows a small AI badge. Hidden items and documents marked “Never send to AI” never show the notice and never make a request.

AI provider. AI requests go to OpenAI’s API (Responses API) with storage turned off for each request (store: false), and without any user or install identifier. OpenAI doesn’t use API data for training. We don’t have a zero-data-retention agreement yet: under OpenAI’s standard API terms, request content may be kept for up to 30 days for abuse monitoring before deletion. We will update this document when a zero-data-retention agreement is in place.

10. Network and infrastructure

  • TLS everywhere: HTTPS only (plain HTTP is redirected), TLS 1.2 or newer (TLS 1.3 preferred) and HSTS for a year on every paperwise.app host, set at the domain level (the website’s header also carries preload, and the domain will be submitted to browsers’ HSTS preload list). CAA records allow only the certificate authorities Cloudflare uses for us (Let’s Encrypt, Google Trust Services, SSL.com) to issue certificates for paperwise.app, and report refused requests to security@. The app only uploads to and downloads from https URLs on Cloudflare R2’s storage endpoint. Public-key pinning of our API in the app at launch (§11).
  • Gateway. Requests pass a gateway that checks the app’s key, rate limits, subscription status and, for Pro without a session, the install’s secret (§5), then forwards work to the processing engine with a signed header. The engine rejects anything not signed by the gateway. The signature also covers the AI budget reserved for the request and the verified identity its work is tied to (both below); if the engine fails unexpectedly, its error still reports what AI work was done. Before any check the gateway normalises the request path (one spelling per route; any percent-encoding is refused), so a trailing slash or another letter case can’t slip past a limit or the subscription check. Each engine route accepts exactly one content type, checked by both the gateway and the engine before the body is read: JSON (application/json, optionally charset=utf-8), or form uploads only for the routes that take a file; anything else is refused. The gateway drops client-set proxy headers (X-Forwarded-*, Forwarded, X-Real-IP) and the engine trusts none. The API sends no cross-origin (CORS) headers, so no website can call it from a browser, and production answers only on api.paperwise.app and share.paperwise.app (no workers.dev address), where the domain’s HSTS, CAA and TLS settings apply. The engine’s API description pages are off behind the gateway.
  • Processing engine. The engine handles only AI requests and Office conversions you start. It has no page-processing, OCR, file-text, PDF or capture-tool routes (the app does that work on the device), and the gateway forwards nothing else. Each request’s image work runs in a separate, memory- and time-limited worker process whose results are size-capped; the engine and the gateway also cap how many requests one install, and one network, can run at once. The worker processes can’t read the main process’s memory or environment (they and the server are non-dumpable, and secrets are removed from their environment), can’t gain privileges (no_new_privs), can’t write any file, have a small limit on open files and no core dumps, and don’t start at all if their memory ceiling can’t be set. They are isolated as processes only: no system-call filter, and they run as the same user as the server, send their results back in a strict non-executable format checked against a schema, and are replaced after 200 jobs. Each worker is tied to one owner: it only ever processes that owner’s files and is replaced before anyone else’s work can use its slot, so a malicious file that exploited a parser bug couldn’t see another user’s documents. The owner is an identity the gateway verified: the signed-in device’s session (so knowing an install id doesn’t get you onto that device’s worker), or the install for a device that isn’t signed in. Fresh workers are prepared in the background when the engine is quiet. Every request field has a size limit (at most 50 pages, 1000 text lines per page, 36 MB for a Word or Excel request) and oversized bodies are refused before they’re parsed. Each route accepts exactly one content type (§10, Gateway), and the engine counts the values of every non-upload body before parsing it. Work stops when the app disconnects (AI calls included), an AI conversion has an overall two-minute deadline, and without Pro the server reads at most 5 pages without text per conversion.
  • AI cost limits. Besides your daily and monthly AI action limits (20 a day, 300 a month), a global daily budget and a per-subscriber daily cap count cost. They count real cost: before forwarding a request, the gateway reserves its worst case (every call to our AI provider at its largest possible input and its answer-length ceiling, every allowed retry; a translation in at most one part per 6,000 characters), or only what is left of the day when that is less, and signs that amount to the engine. The engine retries itself (never the provider’s library behind its back) and starts a call, or a retry, only while the request’s spending so far plus the worst case of the calls in progress fits that amount; otherwise the request stops with “today’s AI limit”. Afterwards the gateway settles on the token counts our AI provider reported (retries included; a call whose count is unknown, such as a timeout, at its worst case). So a subscriber’s cap and the global budget bound what our provider actually bills. An action that fails is given back to you, but the cost of work already done still counts toward those budgets, so failed requests can’t be repeated for free. Each request has a ceiling on the length of the AI’s answer, a long translation is sent in parts at once so it finishes instead of timing out, and the target language must be a language code. AI actions are reserved before any image is decoded. No subscriber may use more than a tenth of the global daily budget, so a few accounts can’t pause AI for everyone (production: $0.50 per subscriber, $5 in total per day). The engine can’t change what is counted: the accounting headers it returns replace any a worker set, a worker may set only an allowlisted few, and the gateway charges a request in full if those headers are duplicated or unreadable.
  • Providers. Infrastructure runs on Cloudflare (gateway, database for counters and the sync log, R2 storage, email routing) and on stateless processing containers, also on Cloudflare. Secrets are stored as encrypted platform secrets, never in code.
  • Deployments. Production is deployed only from release tags, with a deployment token scoped to each environment and given only to the deploy steps; development and production tokens are separate. Deploys are split: a job without credentials installs the dependencies and builds the Worker bundles, the website and the engine’s container image (its base images pinned by digest); a separate job holding the token installs only a lockfile-pinned deploy tool, without install scripts, and uploads that prebuilt artifact unchanged. After a deploy, a health check calls a fixed address per environment. CI runs only tools installed from the lockfiles. Every CI install, the app’s checks included, runs without dependency install scripts and installs exactly the versions and checksums recorded in the committed lockfiles (npm ci), for the app and the website too. Testing overrides (the development Pro allowlist) are development-only secrets, never in code, never set on production, and ignored by the code on production.
  • Email domain. paperwise.app publishes a single DMARC record set to reject, for the domain and its subdomains (p=reject; sp=reject, relaxed alignment), so receivers refuse mail that claims to come from Paperwise unless it is authenticated and aligned with the domain. The record is managed as code, with a guard that stops a second DMARC record from ever being created (two records would cancel the policy). It and the team mail routing can’t be deleted by an infrastructure run: their settings are required inputs (a run without them fails instead of planning a deletion) and the resources are protected from destruction. Paperwise itself sends no email; inbound mail goes only through Cloudflare Email Routing. Team replies from @paperwise.app addresses are sent only through a service that signs them with a paperwise.app DKIM key, never through a personal mailbox’s “send as”. security@ and abuse@ are delivered to a person, not to the Inbox handler.
  • Shared networks. (Accepted trade-off) Some limits apply per network address (all requests, sign-in attempts, unknown-subscriber lookups, opening share links, engine work in flight), because install ids are chosen by the app and can’t be trusted on their own. Phones behind the same carrier-grade NAT or company network share those limits, so a busy shared network can see “slow down” answers sooner. We keep them, with generous values, because per-install limits alone could be bypassed by inventing install ids; account and subscriber limits stay per account.

11. Known limitations and residual risks

What Paperwise can’t protect against, and what is still weaker than we’d like. Items marked (TestFlight) concern the pre-release builds only.

Out of our hands

  • A compromised device. Malware or a jailbreak with control of your phone while Hidden is unlocked can see what you see.
  • Coercion. Someone who forces you to unlock can see the vault you open. The duress passcode and invisible vaults reduce, but cannot remove, this risk.
  • Losing every key. If you lose all your devices and your recovery key, your data cannot be recovered by anyone, including us. This is the price of end-to-end encryption.
  • Share recipients. Anyone who opens a link can save a copy. Expiry and revocation stop future opens, not copies already made.
  • Email in transit, as described in §7.
  • AI and conversions you start. While an AI request runs, its text (or page image) is readable by our processing engine and by our AI provider; while a conversion runs, its pages are readable by our processing engine. We don’t store them, and AI requests are sent with storage turned off, but they are not end-to-end encrypted at that moment, and without a zero-data-retention agreement our provider may keep them for up to 30 days for abuse monitoring (§9). With “Name and sort new scans” on (Pro), documents you add are sent without a sheet each time. That is why nothing goes to AI until you allow it, the app asks first, and AI documents carry a badge.
  • Metadata listed in §9 (sizes, times, counts, devices, usage counts) is visible to us, and some of it (Inbox addresses, sync deletion records) outlives the data it describes.
  • What a server can still do to sync. It can’t forge, replay or roll back your items, but it can delay or withhold changes. The app reports items listed in the newest index it has verified that don’t arrive, and a missing index, but a server that keeps showing a device a frozen but consistent view isn’t detected (no freshness signal yet). A brand-new device trusts the first account index it receives.
  • Deleted server records linger in database backups. Our database provider keeps point-in-time history (Cloudflare D1 Time Travel) for about 30 days. Records deleted or replaced during that time, including old recovery-key escrows, wrapped item keys and encrypted index versions, can be restored by us or by someone who compromises our provider account. They stay encrypted, but changing your recovery key does not change your account key: an old recovery key opens an old escrow from that history for about 30 days. “Crypto-shredding” by deletion therefore holds against an honest server at once, and against our own history only after that window. Rotating the account key in place is planned.
  • Removing a device doesn’t change your keys. It ends the device’s sessions, but a device that already has your account key keeps it until the account key is rotated (planned).
  • Apple and the operating system are part of the trust base: the Secure Enclave, Keychain, Face ID and iOS itself.
  • Hidden’s remaining side channels. Someone who can examine the phone’s decrypted database sees how many hidden items exist (not which vault owns them). The key-slot record’s internal write counter and slot generations grow with every slot write, so someone who has the device secret B (it needs Face ID) and two copies of the record can tell that a duress unlock rewrote many slots; slot records are bound to their purpose but not to their position (each carries its own target); a duress unlock does the same record writes as a normal one before the vault shows, and the clean-up that follows in the background takes about 30–80 ms more. iCloud’s own storage figure for Paperwise includes backup copies of erased vaults, while the app’s totals count every vault together. A restart restarts an unfinished passcode wait (it never shortens it). Shared links made from hidden items are listed without their names; after a duress unlock that list is cleared, but the links themselves stay valid until they expire (at most 7 days) unless revoked.

On the iPhone

  • The Library key is on the phone. Library files are encrypted with a key in the Keychain that isn’t behind Face ID (§3), so the app, sync and the backup can work in the background. Something that can run code as Paperwise on an unlocked phone (malware, a jailbreak) can ask for that key, as it could for the database’s; a copy of the app’s files alone, without the Keychain, is unreadable. While you scan, view, edit or share, the readable copies listed in §3 exist for that moment. Keep your most private papers in Hidden, whose keys also need Face ID (and your Hidden passcode, if you set one).
  • Hidden opens with Face ID alone by default. Without a Hidden passcode, Hidden is exactly as strong as the Face ID check on that phone. Add a Hidden passcode or pattern for a second factor that the phone’s own passcode can’t replace.
  • Counting spaces from inside a space. Someone who can open one space can still learn whether fewer than two of the 20 slots are free (that is, whether 10 spaces exist), by trying to create a new space (at most one try per 24 hours, each counted as a failed attempt). Setting an emergency passcode reveals nothing about free slots (it uses the space’s reserved slot). The screen says an emergency passcode replaces any earlier one, whether or not there was one.
  • Screenshots and the app switcher are best effort (§3). iOS doesn’t let an app block every capture, and the app-switcher snapshot is taken by iOS. With the app lock off, the snapshot can show what was on screen in the vault (the cover goes up only while the lock is on or Hidden is open or on screen).
  • Third-party keyboards are refused everywhere, not only in Hidden: a deliberate trade-off (a keyboard chosen earlier would otherwise still be active when you open Hidden).
  • Video to AirPlay. Playing a Hidden video to an AirPlay receiver sends it to that receiver, as with any app; the receiver isn’t Paperwise’s.
  • Secrets in JavaScript memory. Passcodes, some keys and the database key exist as JavaScript strings for a moment and can’t be wiped from memory deterministically.
  • RevenueCat’s copy of the install id. The RevenueCat SDK keeps the app user id (your install id) and its purchase cache in the app’s standard preferences, which iOS includes in device backups. The install id isn’t a secret and proves nothing on its own (§5): Pro without a session, share-link management and sign-in also need the install’s server-issued secret, which is kept on this device only and can’t be restored onto another iPhone; the app sets it again at every launch, so a backup’s copy restored onto another iPhone is replaced by that iPhone’s own id (use Restore Purchases). It does tell someone who reads a device backup which install made it.

Sync, Hidden and the cloud

  • Erased spaces and the iCloud backup. The backed-up key-slot record carries a counter inside its encryption. Any erase (a duress unlock, turning the Hidden passcode off, deleting a space, erasing Hidden, erasing everything with Delete account), and setting, replacing or removing an emergency passcode, raises a floor kept in this iPhone’s Keychain past every record already uploaded, and uploads the new record at the next chance even when nothing else changed. A restore takes the newest record, refuses one below the floor, and never overwrites a key-slot record already on the phone. Limit: another iPhone restoring from the backup before the post-erase upload has run still receives the older record, with the erased spaces’ slots (each still needs its own passcode, or its recovery key).
  • What a reinstall leaves behind (accepted trade-off). Deleting the app deletes every Paperwise key on the next install’s first launch (§3), except two counters: the anti-rollback floor above and Hidden’s wrong-passcode counter (§4). Wiping the floor would let “delete the app, reinstall, restore an older backup” bring back the slots of spaces erased under duress. Relying on you to delete the iCloud backup instead would make that protection depend on a step people forget. So the floor stays. The cost: someone who can read this iPhone’s Keychain learns that Paperwise was installed before and roughly how many times Hidden’s key-slot record was rewritten. The counters open nothing and aren’t linked to an account; erasing the iPhone (Settings → General → Transfer or Reset iPhone) removes them. If the fresh-install check can’t finish (the phone locked during a background launch), it runs again at the next launch. An update, or an app iOS offloaded, keeps its data and keys: only a new install with an empty container is wiped.
  • Hidden item metadata under the account key. For each synced hidden item, the part of its record encrypted with your account key holds the item’s kind, sizes and page dimensions (none for scans saved straight into Hidden). Anyone with your account key (any of your paired devices, or someone who has your recovery key) can therefore count your synced hidden items and see their kinds and sizes, though not open them. We (without your account key) see only counts and sizes (§9).
  • Server-side protocol limits (details in §5–§8 and the cloud audits): a live Inbox address still answers some mail with a bounce (Inbox full, unreadable message, temporary error; §7), so it can be told apart from an unknown one. Signed download URLs are valid for 15 minutes.
  • The approved-senders list is stored readable so our mail handler can check it (§9).
  • Subscription checks during a RevenueCat outage. When RevenueCat doesn’t answer, a subscriber’s last known status is used only if RevenueCat confirmed it within the last hour; after that, Pro features (AI, sync, links) pause until RevenueCat answers again, rather than stay on for an ex-subscriber. The Inbox keeps delivering mail on the last known status (up to 30 days), so an outage never drops a subscriber’s mail. Looking up an unknown install creates a record at RevenueCat; such lookups are limited per network address.
  • AI cost accounting uses list prices. The budgets count tokens at our provider’s list price for the larger model, so they are conservative; an engine that misreported its usage could only be our own code.

Links and the clipboard

  • A share link is its own key. Whoever has the whole link can open it until it expires, is used up or is revoked. The app copies links to a local-only clipboard that expires after 60 seconds, but anything you paste them into (a chat, an email) keeps them, and an app you share a link to may keep it or copy it to the normal clipboard.
  • Links made while signed out belong to the iPhone that made them. While you’re signed out the app doesn’t list them (it shows “Sign in to see your links”); when you sign in they join your account, and your devices list and can revoke them there. Until then they stop working at expiry (at most 7 days); deleting your account also deletes them.
  • The browser viewer comes from our server. A link opened in a browser runs the viewer page our server sends; a compromised server could send one that leaks the link key. The Paperwise app opens links with its own code instead.
  • A link’s id can be used to block it. (By design) Our server sees only a link’s id, never its key, so it can’t tell the recipient from someone else who has the id (for example from a log or a shared screen): such a person can use up a link’s limited opens or lock it with wrong passphrases. That blocks the link but never reveals the document; make a new link if it happens.

Release readiness (TestFlight)

  • No certificate pinning yet. Current builds connect to our development gateway without public-key pinning (ordinary TLS validation only). Pinning (iOS NSPinnedDomains, for the API host only, not share links or storage) is configured at release; every Release build refuses to build without pins, the production host and CloudKit Production, and refuses development share links, unless it is explicitly built as a TestFlight build. The same build step also refuses an App Store build that still claims the development share address or contains development-client settings, so the rule holds without our upload script. A TestFlight binary is marked as one and shows “Test build” in Account → About; it is for testers only and is never submitted for App Store review.
  • How App Store builds are made. Only through app/scripts/appstore.sh: it prebuilds with PAPERWISE_RELEASE=1 and no TestFlight variables, with the production API and the production app key, and the release guard runs both at prebuild and as a build phase inside Xcode: production API host, at least two pins, CloudKit Production, no development share address, no development-client settings, no TestFlight marker, and an app key that isn’t the development key (compared by hash, never printed; matched against the production key’s hash when it is known). The archive is checked again (check-release-plist.sh --production, which also refuses a JavaScript bundle containing the development key) before upload. A TestFlight build (testflight.sh) must carry the PaperwiseTestFlight Info.plist marker and the app’s TestFlight flag, so a TestFlight binary fails every App Store check; the TestFlight switch no longer skips the checks on its own.
  • iCloud backup environment. The backup’s CloudKit container still uses Apple’s Development environment for TestFlight; it moves to Production before release.

12. Planned

  • Independent security audit. We intend to commission one and publish the report.
  • Zero data retention with our AI provider (§9).
  • Certificate pinning in production builds (§11).
  • Account key rotation (§11), so removing a device or changing the recovery key also changes the account key.
  • Hidden metadata moved under each vault’s own key (§11).

Shipped since earlier versions of this document: the free encrypted iCloud backup (opt-in; CloudKit in your iCloud private database; encrypted on your device with a key protected by your recovery key, so Apple sees record counts, sizes and dates but can’t open your documents) and iPad support with the same protections.

13. Reporting a vulnerability

Write to security@paperwise.app with details and steps to reproduce. We acknowledge reports within 3 business days. We ask that you give us reasonable time to fix issues before disclosure, and we will credit researchers who wish to be named. Please don’t access other users’ data or degrade the service while testing.