Thu Mar 13 22:41:11 PDT 2014
introducing tinypass.py v1.0
Before I can talk about what I did right, I have to talk about what I did wrong.
I host some files for a friend. They're great big zip files full of art, which he sells for money, so he'd like to put a password on them.
"Easy enough, this is exactly what HTTP Basic authentication is for."
But he'd like to be able to set passwords on files without having to ask me to manually fiddle with nginx config files.
"Well, I'll just whip up a quick forms-based thing for editing nginx config files. How hard could it be?"
(A chill wafts over your skin. Dread shivers up your spine.)
It took POSTed form data (
password) from a static HTML page, created a hashed password file from that password, appended a
location /filename block to a config file, then called
And as soon as it was actually used by someone who didn't write it, it blew up.
Oh v0.1, there were so many things wrong with you, how could I possibly count them all?
1.) It had to have permissions to edit nginx config files and reload the server. So I just ran it as root, which meant that I was running a python web server as root, which is an absolute security disaster. I'm listing this first, even though nothing bad actually happened (as far as I can tell) because it was just a complete unforced error. This was the first warning sign that I was doing something dumb, and I completely ignored it.
filename was just a text box, not a dropdown menu or picker, so it was trivially easy to typo a filename, and "set a password" on something that didn't exist. v0.1 had no error checking of any kind, so it couldn't refuse to do that.
3.) HTTP Basic is user-granular, but for this particular use we're doing file-granular permissions. HTTP Basic doesn't handle this very gracefully: if you're already logged in, and try to access a file you don't have permissions for, (say, if you bought several different items, or if you're me, and are trying to troubleshoot your broken fucking login system) then it just hits you with a
405 Authorization Needed error, no login window. Since HTTP Basic doesn't have a
log out button, (hint: where would you put it?) you have to restart the browser, or just wait around until the browser expires your login credentials, which is, as you'd guess, implementation-specific.
4.) Remember when I said that it just appended lines to a config file and reloaded the server? v0.1 had no conception of records-- it was a basic CRUD app in theory, but in practice it only created records, it couldn't read, update or delete them. It would quite happily, create two
location blocks for the same file.
Nginx will refuse to load a config file that has contradictory options. If you
restart it with a bad config file, then it won't start back up, and your web server goes down until you fix it.
A minor decision I made early on really saved my ass here. I heard that using
reload instead of
restart let nginx wait for clients to finish transferring data, so I used it in the script. Luckily,
reload won't take down your sever with a bad config file, it'll just refuse to load it.
So instead of blowing up the server, v0.1 just silently stopped applying changes until the config file was manually fixed.
Now, all these problems have solutions. You could conceivably train the end user to carefully work around the problems, on the theory that your software is great but the user is dumb, but when your tool collapses in a great heap of splinters at the slightest touch, then it's not the fault of the user, it's your fault.
You could also fix each of these bugs, add tests, etc, but the basic architecture of the program is just bad. It's fucked.
Let's try again.
tinypass.py v1.0 is designed to replace HTTP Basic with something about as secure, but a little friendlier to use, as well as letting the end user set and change passwords without fatally confusing nginx.
Rather than sending login credentials in the clear over HTTP headers, like HTTP Basic, tinypass.py sends credentials in the clear over cookies, which is much more secure. Best practices here would be hashed passwords and session ID cookies, which would require more work. I didn't feel like doing that work, because...
I don't really make a living selling games. I sell an ethical life.
How could I make a living selling games? Anyone who wants to pay me for my games doesn't have to. It's not like buying a chair, where they'll chase you down and taser you if you grab it and run out of the store. Nobody who wants my game on Windows or Mac has to pay for it to get it. Frankly, most of them don't.
So why do people pay for it? Because they understand a fundamental fact: For these games to exist, someone has to pay. If everyone just takes it, I'll have to get a real job and the supply will shut off. I don't want to get into one of the eternal tedious arguments about "software piracy". I will instead focus on one single, incontrovertible fact: I have a family to feed. If nobody pays for my games, I can't make them.
So what does someone get when they pay for my game? They get the knowledge that they are Part of the Solution and not Part of the Problem. They know that, in this case, they are one of the Good Guys. It is well-earned self-satisfaction, and it is valuable. To know they are doing the right thing, some people will happily pay 20 bucks. This is how I stay in business.
You can't stop piracy. DRM never works. You can't let somebody look at something without also letting them copy it. Cannot be done, impossible, full stop.
So tinypass.py is a speedbump, not an impassible wall. Since there are no confidential login credentials at risk, I don't go to any great lengths to keep them secure.
So hey! That's it. Check it out, I guess, just as long as you don't look at the commit history.