Skip to content

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.

REVIEWED 30 AUG 2026 · SCOPE APPLICATION CRYPTOGRAPHY · METHOD SOURCE REVIEW

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. There is no column for one in any table, no route that accepts one, and the only cryptographic operation our servers perform is encrypting a login challenge to a public key. 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. Those are set out in full below, because a page that lists only strengths is marketing rather than an audit.

What we use

01

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.

02

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.

03

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.

04

No passwords anywhere

There is no password, so there is no key derivation function, no salt and no iteration count to get wrong. The private key is the account.

Where a key comes from and where it goes

  1. 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.

  2. 02

    Creating a tome generates one AES-256 key in your browser and wraps it to your own public key.

  3. 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.

  4. 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.

  5. 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.

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 password. There is nothing to derive a wrapping key from, so there is nothing to encrypt the private key with. 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.

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.

The key backup is not encrypted

The backup blob is base64 of a JSON object containing the raw private key. Anyone who reads that file has your account and its whole history. It is the only recovery path, and it is exactly as safe as wherever you put it.

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.

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 an unencrypted backup you saved somewhere convenient. Any page that leads with key sizes is pointing at the strongest part of the system and away from the weakest.

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. It is 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