Developer diary · Day 5

An audit found twelve holes in the privacy tiers

Everything is green.

The build passes, the tests pass, fourteen tasks are closed.

Yesterday I had four questions. The third: what it would cost to fix. The fourth: whether the AI would carry it.

A green build answers neither. All it says is that nobody asked.

So I handed the code to a different model. Not to find bugs — to tell me whether we were going the right way.

🔍 Not an auditor, a navigator

The reason isn't the one that sounds good.

I had a doubt I hadn't said out loud until today. Not "are there bugs in here". Are we even going the right way.

And I had a model available to me that was stronger than the one writing the code. I called in Fable.

Not an auditor. A navigator.

The brief wasn't "have a look and see if it's all right". It was a list of areas: cryptography, the pasteboard, races and error handling, conformance to the decisions I'd already frozen. And a required output: a report and a fix-list — for the one that wrote the code.

One writes, the second reads, the first one does the fixing. That's a division of labour, not a trick.

🧨 The first finding

It's the only one marked critical, and it can be explained without a single technical word.

A board in vault mode has a salt — a random piece of data that, together with your passphrase, produces the key.

Switch the board out of vault mode and back to plain: the contents are honestly decrypted and laid down as plain text. The salt stays.

Switch that same board back into vault mode. The app sees the salt and draws a conclusion: there's been a vault here before, so this person knows the passphrase, so it has to be checked. It checks the only way it knows how — by trying to decrypt the first block.

And the first block is plain text now. It can't be decrypted.

So the check fails. With any passphrase.

Including the right one.

Twelve items. The first is the only one marked critical.
Twelve items. The first is the only one marked critical.

The board locks forever, and the key to it does not exist anywhere in the world. Not "hard to recover", not "you'll need a backup" — it isn't there.

How much does a person have to do for this to happen? Two switches. Over and back.

And here's something I'll note about myself before I forget it. This is unrecoverable data loss, and it reads as terrifying. My own reaction turned out to be markedly calmer than the finding deserves.

The reason is simple: there's nothing to lose and nobody to lose it. The app has one user, me, and I haven't put anything in the vault yet.

It needs fixing and I'll fix it. But today it's a line on a list, not a catastrophe. It becomes a catastrophe on precisely the day the first person shows up and puts something they need in there.

🕳 The other eleven

This is where it gets interesting, and it isn't about cryptography.

The cryptography, as it happens, was fine. The algorithms are right, the key is derived correctly, every board has its own salt, what's held in memory is the key and not the passphrase, every encryption takes a fresh nonce. That section of the audit is called "positive", and it's the shortest one in the file.

The holes weren't in the lock. They were in the doorways.

The passphrase is created without confirmation. The screen always says "enter your passphrase to unlock" — both when a vault already exists and when it doesn't. So on first setup, whatever was typed silently becomes the permanent passphrase. Typos included. And it can't be recovered — that was my own frozen decision.

An empty vault lets anyone in. There's a salt, there are no blocks yet, so there's nothing to check against — any passphrase gets in, and that one becomes the real one.

An encryption failure erased data. Couldn't encrypt on save, so it wrote an empty value. Which means a failed edit silently deleted the snippet.

A locked board could be opened without opening it. A board behind a fingerprint is locked and its contents hidden. But that board's settings menu is drawn anyway, and its privacy level can be changed without presenting a finger. Change it to plain, and the contents are on screen. Nobody broke the lock. They walked around it.

All four have exactly one thing in common. Not one of them is about the mathematics. All four are about the edges: when you've only just started, when it's still empty, when something went wrong, when somebody comes in from the wrong side.

The audit's verdict puts it in one line: none of the findings are in the cryptography itself, all of them are in state transitions no test covers.

That's why the build was green. You can make the lock perfect and still leave the door open, if you never thought about how people walk through it.

🎛 The switch that led nowhere

And one more, on its own, because it belongs to yesterday.

Yesterday I happily told you about the toggle that turns the glass off. It existed.

It wrote a value into the settings, and nothing read that value: in all nine places where a surface is drawn, the mode was hardcoded.

The switch switched. Nothing else happened.

Yesterday and today, the same disease: a feature exists in the interface and nowhere else. Yesterday a vault with no encryption, today a toggle that goes nowhere.

Yesterday I wrote that down as the one thing that wasn't in the plan.

I take it back. It isn't an incident.

Not bad code — the code is actually decent. At this speed there is one thing to be afraid of: a convincing demonstration of something that isn't there.

📏 A day of millimetres

Then came a day in which nothing happened except small things.

The audit never looked at cosmetics at all: its own scope says appearance stays with me. So these are mine.

The placeholder in the input field was drawn eight points lower than the text you type into that field appears. Eight points.

You can't catch that by eye. You can only catch it by comparing two screenshots pixel by pixel: an empty field, and the same field with one letter in it. The letter jumps.

The system colour picker ignored the size it was given and overlapped the circles beside it. And it wrote every intermediate shade into the recent-colours history while you dragged the slider — the history filled up with a dozen nearly identical greys.

This is not heroic work. But between "twelve holes in the privacy tiers" and "the placeholder sits eight points low", the only difference is the price of the mistake, not how closely you had to look.

🔌 Engineer honesty moment

Three things.

The first: once I'd closed all twelve items, I asked for another pass. The second pass found seven more.

Of different origins. One appeared along with my own fixes — after a vault was set up, search stopped seeing that board until you touched it again. One the first pass had found and I'd put off myself as not urgent. The rest had been sitting there from the beginning and simply hadn't made the first list.

That's how it works. Closing a list is not the same as finishing, and a second reader of the same code doesn't find the leftovers of the first one. They find something else.

The second: there were two UI tests in the project. Template stubs from Xcode that nobody had ever opened: they checked nothing, launched the app and then couldn't close it, taking the whole run down with them.

I didn't repair them. I deleted them.

A dead test is worse than a missing one: it manufactures a feeling of coverage where there is none. Exactly the same disease as the toggle, from the other side.

And this is where the part I like admitting less begins.

With the list in hand, I started cutting. Dead test — delete. Dead code — delete. Doesn't read well — rewrite. Every individual decision looked sensible, and I still can't point at the one that was wrong.

And the sum of them is that the project looks a great deal more torn open tonight than it did this morning.

The third, and it cost me an hour of its own.

I added a new field to a model and gave it a default value — in the initialiser, the way any normal person would. The app crashed on launch. Not on a fresh store: on the existing one, with real data in it.

It turns out the default has to sit in the field's declaration, not in the initialiser. Otherwise, when an existing store is migrated, it simply isn't there, and the database refuses to open.

An hour for one line. At least it's written down now.

💬 A question

Who reads your code besides you?

Before this project I was sure the answer was "the reviewer on the pull request", and that working alone the role simply doesn't exist. It turns out it does — somebody just has to be assigned to it, and preferably not the one who wrote the thing.

And yesterday's fourth question got an answer today, with a condition attached. It will carry it. As long as it isn't the one checking itself.

Tell me how it goes for you 👇

Next on the list is the part I haven't looked at even once.

And it shows 😉