TxtCipher code review: RC4 keystream reuse and a versioned AES format

AI;DR – The security review described here was done by Claude Code with the Fable model, and the resulting changes and this blog post were developed with significant help from it. If you don’t want to read “AI slop”, stop reading now.

TxtCipher is a small web page that encrypts and decrypts text with a password. I wrote it to exchange text with TxtCrypt, a macOS program that uses RC4, and later added AES-256-GCM as the default mode. The whole thing is one HTML file with no dependencies, served straight from the SourceForge SVN repository, and it works in any current browser on Windows, Linux and Android.

I recently had Claude Code, using the Fable model, review it with a focus on security. Two of the findings are worth writing down, because they affect how the tool should be used.

RC4 mode: one password, one keystream

The TxtCrypt format is simple: the UTF-8 bytes of the password are the RC4 key, there is no salt and no IV, and the result is Base64 encoded. That is what makes the two programs compatible, and it cannot be changed without breaking that compatibility.

It also means that every text encrypted with the same password uses exactly the same keystream. The consequences are more concrete than the usual “RC4 is broken”:

  • XOR of two ciphertexts gives XOR of the two plaintexts, and with a bit of context that is often enough to read both.
  • Anyone who has one plaintext together with its ciphertext can recover the keystream and decrypt every other text encrypted with that password, up to that length, without ever knowing the password.
  • There is no integrity check, so bits in a ciphertext can be flipped without anybody noticing.

None of this is a bug in TxtCipher, it is inherent in the format. So the rule is: use RC4 mode only to exchange text with TxtCrypt, and use AES-256 for everything else. This isn’t new, but I thought it worth mentioning again. TxtCipher defaults to AES-256 and marks RC4 as legacy.

AES mode: a format that can be upgraded

The AES mode derives the key from the password with PBKDF2 and encrypts with AES-256-GCM, with a random salt and IV for every text. TxtCipher originally used just salt, IV and ciphertext, with a fixed 100000 PBKDF2 iterations.

The problem with that was not the security of the format itself but its future: the iteration count was hard-coded. Raising it, which one should do every few years as hardware gets faster, would have made every existing ciphertext undecryptable, because the decrypt side had no way to tell which count a given ciphertext was made with.

That’s why the format now starts with a small header: a version byte and the iteration count. The header is passed to GCM as additional authenticated data, so tampering with it fails the authentication tag instead of silently deriving a different key. Ciphertexts in the old header-less format are still decrypted, the page tries the new layout first and falls back to the old one. With that in place I raised the iteration count for new ciphertexts to 600000, which is the current OWASP recommendation for PBKDF2 with SHA-256. According to Claude’s measurement on my machine, that makes encrypting or decrypting a text take about a third of a second instead of a twentieth, and it makes every password guess an attacker has to try six times more expensive.

Old ciphertexts keep working. New ciphertexts cannot be read by an old copy of the page, but since the page is served from the repository, nobody should have one.

Smaller things that came out of the same review

  • Offline support finally works. The page had registered its service worker from a blob: URL since it was added, and no browser accepts that, they just failed silently. The worker now lives in a separate file next to the page.
  • Over a plain http address WebCrypto is not available, so AES mode cannot work. The page used to report that as “wrong password”; it now switches to RC4 and says that https is required for AES-256.
  • A Content Security Policy guarantees that the page never talks to the network: no fetch, no beacons, no form posts.
  • Android now gets proper icons for “Add to Home screen”, including the maskable variant that launchers cut into a circle.

The current version is v1.26.0.42, revision 45 in the repository.