Security at PRTO
PRTO encrypts your task content on your device before it reaches our servers. This page states exactly what that covers, what it does not cover, and where the boundaries are.
The claim
No PRTO-held secret, alone or combined with the database, can decrypt user content.
It holds because your content is encrypted with keys that never leave your devices in a form we can use. Encrypted rows sit in our database as ciphertext, and our servers relay ciphertext. There is no code path by which PRTO infrastructure produces plaintext from an enc:v1: envelope.
What is encrypted
The following fields cross our API as enc:v1: ciphertext, and sit in our database that way once written or migrated (see Progressive encryption coverage below for the legacy-row window):
| Entity | Encrypted fields | Plaintext metadata (never encrypted) |
|---|---|---|
things |
title, description |
id, areaId, parentId, status, order, depth, priorities, deletedAt, timestamps |
notes |
title, content |
id, thingId, noteType, timestamps |
areas |
title, description |
id, icon, color, order, priorityDepth, deletedAt, timestamps |
Content is encrypted under a per-area key. The server stores and relays these values opaquely: it does not decode them, validate them, or transform them.
What we can still see
Encryption covers content, not existence. The claim is "we can't read your content," not "we see nothing." We can see:
- The access graph. Which accounts hold a key grant for which areas. The grants themselves are ciphertext, but the shape of who-shares-what is visible.
- Organization membership and roles. Who belongs to which org, and at what role.
- Operational metadata. Timestamps, ordering, status and priority values, deletion markers, area icons and colors, note types. This metadata is required to run the product and stays plaintext by design.
- Security-event metadata. Activity types, and the requester IP address and user agent that appear in security notification emails (for example, when someone requests a recovery share release).
- Rate-limit and abuse counters. Request counts per user and per IP, used to keep the service up.
If a feature would need your plaintext on our side, it does not get built. Server-generated email, for example, is contentless by rule: our messages can say "you have updates" but never what the updates are.
How keys work
You do not need to understand cryptography to check this model:
- Your passphrase (or 12-word recovery phrase) feeds a key derivation function and produces your master key, on your device only.
- Your master key is never used on content directly. It wraps per-area content keys, and those wrapped keys are what we store.
- Each area key encrypts the content fields listed above. Wrapped area keys travel to us as ciphertext; your master key does not travel at all.
Because every layer below your passphrase is a wrap, rotating your passphrase or recovering your account never requires re-encrypting a single task.
Recovery: what it can and cannot do
Losing every copy of your secrets means losing your data. That is the deal a real "we can't read it" system makes, so we built recovery that keeps the claim true instead of weakening it.
When you set up encryption, your client splits a recovery secret into three shares, any two of which can rebuild it:
- Share 1: your recovery phrase. Derived from your 12-word phrase. Never stored anywhere.
- Share 2: your recovery kit. A file you download once and keep somewhere safe.
- Share 3: the escrow share. Held by PRTO. Exactly one share, never two; the API rejects anything else.
One share alone is mathematically useless, including the one we hold. Reconstruction happens on your device; our server never sees two shares.
The escrow share is never released instantly. A release request starts a 24-hour hold. We email you and show an in-app notice with a cancel link; canceling voids the request. The hold is the security feature: a hijacked session can ask for the share, but it cannot get it before you have had a full day to say no. Claims are one-shot, and a released share should be re-minted afterward.
What recovery cannot do, stated plainly:
- If every user-held secret is lost (no passphrase, no phrase, no kit, no surviving device) and no usable share set exists, recovery is cryptographically impossible. Permanent. Any product that promises otherwise is holding a key it claims not to hold.
- Escrow sets minted before the release flow existed have no releasable share (the API reports this as
ESCROW_LEGACY). Kit-only recovery for those sets is a dead end until the set is re-minted from an unlocked session. - Claiming burns the release. Once you claim the escrow share it cannot be claimed again; if the kit you combine it with turns out to be wrong or superseded, you start a new 24-hour hold. Check you have the right kit before claiming.
- Recovery protects against future loss only. Enabling it requires an unlocked session; it cannot be provisioned from a locked account.
Progressive encryption coverage
Content written before encryption shipped is migrated lazily, on your device: rows re-encrypt as they are read, and a sweep after each unlock re-encrypts the remainder. Until a user next logs in, their oldest rows may still be readable in our database. Coverage converges to ciphertext-only over logins; we do not claim it is already complete.
Infrastructure notes
- Decryption happens in your browser only. Content is fetched and decrypted after the page loads on your device; plaintext never transits our server-side rendering or serverless function memory.
- Error reports are scrubbed. Our error tracking drops anything shaped like an encrypted envelope and redacts content-field values. If the scrubber itself fails, the event is dropped rather than sent.
- Session replays are masked. Analytics replay masks elements that render user content, marked with a dedicated attribute on every content surface.
- Our servers store content opaquely.
enc:v1:values are never parsed, sanitized, logged, or embedded in notifications by server code.
The primitives
For reviewers who want to check the math:
- Key derivation: Argon2id
- Content encryption: XChaCha20-Poly1305 authenticated encryption,
enc:v1:envelopes with per-purpose domain separation - Key wrapping and sharing: Curve25519 keypairs and sealed boxes
- Recovery: Shamir secret sharing, 2-of-3, over GF(2^8)
- Implementation:
libsodium-wrappers-sumo, fail-closed throughout: a malformed ciphertext produces an error state, never a silent plaintext fallback
For how we handle the personal data that is not encrypted content (account details, billing, feedback you send us), see the Privacy Policy.