[TOTP]:
A tool to generate time-based one time passwords straight from the command line. Easy to wrap into a script that copies it to the clipboard for easy 2FA, so I don’t need to have my phone in the office.
For some reason, I’ve always wanted to implement this myself and it turned out to be a fun project. Implemented straight from the RFCs, which I’d never personally done before.
RFC-6238 presents the Time-Based OTP, which builds on RFC 4226 for the HMAC-Based OTP. Zig’s std lib has the HMAC functions and algorithms, so fortunately/sadly I did not have to implement those from scratch.
The time-based OTP alg is pretty cool. Essentially, it takes a shared secret and a shared start time (usually the Unix epoch), interval size (usually 30s), and number of digits (usually 6). When you need a password, you calculate how many intervals have passed since the shared start time, and use that as the input to the HOTP (HMAC based one-time password).
The HOTP is a similar idea, except using a shared counter instead of time offset. You then use an HMAC alg on the secret and input value to get a hash. You look at a certain byte, its value tells you where to grab other bytes from, you do a little math, and you end up with a 6 digit code.
This is how your authenticator apps are always ticking away and can generate a secret even if you have no signal.
I also needed a base 32 decoder, which I wrote (most of) an implementation of from scratch. It only handles cases where the encoded data fits evenly into the base 32 chars because that’s really all I needed. If someone gives me a key with the padding = chars, I suppose I’ll handle it then.
To decode, you break the input stream into groups of 8 base32 chars, each char represents a 5-bit value, and then you treat that 8 x 5-bit group as a 5 x 8-bit group. I did this by putting 8 u5 values into a packed struct and then @bitCasting into a [5]u8. I doubt this is the most effective way to do it, but I thought it was a fun opportunity to use Zig’s arbitrarily-sized integers.
Along the way, I learned a little bit about re-interpreting bytes (@bitCast size mismatch for macos and windows but not linux and Reinterpreting bytes).
It’s still rough around the edges and haven’t implemented all the options, but it’s working for me and gives me a little satisfaction every time I need to generate a 2FA now. Still fairly new to Zig, so if anyone has any suggestions to how to better structure (I tried to keep all my side-effects in main and have everything else be essentially pure functions) or test things (not sure how to test main, for example), I’m all ears.
Note: this was only tested on a little-endian machine, so if you have a big-endian, please give it a shot and let me know if the tests pass and/or it works for you! Or if you have a suggestion on how to easily test that.