Audit
Security Audit
An internal review of the encryption ImageTome actually implements: how keys are generated, where they are stored, what we can still read, what it would cost to break the mathematics, and the parts of the design that will not protect you.
Summary
This audit was carried out by several AI agents reading the source code directly, each given a separate scope, and then reviewed and edited by human staff. Every figure on this page was read out of the code rather than recalled from documentation.
The findings are, in short: the cryptography itself is sound and conservatively chosen. Content is encrypted in the browser with AES-256-GCM under a key per tome, and that key is wrapped for each member with RSA-4096. Every encryption uses a fresh random initialisation vector, generated inside the encryption function so that no calling code can supply or reuse one. Nothing is hand-rolled; everything runs through the browser's Web Crypto API. There is no algorithm negotiation, so there is nothing to downgrade.
We hold no private keys, with one exception you have to switch on deliberately. Leave recovery off - the default - and there is no column in any table holding your key and no route that accepts one. Turn it on and we store a copy that was sealed in your browser under a passphrase we never receive, cannot derive and cannot brute force from anything in the database. We still cannot read your content in either case. Your device token is an authentication credential that is stored hashed and is unrelated to your encryption keys: stealing one yields ciphertext and metadata, never plaintext.
The weaknesses are not in the mathematics. They are in key storage, in key distribution, and in what remains visible around the encrypted content. Your private key is stored on your device without a passphrase. We do not give you a way to verify that somebody's public key is really theirs. Removing a member does not rotate the key they already hold. And recovery, if you enable it, moves a sealed copy of your key onto our servers, which is a real change to what a total break-in would yield. Those are set out in full below, because a page that lists only strengths is marketing rather than an audit.
What we use
RSA-4096, OAEP, SHA-256
Your identity keypair. Public exponent 65537, generated by the Web Crypto API in your browser. It never signs anything; it only wraps and unwraps the keys below.
AES-256-GCM
One key per tome and one per conversation. GCM is authenticated, so tampered ciphertext fails its tag rather than decrypting to something an attacker chose.
96-bit random IV
Twelve bytes from the operating system CSPRNG, minted inside the encrypt call. No caller can supply one, which is why no IV is ever reused.
PBKDF2-SHA256, only if you ask
There is no account password. Two optional things use one: switching on recovery, and sealing an exported backup. Either derives a wrapping key in your browser at 600,000 iterations over a random 16-byte salt and encrypts under AES-256-GCM. Those are the only password-shaped things here, both are choices, and neither passphrase reaches us.
Where a key comes from and where it goes
-
01
Registration generates the RSA-4096 keypair in your browser. The public half is uploaded. The private half is written to your device and never transmitted.
-
02
Creating a tome generates one AES-256 key in your browser and wraps it to your own public key.
-
03
Inviting somebody re-wraps that same tome key to their public key, in your browser, using your private key. The unwrapped key never leaves the tab.
-
04
Every post, comment, message, filename, image, video and thumbnail is encrypted under the tome key with its own fresh IV before it is uploaded.
-
05
Signing in means decrypting a random challenge we encrypted to your public key. We store only its SHA-256, compare in constant time, and discard it.
-
06
Exporting a backup hands you the same private key your device already holds. You are asked whether to seal it first; if you do, the passphrase stays in the tab and the file we never see is the only thing that leaves.
-
07
If you turn on recovery, your browser seals a copy of your private key under your passphrase and passcode and uploads only the sealed result. Getting it back means proving the passcode to us, then opening the seal in your browser with the passphrase. We never see either secret, and the endpoint that hands the sealed copy back cannot sign you in.
Key generation
Every key in the system comes from the operating system's cryptographically secure random number generator, reached through the browser's Web Crypto API. The keypair is RSA-4096 with public exponent 65537 and SHA-256, generated once at registration and never regenerated. Tome and conversation keys are AES-256. Initialisation vectors are twelve random bytes.
The word "Math.random" does not appear anywhere in this application. Neither does any third-party cryptography library: there is no JavaScript bignum implementation, no custom cipher mode, no custom padding. Algorithm names and key lengths are hardcoded constants rather than values read from a server response, so there is no field an attacker could set to ask for something weaker.
Initialisation vectors deserve a specific note, because reusing one under AES-GCM is catastrophic and is the mistake that most often undoes an otherwise correct deployment. The function that generates them is called from exactly two places, both inside the encryption routines themselves, and no calling code can pass its own. Reuse is prevented by the shape of the interface rather than by everyone remembering.
Key storage
Your private key is stored on your device in browser storage, as base64 of the standard PKCS#8 encoding, with no passphrase wrapping it. When the application loads it back, it is imported as a non-extractable key object, so a script cannot export it from memory - but the plain copy it was imported from is sitting beside it, which is the honest limit of that protection.
This is the direct consequence of having no account password. There is nothing to derive a wrapping key from, so there is nothing to encrypt the private key with on your device. It buys a genuinely passwordless account and costs the ability to protect the key at rest. We think that trade is worth stating rather than hiding.
A passphrase appears in two optional places, and neither of them protects the copy sitting in browser storage. The first is recovery, which protects the copy we hold. Your browser derives a key from the passphrase and passcode with PBKDF2-SHA256 at 600,000 iterations over a random salt, seals the private key with AES-256-GCM, and uploads only the result. The seal is bound to your username and to a fingerprint of your public key, so a copy made for one account cannot be opened as another. Before we report recovery as on, your browser opens the seal it just made and checks it round-trips - a backup nobody has tested is not a backup.
The second is the exported backup, which protects a file you hold and we never receive. The same derivation, then AES-256-GCM, with the envelope's own parameters authenticated alongside the key so a substituted iteration count or hash cannot be accepted quietly. Your browser opens that seal too and compares what comes back against the key on the device before it shows you anything. It is bound to nothing else on purpose: a sealed backup carries no username and no readable public key, so the file itself says nothing about whose account it opens. The cost is that a wrong passphrase, a corrupted file and an edited one are one indistinguishable failure, which is why the error names all three rather than telling you that you have forgotten something.
On our side, each member of a tome has one row holding that tome's AES key wrapped to their RSA public key. That wrapped key, the ciphertext and the initialisation vectors are all we store. Encrypted files are written to a private disk and streamed back without ever being decrypted, and the caches your browser keeps hold ciphertext only.
Sharing without weakening anything
A tome has one AES key. Every member holds a copy wrapped with their own RSA public key. When you invite somebody, your browser unwraps the tome key with your private key, re-wraps it with theirs, and uploads the wrapped result. At no point does an unwrapped tome key exist on our side.
That means a shared tome is exactly as encrypted as a private one. There is no weaker mode you slip into by adding a second person, which is the point at which several larger services quietly stop being end-to-end encrypted.
The gap is in the step before that. Your browser wraps the key to whichever public key our server hands back for that username, and there is nothing in the product that lets you check that key is really theirs. A fingerprint you could compare out of band would close it. We have not built one, and until we do, sharing requires trusting that our server told you the truth.
What we can still see
Tome names and descriptions, post titles and bodies, comments, direct messages, filenames, images, videos and thumbnails are all ciphertext to us. We cannot read any of it, and there is no key on our side that would let us.
What we can read is the structure around it: your username and profile fields, your avatar, which people are in which tome and in what role, who exchanges messages with whom, whether a message was delivered and read and when, the exact byte size and MIME type of every file, the duration and dimensions of every video, and every timestamp. Taken together that is a considerable amount, and an adversary holding a copy of a file can confirm it is in a tome by its exact size without decrypting anything.
We would rather write that down than describe it away. Encryption of this kind protects content. It does not protect the shape of your activity, and nothing in this design pretends otherwise.
The trust floor
Your browser fetches the code that performs the encryption from our servers. If we served you modified JavaScript, it could take your key before it ever encrypted anything. No amount of key length changes that, and subresource integrity does not help, because the same server writes the hashes.
What we can do, and have done, is keep the set of parties you have to trust as small as possible. No third-party origin appears anywhere in the content security policy. Fonts are served from this domain. There is no analytics script, no tag manager, no session recorder, no advertising pixel. The policy blocks inline and dynamically evaluated script on the application itself. So the trust set is one party rather than a dozen, and the ninety-second check on our encryption page lets you confirm the uploads really are ciphertext.
What that check cannot show you is a substituted public key, because that attack requires no modified code at all. It is worth being precise about which assurances a network tab can actually give you.
Findings
The weaknesses this audit found, most consequential first. None of them is a flaw in the cryptography. All of them are places where the guarantee is thinner than the key lengths suggest.
The private key sits in browser storage in the clear
It is base64 PKCS#8 in localStorage, with no passphrase protecting it. Any script that reaches this origin, any browser extension with host access, and anyone with the unlocked device or a disk image of it gets the key - and with it every tome you can read, permanently. There is no key rotation, so a leaked key cannot be retired.
An exported backup can still be taken in the clear
Sealing one is offered now, and it is a choice rather than a default. Take the seal and the file is AES-256-GCM under a key derived with PBKDF2-SHA256 at 600,000 iterations, and we refuse a passphrase that would not survive a year of offline guessing. Decline it and the blob is what it always was: base64 of a JSON object holding the raw private key, as safe as wherever you put it and no safer, where anyone who reads that file has your account and its whole history. The unsealed route stays because a passphrase somebody forgets loses an account as thoroughly as no backup at all - but nothing preselects the safer option, and we cannot tell from here which one you took.
We do not verify that a public key belongs to the person named
When you invite somebody, your browser wraps the tome key to whatever public key our server returns for that username. There is no fingerprint to compare and no pinning. A compromised server could substitute a key it controls and receive a copy of the tome key, without modifying any code you could inspect.
Removing a member is access control, not revocation
Removing somebody from a tome detaches them. It does not rotate the tome key or re-encrypt anything. Somebody who kept the wrapped key they were legitimately given can still decrypt that tome, including things posted after they were removed.
No forward secrecy
Tome keys are long lived and wrapped to long-lived identity keys. One private key compromise decrypts the entire archive, not just what was sent afterwards.
Profile photos are not encrypted
Avatars are stored as ordinary image files on a public disk and served over an unauthenticated URL. Everything inside a tome is encrypted; your profile picture is not, and neither are the profile fields beside it.
Recovery is an escrow, and it changes what a break-in costs you
Leave it off and somebody who takes our entire infrastructure gets ciphertext nobody can ever open. Turn it on and they also get a copy of your private key, sealed under your passphrase. A stolen database on its own is still useless: both the passcode check and the outer seal are keyed by a secret held outside the database. But that secret sits in a file on the same host, so anything that reads the server's filesystem reduces your key to how good your passphrase is. That is the trade recovery asks of you, and it is why we generate a six-word passphrase rather than letting you invent one.
Your recovery passcode reaches us, and its stored form is not slow-hashed
We have to check the passcode to decide whether to hand the sealed copy back, so it arrives in the clear over TLS. We never store it; we store a keyed hash, keyed with that same off-database secret. The hash is fast rather than deliberately slow, so an attacker holding both the database and the key file recovers a short passcode in well under a second. The passcode guards the release of the sealed copy. Only the passphrase guards what is inside it.
Metadata is readable and it is not trivial
Ciphertext hides content, not structure. We can see who is in which tome and in which role, who exchanges messages with whom and whether they were read, exact file sizes, MIME types, video durations and dimensions, and every timestamp. A known file can be confirmed present in a tome by its exact byte length, without decrypting anything.
Abuse reports are stored in plain text
The free-text box on a report is not encrypted, because staff have to read it. If somebody quotes decrypted content into a report, that quotation is stored in the clear.
The mathematics is not the weak part, and an attacker will not go near it. They will go for the device, the delivered code, or the moment a key is handed to somebody else. That is where we would look too, and it is where the work is.
Brute force, quantum computers, and what actually matters
Longer than the energy budget of the solar system allows, which is a different statement from "a very long time". Counting through 2^256 states - before performing a single decryption - costs at least 3.3 x 10^56 joules at the Landauer limit. The Sun radiates about 1.2 x 10^34 joules a year in every direction. You would need roughly 3 x 10^22 years of its entire output, captured perfectly. A faster computer does not help; only a colder universe would.
Nobody guesses an RSA key; they factor the modulus. The largest publicly factored RSA modulus is RSA-250, at 829 bits, in February 2020, which took about 2,700 core-years. Extrapolating the number field sieve to 4096 bits gives somewhere in the region of 10^22 to 10^27 core-years, depending on whether you follow the asymptotic formula or NIST's more conservative interpolation. At the optimistic end for the attacker, a machine with a billion dedicated cores still needs around a thousand times the age of the universe.
Two very different things. Grover's algorithm halves the effective strength of AES-256 to a 128-bit security level, which remains far out of reach, parallelises poorly, and is why NIST has not asked anyone to increase symmetric key sizes. Shor's algorithm breaks RSA outright. Published estimates put a 2048-bit modulus at under a million physical qubits for about a week; RSA-4096 costs several times that. Real devices are orders of magnitude away, so this is a future risk rather than a present one.
It is the honest long-term concern with this design, and it lands on RSA rather than AES. The wrapped tome keys sit in the database next to the ciphertext, so somebody who copies the database today and waits for a quantum computer capable of Shor would break the RSA wrapping and recover the AES keys - the symmetric layer would never be attacked at all. We use no post-quantum hybrid today. That is a defensible choice now and a migration that will have to happen.
None of them. Every figure above describes an attack no competent adversary would attempt. The private key is a string in your browser's storage, reachable by a malicious extension, by anyone holding your unlocked device, or from a backup you chose not to seal and saved somewhere convenient. Any page that leads with key sizes is pointing at the strongest part of the system and away from the weakest.
We built one, and it is off until you turn it on. The old position was defensible but it cost people their accounts for no better reason than a cleared browser. Recovery seals a copy of your private key in your browser under a passphrase and a passcode, and stores only the sealed result. What has not changed is that we cannot open it: the passphrase never leaves your device, and nothing we store lets us derive it. What has changed is that a copy now exists on our servers at all, which is a genuine shift in what a total compromise of our infrastructure would yield, and it is listed among the risks above rather than buried.
For content, yes, and unchanged: we hold no key that decrypts a tome, a message or a file. For your identity key the honest answer is that enabling recovery moves you from "they hold nothing" to "they hold something they cannot open". Those are different claims and we will not blur them. The seal is AES-256-GCM under a key derived from a passphrase we never receive; the database alone contains nothing to attack it with. If that distinction matters more to you than the safety net, leave recovery off - the export backup remains, and so does the original guarantee.
Strong enough to be the last line, because in a total server compromise it is. We generate six words from a 2,048-word list, which is 66 bits: at the roughly one million guesses a second that PBKDF2 at 600,000 iterations allows a well-funded attacker, that is far longer than any of this will matter. If you insist on typing your own we measure it and refuse anything under about 46 bits - the point at which a year of that guessing would still not have found it - deliberately scoring repetition and reused patterns for what they are rather than for their length. The same bar now applies to a backup you choose to seal. Take the generated one.
They fail differently, which is why both exist. A sealed backup is a file you hold: it keeps working if we disappear overnight, it needs nothing from us to open, and it is worthless to anyone who takes our infrastructure because we never had it. Recovery needs us to still be running and still willing to hand the sealed copy back, and it puts a copy of your key on our servers that was not there before. What a backup asks of you is that you do not lose a file. What recovery asks is that you accept we hold something we cannot open. The backup is the stronger position and by far the easier one to neglect, which is the whole reason recovery was built.
It was carried out by several AI agents reading the source directly, each assigned a separate scope - key generation and primitives, key storage and transport, and residual risk - and then reviewed and edited by human staff before publication. The recovery feature was put through the same process a second time before release, adversarially: that pass found a strength meter that accepted a repeated character as a strong passphrase, and an enrolment path that never checked the sealed copy could be opened again. Both were fixed before this page changed. It remains a point-in-time review of the code as it stood on the date above, not a certification, and not a substitute for an independent third-party audit.
Read the design before you trust it.
The encryption page walks through the key hierarchy and shows you how to verify the uploads yourself in about ninety seconds.
How the encryption works