← Back to context

Comment by 3eb7988a1663

5 days ago

It is a bit terse, but there is a 20-line Python implementation which cleared up the ideas for me: https://github.com/susam/mintotp

It is even shorter without boilerplates:

    def hotp(key, counter, digits=6, digest='sha1'):
        key = base64.b32decode(key.upper() + '=' * ((8 - len(key)) % 8))
        counter = struct.pack('>Q', counter)
        mac = hmac.new(key, counter, digest).digest()
        offset = mac[-1] & 0x0f
        binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
        return str(binary)[-digits:].zfill(digits)
    
    def totp(key, time_step=30, digits=6, digest='sha1'):
        return hotp(key, int(time.time() / time_step), digits, digest)

I love this one. The neat thing about TOTP is that while the algorithm itself is simple, the algorithms it depends on are also relatively simple, at least for cryptography. For HMAC you just need SHA1, and that can be implemented relatively easily without much more code. As a learning exercise it's quite good.

I adapted code for Java back in the day from here: https://github.com/j256/two-factor-auth/blob/master/src/main...

A bit longer but most of it is just boilerplate Java stuff to deal with polymorphism and a base32 implementation. I recall, stripping most of that away in our internal adapted version of that.

Key points:

- generate a 16 character base32 secret and stuff it in a totp link. otpauth://totp/Alice:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Alice

- stuff that in a QR code and show it to the user so they point their phone authenticator app at it to store the secret. We used a js library for this.

- store the secret with the user account in a secure way (we used aes encryption for this)

- when verifying, use the secret, a timestamp in seconds after the epoch divided by 30 (simple normalization step applied on the client as well) and use the user provided number to construct a sha1 hmac and grab the last digits and prepend with zeros. The calculated string should be the same as what the user typed from their token app as long as their clock is in sync.

- we actually implemented a grace period by calculating the before and after code as well so the user isn't screwed over if the number rotates while they were tapping out the code.

While relatively easy to implement, we ran into a lot of friction rolling this out to normal users. Basically non technical people find this stuff super confusing and we had to hand hold quite a few people through the process and we also had to deal with people that lost their secret, or kept on using the wrong code (for a different account). The UX of this stuff is just terrible. Be prepared to deal with a lot of support overhead if you choose to roll this out. A non trivial percentage of users will manage to lock themselves out of their accounts.

Those `>Q` and `>L` just make it more confusing for me, they just feel like a different language in the language...

  • Perhaps you could contribute a version in a language that uses more descriptive names, something like BitPacker.WriteInt64 for >Q if I'm guessing correctly what that means (I'd equally need to check the docs to know what format these letters represent, but I don't find it too confusing when you know it's simply some binary / byte array version of the same thing)