I was reviewing Memora for a possible public GitHub release when I found a flaw in the encryption code I had written.
Memora is a local-first journal. Entries and attachments are encrypted before they are stored, so the cryptographic boundary is one of the product's most important promises. That made the mistake uncomfortable, but also exactly the kind of thing a release review is supposed to uncover.
The problem was nonce reuse.
What I had built
Each journal entry gets a key derived from the master key, the entry ID and a key version. Attachments follow the same pattern.
That part was intentional: different objects should not all use one undifferentiated key.
The mistake was that the same derivation function also produced the nonce.
For a given entry ID and key version, both values were deterministic. Encrypting the same entry again therefore reused the same nonce with the same secretbox key.
The code worked. Encryption roundtripped. Wrong keys failed. Different entry IDs produced different ciphertext. My original tests passed.
None of those tests asked the question that mattered: what happens when the same entry is edited and encrypted twice?
Why that is a real security bug
Memora uses NaCl secretbox, which is based on XSalsa20-Poly1305. A nonce does not need to be secret, but it must not be reused with the same key.
Reusing a nonce with a stream-cipher construction can leak relationships between encrypted messages and undermine the confidentiality the encryption is supposed to provide.
The dangerous part here was not a dramatic crash. It was that the application could continue behaving normally while violating a cryptographic assumption underneath the interface.
That is why security bugs often survive ordinary product testing.
The test suite had encoded the wrong assumption
One of the more useful moments in the review was noticing that the tests were not merely missing coverage. One test explicitly checked that the derived nonce was deterministic.
In other words, the suite was confidently protecting the flaw.
That changed how I thought about the fix. I did not only need to change the implementation. I needed to replace the property the tests considered correct.
The new invariant is:
- the per-object key may remain deterministic for the same object and key version;
- every encryption operation gets a fresh random nonce;
- the nonce is stored alongside the ciphertext;
- decrypting uses the stored nonce;
- encrypting the same plaintext for the same object twice should produce different ciphertext and nonce values.
Fixing it without breaking existing journals
Fortunately, the storage format already persisted the nonce with each encrypted record.
Decryption derived the key again but read the nonce from the stored ciphertext record. It did not need to regenerate the old deterministic nonce.
That gave me a clean migration path.
New encryptions now generate a random nonce with the cryptographic library. Existing entries remain readable because their original stored nonce is still available. There is no need to invalidate or silently rewrite every old record just to change how future encryption chooses a nonce.
Attachments use the same corrected pattern.
The regression test is about behaviour, not implementation
I added tests that encrypt the same entry twice using the same master key, object ID and key version.
The test now requires the two nonces to differ. It also requires both ciphertexts to decrypt correctly to the original plaintext.
The same check exists for attachment bytes.
That is a better security test than asserting how the derivation helper happens to be written. It protects the property the application actually needs.
Why I am publishing the mistake
It is easy to make a portfolio that contains only the moment after everything worked.
That leaves out a large part of engineering.
Finding a bug in your own system, understanding why the existing tests did not catch it, changing the implementation without breaking stored data, and adding a regression test is part of the work too.
In this case, the bug surfaced because I was preparing a private project for public release and reviewed the security claims against the code instead of treating the README as evidence that the implementation was correct.
The Memora repository is still private while the hardening branch goes through the remaining test and release gates. I would rather delay making a repository public than publish a security claim I no longer believe is justified.
That is the lesson I want the portfolio to preserve: confidence should come from being willing to inspect your own assumptions, including the ones you previously wrote tests to defend.