Whitepaper · version 1.0 · September 2026
From a password to a permanent hash
The cryptography of crycrypt, specified as it is implemented: how keys are derived and wrapped, how files are encrypted, how the vault lock works, what the server can and cannot see, and how a file is kept for good on Arweave. Every construction names the module that implements it; every figure is computed from the parameters in the code.
1Goals and threat model
crycrypt stores arbitrary files for a user, and is built around five properties.
- Confidentiality against the crycrypt server, the storage provider and anyone holding a copy of the database: contents, names, types and folder structure are encrypted before they leave the browser.
- Integrity: every ciphertext is authenticated and bound to its context — item identifier, chunk index, purpose — so nothing can be swapped, reordered or truncated unnoticed.
- No self-opening: the server holds key shares, never enough to derive a vault key.
- Indistinguishable failure: an unlock attempt reveals whether it succeeded, never which factor was wrong.
- Stated limits: availability, code delivery and freeze deadlines depend on the operator, and the document says so where it matters (§14).
| Adversary | Can | Cannot |
|---|---|---|
| Database thief | read every table offline | derive a vault key: it needs the lock secrets (≥ 180 bits) behind Argon2id |
| Curious server | log requests, sizes and timings | decrypt names, contents or keys |
| Storage provider | read, drop or alter blobs | read them; any alteration fails the GCM tag |
| Network attacker | see TLS metadata | anything else (HTTPS assumed) |
| Active malicious server | serve modified JavaScript | — this is the limit of cryptography in a web page (§14) |
- authHash, the lock verifier, envelopes, encrypted metadata and ciphertext streams — never a key, never plaintext.
- Key shares, released only once the required factors are proven.
- Ciphertext relayed to Gofile with the operator's token; Gofile never talks to the browser.
- Signed Arweave items, relayed to Turbo through a proxy and kept nowhere: ciphertext, or the public file in the clear.
- Permanent copies read back from public gateways, straight to the browser.
2Notation and primitives
is concatenation, the empty string, AES-256-GCM of under with additional authenticated data and, unless stated otherwise, a fresh random 96-bit IV. is the server's HMAC secret (SERVER_SECRET). Every sub-key is produced by HKDF with a 32-byte output, which is a single HMAC block:
| Primitive | Used for | Parameters | Implementation |
|---|---|---|---|
| Argon2id (RFC 9106) | password, lock and recovery material | m = 64 MiB, t = 3, p = 1 | hash-wasm |
| HKDF-SHA-256 (RFC 5869) | every sub-key, one label each | 32-byte output | WebCrypto |
| AES-256-GCM (SP 800-38D) | envelopes, metadata, file chunks | 96-bit IV, 128-bit tag | WebCrypto |
| HMAC-SHA-256 | server-side verifiers and codes | key = S | node:crypto |
| scrypt (RFC 7914) | stored password fingerprint, PIN | N = 2^14, r = 8, p = 1 | node:crypto |
| Ed25519 (RFC 8032) | freeze signatures | — | @noble/curves |
| TOTP (RFC 6238) | second factor | HMAC-SHA-1, 30 s, 6 digits | node:crypto |
| RSA-PSS 4096 (RFC 8017) | Arweave data items | SHA-256, 32-byte salt | WebCrypto |
| SHA-384 | Arweave deep hash | — | WebCrypto |
Random values come from crypto.getRandomValues in the browser and node:crypto on the server. The primitives live in lib/crypto/, and the same code runs in both places.
3Accounts, sessions and registration
3.1 The password never leaves the browser
The server stores . A leaked table gives neither the password nor a value that can be replayed, and HKDF being one-way, authHash does not lead back to . For an unknown address the pre-login step returns a deterministic decoy salt, truncated to 16 bytes, so that the salt endpoint does not tell which addresses exist.
The interface accepts only a strong password, at sign-up as on a reset: 12 characters or more, upper- and lower-case letters, a digit and a symbol, and no common pattern — a well-known password at the start, a keyboard or number run, one character four times in a row. The rule lives in the browser alone: the server only ever receives authHash, so it has nothing to check it against. The password opens the session, never the vault (§3.3).
3.2 Second factors
A TOTP code is accepted for the counters , and only if : a code already used cannot be replayed, even within its 30 seconds. A blind guess therefore succeeds with probability at most . Email codes are six CSPRNG digits, stored as
valid ten minutes and for five attempts: a guess succeeds with probability at most per code.
3.3 Registration ends with the lock
Nothing is written to the accounts table until the lock of §5 exists. The first step only creates a pending sign-up — salt, scrypt of authHash, TOTP secret and the HMAC of an email code — bound to a random 256-bit token kept in an HttpOnly cookie restricted to /api/auth/register; the database keeps , not the token. The second step checks the authenticator code and the email code, and only then draws the salts and shares of the lock. The third step is the lock itself: the browser draws , the seed, the three keys and the recovery key, the user chooses whether an email code is asked at each opening, the browser seals under both paths, and the server writes the account, its vault, its lock, its recovery envelope and its free subscription in one SQLite transaction (lib/server/registration.ts). No phone number is asked, at sign-up or later.
A sign-up abandoned at any point expires on its own — after an hour, or two once the codes are proven — and leaves the address free. Three properties follow: there are no ghost accounts; nobody can reserve someone else's address under their own password, since that now requires reading its mailbox; and a vault never exists without its lock — is never wrapped under the password, which only ever opens the session. The server enforces the last point too: every vault route refuses a vault that has no lock.
3.4 Sessions and devices
A session token is 256 random bits; the database stores its SHA-256. A session is either at the password stage (15 minutes, only the second factor may follow) or full (7 days, 30 with “keep me signed in”). Cookies are HttpOnly, SameSite=Lax and Secure in production, and every mutation checks the request origin. A trusted device carries a 30-day cookie
and on any other device the email code is required on top of the authenticator code.
3.5 Repeated failures and a new password
Wrong passwords never block sign-in: the right password still passes, and the authenticator code stays the only judge after it. Failures more than a day apart do not add up, and attempts are rate-limited per address and per IP. The counter is keyed by the address typed, whether an account exists or not, so it says nothing about an address. The third failure — or “Forgot your password?” — emails the account a reset link carrying a random token , stored only as , valid 24 hours, single-use, at most one every two minutes. The mail leaves after the response, so its latency does not reveal the account either.
The token travels in the fragment, /reset-password#token=t: a browser never sends a fragment to a server — no request line, no access log, no Referer — and the page clears it from the address bar once read. Choosing the new password takes the link and an authenticator code, so a mailbox alone is not enough. The browser derives a fresh and authHash as in eq. (2); the server replaces the stored fingerprint and closes every session. Nothing else moves: is never wrapped under the password. The one exception is an account created before the lock, whose is still wrapped under its password (eq. 5): it never receives a token, only a pointer to the recovery-key path, which re-wraps under the new password.
4Key hierarchy
At registration the browser draws . Everything else is derived from it or wrapped under it (lib/crypto/keys.ts):
A vault created today is sealed by the lock of §5 from its first second. Accounts created before the lock became part of sign-up had wrapped under the password and a server share , released after the second factor:
For those accounts, an optional 160-bit recovery key of the same generation could wrap as well ( labels vcrypt/v1/recovery*). Such a vault cannot be used until its lock is set, and both envelopes are destroyed the moment it is: from then on the password opens the session, not the vault.
Domain separation. Every HKDF call has its own label and every envelope its own AAD (appendix B). A value made for one purpose is never accepted for another: the recovery envelope fails the GCM tag if presented to the lock path, and the metadata of item fails if moved onto item .
5The vault lock
5.1 Construction
Opening a locked vault takes three 12-character keys in order, the answer to a secret question and a code from the authenticator app — plus a code by email, asked by default: up to six things at once. The keys and the answer form the lock material (lib/crypto/vault-lock.ts); the 20-character seed is not part of it and serves the recovery path only (§5.7):
The server stores , , , , the question label, the lock version and whether the email code is required. The normalisation decomposes accents, lowercases and drops every character outside , so that “L'OM”, “l om” and “LOM” open the same vault; tokens are uppercased, I and L read as 1, O as 0, separators dropped. The separator | belongs to neither alphabet and the token lengths are fixed, so is injective: two different sets of factors never yield the same material. The codes are not in : they gate the release of (§5.4) and enter no derivation.
5.2 Entropy
The alphabet is 0123456789ABCDEFGHJKMNPQRSTVWXYZ (no I, L, O, U): 32 symbols, 5 bits each. A symbol is for a CSPRNG byte ; since , every symbol has exactly eight preimages and the draw is exactly uniform — there is no modulo bias to correct.
The answer adds its own, unknown entropy on top of the 180 bits. The order of the keys is enforced by the arithmetic rather than by a comparison: swapping two keys changes , hence and , and the GCM tag of fails. Taking the seed out of the opening costs 100 bits that were never the weak point — 180 bits behind Argon2id stay out of reach (§5.3) — and leaves the seed to guard the recovery envelope, the one path that asks for no code.
5.3 One verifier, never one per factor
With a verifier per key, a database thief could attack each key separately and the costs would add up instead of multiplying:
At a deliberately generous Argon2id evaluations per second — 64 MiB each, a data-centre budget — guesses take s, about 36 years, so a key checked on its own would eventually fall; guesses take about years, before counting the answer. A single verifier is also what makes the generic error structural: the server sees one value and cannot know which factor made it wrong.
5.4 Unlock protocol
Here and are the account's settings (§5.5): the email code is required by default; codes by SMS are not offered at the moment, so is always false; and a required code that is missing counts as wrong. The predicates are all evaluated before being combined, so the response time does not depend on which one failed; the message is the same in every case; and the codes are consumed only on complete success — invalidating them after a partial failure would reveal that they were right. Each attempt is logged server-side with the per-factor detail, for the account holder; no email is sent, whether the opening succeeds or fails.
5.5 Choosing the codes
The authenticator code is always required. The email code is on by default and can be switched off — a shared or lost mailbox (app/api/vault/factors). Codes by SMS are not offered at the moment, and no phone number is stored. No email announces a change: the only alerts are a recovery (§5.7) and a reset link after repeated failures (§3.5).
A change moves the gate in front of , so it takes more than a signed-in session. The session's “vault open” state proves nothing: it only records a wrapping key sent by the browser (§9). The server requires together a single-use nonce it issued less than five minutes earlier, a fresh authenticator code, and a signature by the key derived from (eq. 4):
checked against . Only a page where the vault is really open holds ; the nonce makes a signature useless a second time, and the action and its detail — on, off, the number as typed — make it useless for any other change. Switching a code off never touches the lock material: it removes a gate in front of , not a single bit from .
5.6 Rate limiting
After the -th consecutive failure the next attempt waits : three free attempts, then 30 s, 1 min, 2 min… up to 24 h. The counter is persisted (lockouts table, it survives a restart), a success clears it, and a counter left idle for 24 hours is forgotten. Rate limiting is defence in depth here: against 180 bits behind Argon2id, it is not what stops an attacker.
5.7 Recovery path
The recovery key has 30 symbols, 150 bits; with the seed the path weighs bits before the answer. It asks for no key and no code — it exists for when a key, the phone, the mailbox or the authenticator app is lost. The answer is asked on both paths, so that the written secrets alone — keys, seed and recovery key found together — open nothing; the price is that a forgotten answer loses the vault (§14).
A recovery replaces one element only, chosen by the user: the three keys (a new lock envelope, eq. 6–8, under the same answer), the email address (proven by a code sent to the new address), the authenticator app (a new TOTP secret, proven by a code). The seed and the answer never change, nor do the question and the email-code setting. Only is renewed, every time, because it works once:
and the old key stops working the moment the new envelope is written. Everything is checked before the first write and written in one transaction; the other sessions are revoked, and an alert goes to the account's address as it was before the recovery — the old mailbox, when it is the address that changed.
5.8 Locks sealed before version 3
Earlier vaults put the seed in the opening material and the keys in the recovery material, under vcrypt/v2/* labels:
The server records a version with each lock and each recovery envelope, and the challenges tell the browser which rule to apply; prefixes and labels differ, so material made under one version never opens the other. Such a vault keeps its rule — 280 bits to open, 430 to recover — until its first recovery, which seals it again under version 3. The answer, absent from a version-2 recovery, is asked as well and checked in the browser, by opening the version-2 lock envelope with the seed and keys just proven — the recovery session receives that envelope and its share, holding already. The lock is then sealed under version 3 with the same keys, unless they are the element being replaced. One side effect: for an unknown address the recovery challenge answers with the current version, so while version-2 envelopes exist, a “2” tells that an account exists.
6File encryption: VCR1 and .crycrypt
6.1 The VCR1 format
Each file, and each edit of a file, gets a fresh . A plaintext of length is cut into chunks of 1 MiB and streamed (lib/crypto/file-format.ts):
- Reordering: a chunk decrypted at another position fails, its index being in both the IV and the AAD.
- Truncation: the header fixes , hence ; the last chunk carries the final flag, so a missing chunk, or an earlier chunk passed off as the last, fails.
- Extension: any byte after chunk is rejected.
- Header substitution: the header is in every AAD.
Because is fresh for every file version and the IVs differ for , no (key, IV) pair is ever used twice; a file can reach PiB. Small envelopes — wrapped keys, metadata — use random 96-bit IVs under long-lived keys (, ). After envelopes under one key, the probability of an IV collision is bounded by the birthday bound:
6.2 Exporting a file: the .crycrypt container
Any file can leave the vault still encrypted, as a .crycrypt file on the user's disk (lib/crypto/vcrypt-file.ts). The browser decrypts it in memory, as for a download, and seals it again at once under a fresh key; that key and the metadata are sealed under a key derived from and used for nothing else:
- Only its vault opens it. Without , neither nor the name can be recovered. A file exported by another vault fails the GCM tag of and is refused — never stored as it is.
- Unlinkable. No vault identifier is written in the clear; is random and fresh, so two exports of the same file share nothing but the magic and the structure.
- Integrity. and are bound to by their AAD, every VCR1 chunk is authenticated (§6.1), and the body must measure exactly for the sealed in .
- Back as it was. Dropped into the vault, a
.crycryptis decrypted in full — every tag checked — then uploaded like any file, under a new and new metadata: name, type and dates come back; the item is new. “Decrypt a .crycrypt file” performs the same reading and hands the original file to the browser, without storing anything.
Export and decryption are file exits: the confirmation code of §10 is asked. What a disk holding a .crycrypt reveals: its size, within a few hundred bytes of the original, and the name it was saved under — the original base name, with .crycrypt in place of the real extension. An export stays readable for as long as its vault exists, even after the file is deleted from it.
7Metadata, names and the local cache
The server sees no name, no type and no folder structure. The item identifier being the AAD, it cannot move one item's metadata onto another. In the explorer every file shows as name.vcrypt — a file type already says something to whoever looks over a shoulder — and its real name comes back for five minutes after a decryption only. The IndexedDB cache holds encMeta, wrapped keys, sizes and versions under : no email, no user identifier, no name in the clear, and it is wiped at sign-out.
Moving items — one, or a whole selection dragged onto a folder — rewrites their encMeta with the new parent, item by item; for the server a move looks exactly like a rename. Since the server ignores the tree, the browser is the one that refuses to put a folder inside itself or inside one of its descendants.
8Time freeze
The server verifies against , registered at sign-up, keeps , and refuses , the content and deletion until — even to a fully authenticated session, even through recovery. After it returns ; the browser recomputes the digest, checks , and wraps back under . is erased only after that, so an interruption never loses data. The freeze relies on the operator for timing: a compromised server could release early — never read the file.
9The open window
After an unlock the browser draws a random , keeps in sessionStorage and hands to the server, which keeps it in the session for twenty minutes — an hour with Premium. That is the vault's open window, shown as a countdown: a reload within it fetches back; at its end the interface closes the vault and the server stops returning , so the lock is asked again; signing out destroys it sooner. Neither half opens anything alone. What is conceded: during that window a script injected into the page could join the two halves — the same script could already read from memory while the vault is open.
Reopening with one character of each key. After a complete opening the browser also keeps the three keys, split the same way:
stays in localStorage, in the server session until that session ends. When the open window runs out on its own, both stay, and the next opening asks for one character of each key, at positions drawn afresh for every attempt, with the answer and every code as usual. The browser joins and , compares the characters itself and never says which one is wrong:
where moves the first symbol of one place along . The material of (6) is then built from : a wrong character does not stop the attempt in the browser, it makes it fail at the single verifier of §5.3 — same message, counted by the lockout of §5.6. Locking the vault by hand, signing out, signing in again (a new session holds no ) or choosing “Forget my keys” erases both halves, and the three keys are asked in full again. A version-2 lock, which still asks for the seed to open, keeps the seed with the keys.
10The confirmation code
Four digits, stored as scrypt, asked by the interface before any file leaves the vault — open, download, edit, export as .crycrypt, decrypt a .crycrypt, make permanent. It enters no derivation and protects no key: it guards against a session left open on a desk. Wrong codes follow the schedule of §5.6. It is checked by the interface, not by the endpoints that serve ciphertext: a speed bump for an unattended session, not an access control.
11Storage, deletion and payment
Gofile receives files named <uuid>.bin in one private folder per vault, named v-<random>, through upload.gofile.io (nearest region) with the operator's token, which never reaches a browser. It sees sizes and times; never a name, a type, a structure or a key.
What the database links. The server keeps, in the clear, the Gofile folder of each vault and the Gofile identifier of each blob: it needs them to relay downloads and deletions and to count quotas. Whoever holds the database can therefore list the blobs of an account, or find the account behind a blob — without being able to read any of them.
Deleting. Deleting a file, or a folder with everything inside it, deletes the blobs from Gofile too. The browser sends every identifier — the server ignores the tree — and the server, in one transaction, removes the items and writes their blob references to a deletion queue (storage_deletions); it then deletes them from Gofile. A reference Gofile does not confirm stays in the queue and is retried every hour by the server's maintenance task (lib/server/maintenance.ts); a blob already gone counts as deleted. Editing a file queues its previous blob the same way. A deleted file cannot linger at the host because a request failed.
Paying for Premium. Premium is paid in cryptocurrency — BTC, ETH, USDT, USDC, SOL or XMR — through Plisio, one period at a time: a month or a year, never renewed on its own. The server sets the price and creates the invoice in euros, payable in the chosen coin and network; the billing page shows the address, the exact amount and a QR code, and the customer pays from any wallet without leaving the site. Plisio receives a random order identifier, the price and the coin — never an email address, nothing from the vault. Its webhook is signed with , the provider's scheme, which nothing here relies on alone: whatever prompts a check — the page every ten seconds, Plisio's webhook, the hourly maintenance — the state of the payment is read back from Plisio's API by the invoice reference we stored ourselves, and only a completed invoice for the same order, price, coin and network adds the period, once. A forged webhook can at most trigger that check. Without a Plisio key there is no payment at all: Premium is given directly, with no end date.
End of Premium. When a paid period ends without a new payment the account falls back to the free plan at that moment, and the end of the period is recorded as . One month later — — the maintenance task deletes every item of the vault and queues all its blobs: files, folders, freezes and the links to permanent copies. A payment before cancels it. The vault and the billing page warn a week before and show after it; no email is sent (§5.5). Permanent copies stay on Arweave, where nobody can delete them: a public one stays readable by anyone holding its link, an encrypted one becomes unreadable, its key having gone with the item — unless it was exported first (§6.2).
12Permanent storage on Arweave
12.1 Two modes
Premium and Business accounts can place a file of up to 100 KB on Arweave through the free tier of Turbo (ArDrive / ar.io). Encrypted: Arweave receives VCR1 ciphertext under a fresh , wrapped under like any other file key. Public: Arweave receives the file itself — an HTML page included — readable by anyone who has its identifier, for as long as the network exists. The public mode requires a separate confirmation, in red: it is the only action in the product that no key, no support and no deletion can undo.
The storage is paid once, into the network's endowment: a reserve held in its own token, AR, that pays the independent nodes keeping copies, year after year, in many countries — out of reach of any single operator, host or jurisdiction. Arweave is built to keep data online for fifty years and more; Premium includes it at no extra cost, through Turbo's free tier.
12.2 The data item
The browser builds an ANS-104 data item and signs it itself (lib/crypto/ans104.ts); the server only relays the signed item to Turbo (§12.5).
Here is the length written in decimal, the raw 512-byte RSA modulus, and the anchor 32 random bytes. Tags are an Avro array of zigzag varints and strings: Content-Type: text/html encodes to 02 18 "Content-Type" 12 "text/html" 00. The key pair is ephemeral — one per upload, private half non-extractable: an Arweave address is public and permanent, and reusing one key would link all of a user's uploads forever. The free tier is counted per IP address, so a fresh key costs nothing.
12.3 Size budget
is read from Turbo at run time rather than hard-coded. An encrypted 100,000-byte file weighs bytes once in VCR1, and its Content-Type tag 40 bytes: the item is bytes. The 100 KB rule therefore always fits, with 6 KB to spare.
12.4 Compressing images
An image above the limit is re-encoded in the browser — WebP when the browser can write it, JPEG otherwise — and its embedded metadata (GPS position, device, date) disappears with the re-encoding. The search starts from a pixel budget, walks down a quality ladder, then shrinks:
is the byte budget, the original size, and the smallest encoding obtained at scale over the qualities 0.86, 0.74, 0.62, 0.50 and 0.40. Encoded size grows roughly with the pixel count, so each round divides it by at least ; the search stops after 12 rounds or below 32 pixels. Measured while testing: a 3.48 MB PNG of 2400 × 1600 pixels became a 92.5 KB WebP of 866 × 577. Any other file above 100 KB is refused.
12.5 The link stays encrypted
The transaction identifier, the mode and a SHA-256 fingerprint of the published bytes are stored only inside encMeta (§7). The server stores an opaque item: encrypted metadata, a wrapped key in encrypted mode, no storage reference, zero bytes counted against the quota. The signed item does pass through it: a relay forwards it to Turbo through the operator's proxy — Turbo sees the proxy's address, never the user's — and keeps nothing, neither the bytes nor the identifier, not even in a log. An encrypted copy crosses it as ciphertext, a public one in the clear. The Premium check comes before anything irreversible leaves: the link is recorded first, the upload follows, and the link is withdrawn if the upload fails. The interface shows the identifier and the gateway link of every permanent copy, to copy one by one or all at once — what remains of a copy if its link in the vault ever disappears (§11).
12.6 Deleting
Deleting the item deletes the link. In encrypted mode it also deletes the only copy of : the ciphertext stays on Arweave forever, but without it is computationally indistinguishable from random bytes — the copy is crypto-shredded. In public mode nothing can be withdrawn: the file stays readable at its identifier.
12.7 Reading back
The browser fetches /raw/{id} from turbo-gateway.com, which serves an upload immediately, then from arweave.net, which serves it once indexed (40 to 60 minutes), and compares its SHA-256 with the fingerprint in encMeta. In encrypted mode GCM would detect tampering anyway; in public mode the fingerprint is the only defence against a gateway serving something else.
12.8 Limits
- Turbo sees the proxy's address, not the user's. The server sees every item it relays: a compromised server could note which copy belongs to which account.
- A gateway may refuse to serve some content; the data stays on the network, and other gateways can serve it.
- The free tier allows about 10 MiB per IP address for life, and 107,520 bytes per item.
- Someone holding both the crycrypt database and Turbo's logs could correlate an item's creation time with an upload.
- One file, one identifier: a public HTML page must be self-contained (inline styles and images).
- The fifty-year horizon is the network's, not ours: it rests on Arweave's economics — the value of its endowment and the falling cost of storage — which no party to crycrypt controls.
13What each party sees
| Data | Browser, vault open | crycrypt server | Gofile | Arweave · Turbo |
|---|---|---|---|---|
| Password | derives, then forgets | no — scrypt of a derivative | no | no |
| Seed, keys, answer, recovery key | while typed | no — one verifier per path | no | no |
| Vault key VK | memory only | no — envelopes | no | no |
| Names, types, folders | yes | no — encMeta; a public copy's type, in transit | no | type tag, public mode |
| File contents | when opened | no — a permanent copy only passes through | ciphertext | ciphertext, or the public file |
| Email address | yes | yes | no | no |
| Which codes open the vault | yes | yes | no | no |
| Password reset token | in the fragment, then cleared | no — SHA-256 only | no | no |
| An exported .crycrypt | while sealing or opening it | never sees it | no | no |
| A deleted file | gone | blob reference until Gofile confirms | until the deletion | — |
| TOTP secret | once, at set-up | yes | no | no |
| Which blob belongs to whom | yes | yes, in the clear | one folder per vault | — |
| Which permanent copy belongs to whom | yes | no — inside encMeta; seen only in transit | — | the proxy's IP, not the user's |
| Sizes and times | yes | yes | yes | yes |
| A Premium payment | while paying | account ↔ address, amount, transaction | no | no |
Plisio, which processes payments, sees for each one a random order identifier, the price, the coin and the blockchain transaction — never an email address, nothing from the vault. On public chains the paying address and the amount are visible to anyone; Monero hides them.
14Limits and assumptions
- The code is served by the server. A malicious server could send a modified client that leaks keys. This is the standing limit of encryption in a web page; HTTPS, audits of the deployment and a content-security policy reduce it. Every script carries a Subresource Integrity hash, and each release publishes an Ed25519-signed manifest of those hashes on /integrity: anyone can compare what their browser received with what was signed. This detects a swapped script after the fact; it cannot stop a server that serves a backdoored page to one person only, at one moment.
- The TOTP secret is stored in the clear server-side — verifying a code requires it. It opens nothing without the three keys and the answer.
- A forgotten answer loses the vault. It is required to open and to recover (§5.7); no third path exists, by design — the operator could not offer one without being able to open vaults itself.
- Remembered keys belong to the signed-in browser. Whoever holds that browser with its session can join and and read the three keys (§9); the characters asked at reopening come from what the browser already holds and add nothing against that person. The answer, the authenticator code and the email code are still required, and the lockout still counts. On a shared computer, lock the vault by hand: that forgets the keys.
- A payment links an account to a transaction. For every payment the server keeps the account, the paying address and the transaction (§11); on public chains the last two are visible to all. Monero hides them on-chain, not in our database.
- Nothing renews on its own. A period not paid in time ends Premium and starts the month of grace; the vault warns a week before, no email does.
- The password rule is the interface's. The server never sees the password (§3.1); a modified client could register a weak one. It would only weaken that account's sign-in, never its vault.
- No alert for a failed opening. Email carries codes, the recovery alert and the reset link, nothing else; refused openings only appear in the account's activity journal. Its holder can erase what is older than 48 hours — never the last 48 hours, so a recent sign-in cannot be wiped by whoever made it — and the erasure is itself recorded.
- The codes are a gate, not a key. The email code can be switched off (§5.5); doing so removes a check before is released, not entropy from the lock material.
- Freeze deadlines rely on the operator for timing (§8).
- Inline styles are allowed by the content-security policy (
style-src 'unsafe-inline'): the interface library sets positions and animations through the style attribute. Scripts stay strict — each one needs the per-request nonce — so an injected style cannot run code. - The request limiter is in memory: several server instances need a shared store. The vault lockout of §5.6 is persisted and not affected.
- Blob identifiers are linkable in the database (§11); permanent copies are not (§12.5).
- Registration tells whether an address already has an account, by refusing it at the first step — and so does a version-2 recovery challenge, until those vaults are recovered (§5.8).
- The answer is normalised permissively: it enters both paths, and a strict normalisation would lose vaults over an apostrophe.
- The end of Premium deletes everything, for good. One month after the plan ends, every stored file goes, and no email warns about it: the vault and the billing page show the date. Exported
.crycryptfiles survive, since they live on the user's disk. - A .crycrypt is as strong as its vault. Whoever can open the vault can open every file ever exported from it, including files since deleted; its name on disk keeps the original base name.
- Maintenance runs inside the server process. With several instances, run
npm run maintenancefrom a scheduler instead. - Permanent means permanent. A public copy cannot be withdrawn by anyone, the operator included.
AParameters
| Parameter | Value |
|---|---|
| Argon2id | m = 65,536 KiB · t = 3 · p = 1 · 32 bytes |
| scrypt (authHash, PIN) | N = 16,384 · r = 8 · p = 1 · 32 bytes |
| Seed · key · recovery key | 20 · 12 · 30 symbols, 5 bits each |
| VCR1 | 1 MiB chunks · 25-byte header · 16-byte tag |
| Session | 15 min (password stage) · 7 days · 30 days (remember) |
| Pending sign-up | 1 hour · 2 hours once both codes are proven |
| Email code (sign-in) | 6 digits · 10 min · 5 attempts |
| Repeated failures | 3 wrong passwords in a row → reset link emailed · never blocks sign-in · failures forgotten after 24 h |
| Password reset link | 256-bit token in the fragment · SHA-256 stored · 24 h · single use · 1 per 2 min |
| Codes to open | authenticator always · email on by default |
| Vault codes (email) | 6 digits · 5 min · 5 attempts |
| Codes settings | single-use nonce · 5 min · signature + authenticator code |
| Premium payment | Plisio · BTC, ETH, USDT, USDC, SOL, XMR · invoice valid 1 h · one month or one year per payment, no renewal |
| Payment checks | page every 10 s · webhook · maintenance for 2 days · state always read back from the API |
| End of Premium | free at the end of the paid period · files kept 30 days, then every item deleted and every blob queued |
| Maintenance | every hour · queued blob deletions retried up to 50 times |
| .crycrypt | 12-byte prefix · header ≤ 64 KiB · fresh FK′ per export |
| Recovery | replaces one of: keys · email · authenticator — new recovery key, same seed |
| Vault open window | 20 min (Free) · 1 hour (Premium) |
| Remembered keys | one character per key · fresh positions each attempt · until sign-out, manual lock or “Forget my keys” |
| Password | ≥ 12 characters · upper and lower case · digit · symbol · no common pattern (browser) |
| Lockout | 3 free attempts · 30 s doubling · 24 h cap |
| Real names shown after decryption | 5 min |
| Freeze deadline | 60 s to 10 × 366 days |
| Permanent copy | file ≤ 100,000 bytes · item ≤ 107,520 bytes |
| Quotas | 3 GB (Free) · 1 TB (Premium) |
BDomain-separation labels
| Kind | Labels |
|---|---|
| HKDF info | vcrypt/v1/auth · vcrypt/v1/unlock · vcrypt/v1/meta · vcrypt/v1/sign · vcrypt/v1/local-tag · vcrypt/v1/recovery · vcrypt/v1/recovery-verifier · vcrypt/v1/recovery-unlock · vcrypt/v1/freeze · vcrypt/v1/export · vcrypt/v3/vault-verifier · vcrypt/v3/vault-lock · vcrypt/v3/recovery-verifier · vcrypt/v3/recovery-lock · vcrypt/v2/vault-verifier · vcrypt/v2/vault-lock · vcrypt/v2/recovery-verifier · vcrypt/v2/recovery-lock · vcrypt/v2/fingerprint |
| GCM AAD | vcrypt/vault-key · vcrypt/v3/vault-key · vcrypt/v3/recovery-key · vcrypt/v2/vault-key · vcrypt/v2/recovery-key · vcrypt/item/{id} · vcrypt/export/key|{e} · vcrypt/export/meta|{e} · vcrypt/v2/open-session · vcrypt/v1/remembered-keys · VCR1 header ‖ u32(i) ‖ last |
| Lock material prefixes | v3 (open) · v3r (recover) · v2 · v2r (earlier locks) |
| File magics | VCR1 (stored content) · VCRYPT, version 1 (exported file) |
| Server HMAC | vault-verifier · recovery-verifier · email-code · vault-email-code · vault-sms-code · register-code · device · fake-salt |
| Signed messages | vcrypt/freeze/v1|{id}|{deadline}|{SHA-256 of the envelope} · vcrypt/factors/v1|{uid}|{nonce}|{action}|{detail} |
| Arweave deep hash | dataitem · 1 · 1 (signature type: RSA-PSS 4096) |
The detailed security model, in French, is kept with the source code in docs/SECURITY.md.