The Server I Said I Didn't Need
I put a password in an Android app. Not quite as badly as that sentence makes it sound, but close enough that I deserve the sentence.
The app is Lethio Macro Tracker, and one of the things it can do is contribute missing products back to Open Food Facts. I wrote about that recently: scan something neither the local database nor Open Food Facts knows, type in what is on the label, and send it back. The contribution goes under an account belonging to the app, so the person using it does not need an Open Food Facts account of their own.
I was rather pleased with this. There are no Lethio accounts, no registration, no email address. Each installation gets an identifier so Open Food Facts can distinguish one contributor from another if somebody starts submitting rubbish, but that identifier does not tell me who the person is. Simple, apart from the small matter of how the app logs into Open Food Facts.
The thread
My first solution was to have the app do it, which sounds obvious until you ask where the password lives.
An Android APK is a package of software handed to a stranger. They own the phone. They can copy the package, unpack it, decompile it, inspect its resources and watch what it does while it runs. Anything the app needs in order to recover a secret is necessarily in the app too. Encrypt the password, and the decryption key has to be there. Split it into pieces, and the instructions for putting the pieces together have to be there. Hide it in native code, and it is harder to find, perhaps, but still there. Obfuscation can make extraction annoying; it cannot make a secret secret from the person whose computer must eventually possess it in usable form.
This should have been obvious to me, and it wasn't. What did bother me was the feeling that I was relying on cleverness to protect a credential. I have worked in security, and one of the better managers I worked for taught me to pull on things like that: if one part of a system makes you uncomfortable, don't explain the discomfort away, follow it and see what falls out.
So I pulled. The password fell out first, and then quite a lot of the architecture followed it.
The annoying correct answer
The credential has to live somewhere the Android app cannot reach, which means a server.
This is slightly inconvenient, because I have spent a fair amount of time on this site proudly explaining that my software does not need my servers. I still believe in that. Kofte does not need one, Time Converter does not need one, and if a feature can run on the device then the device is where it belongs. But "we run no servers" is not a religion. The contribution feature has to authenticate to somebody else's service, and if I put that authentication secret on every phone it is no longer a secret. I can either pretend that sufficiently elaborate hiding changes that fact, or put the credential on a machine I control.
So Lethio now has a very small backend. Personal growth is humiliating.
The world's most boring server
It does almost nothing. The app sends a product contribution and its installation identifier to the relay; the relay checks that the request is something it is actually willing to forward, adds the Open Food Facts authentication, sends it onward and returns the result.
There are no Lethio accounts. No user database, no submission database, no sessions, cookies or analytics. The Open Food Facts credential never reaches the phone, which also means that somebody who pulls the APK apart gets exactly what they should get: my code, and not my password. The server exists because it knows one thing the client must not.
Then the privacy problem moved
Fixing one problem created another. Previously the phone talked directly to Open Food Facts, so their server necessarily saw the connection coming from the user's IP address. Put my relay in the middle and Open Food Facts sees my server instead, which is good for the user's privacy from Open Food Facts. It also means my server sees the connection instead, and that is not a detail I get to wave away while claiming not to track people.
Web servers love logs, and logs are extremely helpful. Nginx will cheerfully write down who connected, when they connected, what they requested and what happened next. Application servers have logs. System services have journals. Hosting infrastructure may have logs of its own.
I know this particularly well because there is a live demonstration on the front page of
this website. My server is attacked constantly. Fail2ban reads the authentication logs,
finds repeated SSH failures and blocks the addresses responsible, and the Wall of Shame
publishes a deliberately truncated version of those addresses. The full addresses stay in
the server log because in that case I have an actual operational reason to retain them: I
cannot block an attacker without knowing where the attack is coming from. I have been
almost comically careful about documenting that, so it would be a bit rich to build a
privacy-focused app and accidentally leave its users sitting in
/var/log/nginx/access.log.
The contribution relay therefore gets the opposite treatment. Where logs are not required, they do not get retained. Request bodies are not stored. Installation identifiers are not accumulated into a convenient little database "just in case." Logs that must temporarily exist for operating the service get aggressive retention rather than becoming archaeological strata because disk space is cheap.
Servers are not inherently bad. But data you never collect is data you never have to protect, leak, sell, subpoena, explain or eventually remember to delete.
Better than where I started
The funny thing is that I ended up happier with the privacy model than before. The first design looked private because I wasn't in the path, but it required distributing a shared credential to every copy of the application, and relied on that credential remaining difficult enough to extract. The new design puts a machine I control in the path, which gives me another responsibility. In return, the authentication secret stays where secrets belong, Open Food Facts no longer receives the user's IP address directly when they contribute, and I can make deliberate decisions about exactly what my part of the system retains.
There is a cost to that, and I should say it plainly: users now have to trust my server for this optional feature. The contribution relay can see an incoming IP address while handling a connection, because that is how TCP/IP works. I can configure my software not to retain it. I cannot make the network forget that an address existed while the connection was open, and I cannot make promises on behalf of every layer of infrastructure between somebody's phone and mine.
That distinction matters more to me than being able to put "ZERO SERVERS" in large friendly letters on a website.
The configuration is public too. The nginx and systemd files, the tests that pin down the privacy properties, and a mildly excessive diagram of the request path live in the Lethio relay infrastructure repository. It is not a dump of the server: operational details and secrets stay elsewhere. It is the small boundary described here, with the reasoning left attached.
Things I apparently know now
This project keeps doing this to me. I started building a calorie tracker. Then I needed a food database and learned rather more about database licensing than I expected, I needed search and learned why testing ranking by vibes is a bad idea, and I needed contributions and learned that a secret distributed to an untrusted computer is not a secret. Every time, the first version works until I understand the problem well enough to become dissatisfied with it, and I am beginning to think that is the useful part.
I am still inexperienced as a developer. There are whole categories of mistake I have not encountered yet, which presumably means I am currently making several of them. But security has given me one habit that transfers rather well: when something feels wrong, pull the thread. Sometimes you find nothing, sometimes you find your password sitting in an APK, and sometimes, after a day of dismantling something you were quite proud of, you end up with a system you trust more than the one you started with.
I'll take that.