I finally got around to using
clj in a prototyping context. And it's going relatively smoothly so far. My only real complaint is that I seem to have to put
at the top of every file where I want to use my cool new
set literal syntax. I'm hoping there's some way to fix this by just putting it at the top of a
package file or something, but that naive solution doesn't seem to work. At first glance, there doesn't seem to be a way to express "load this project with a given, non-default
readtable", and I'm not entirely sure why yet.
The project I put some work into is an old piece of arcana from the earlier days of the Toronto CS Cabal. A simple voting system to help us decide what we're reading in a given week. The next step I'm going to take is implementing the actual voting. Step one was just the authentication system.
So here's the deal. Passwords suck, public keys aren't really being used widely for website/app authentication, and that doesn't seem to be something I can easily change. Authenticator apps and 2FA are propagating though. For low-security-requirement situtations, one plausible alternative to passwords is just using that authenticator. So, like, 1FA. The current state of
cl-vote is an implementation of such a system in Common Lisp.
The workflow looks like this:
- You register by picking a user name that hasn't already been picked.
- The system instantly sends you to a screen that displays a QR code compatible with FreeOTP or Authy or whatever
- When you want to log in later, enter your username and your authentication code
That's fairly simple. There's no need to remember passwords, though you do now need your phone or authenticator app/browser plugin/what-have-you.
During the construction of this, I briefly considered taking the Raskin approach of letting users log in with just their "password"s. Mechanically, this would involve iterating through the entire user database in order to find if there's anyone whose next code matches the input at login. I decided against it for three reasons
- It opens up the attack surface; instead of guessing a particular users' next code an attacker now needs to guess any valid code that collides with any existing user. Still improbably, but lets not throw caution to the wind entirely, huh?
- Makes login more expensive; instead of getting a particular user entry and checking their code against the given one, I need to do it for each user until I find a matching one. In the extreme case, like a user database big enough to shard, this will take an extremely long time. Which segues nicely into
- Makes login more inconsistent; if we hit the negative extreme case, it might take long enough to verify codes that the given code might have expired in the meantime, giving us false negatives. This doesn't feel like something that would happen too often, but it's not something that's trivially or implicitly soluble either.
A user name solves enough problems that I'm content burdening users with the task of picking one.
Once I combine it with some form of hammering protection, this system is resistant to the sorts of guessing attacks that plague password systems. It's still not resistant against server database breaches. Granted, this particular one is tricky to crack in that way because it's immune to injection attacks as a result of its' data storage model 1, but that's cold comfort. If you did manage to expropriate a user record, you'd gain access to that users' shared secret and could thereafter generate correct solutions for their account at will.
That's sort of the point.
One thing I could do, as a web app proprietor, is keep client fingerprints around and be a bit more cautious about logins coming from devices that a user hasn't used before. It's not entirely clear to me what to do if I detect an anomaly. I guess one thing I could do is request a challenge answer through a different contact method. Like an SMS sender or email, to which I would send a challenge generated by a session-specific secret key and then expect a response.
Doing that would also effectively mitigate the database expropriation attack. It wouldn't mitigate a successful server takeover, but I'm not sure there's a reasonable way to mitigate that at all yet. This might be good enough.
Account recovery codes are a thing that 2FA systems use to "make" "sure" that a user can still get into their account if they lose their phone/authenticator token/whatever. The way this works is by having the user write down a bunch of codes, each of which can presumably be used for a one-time entry into the system without other authentication methods being available. Cool, I guess. I haven't had to use them yet, and I suspect the sorts of systems I'm planning to build lend themselves more easily to the "make a new account" recovery path than this, but it might still be worth doing.
Mechanically, this means generating some number of alphanumeric codes that are either easy to write down or easy to remember. Then giving the user a workflow where they can enter one of these codes, at which point they are logged in but the code they used is marked as expired.
I'm going to try to implement a couple of these extras, then get bored and move on to the main point.
Which is collective decision making.
- And also the "Who would actually try to hack a Common Lisp app" thing. There are definitely lower hanging positions that bear more fruit.↩