logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault git clone https://hacktivis.me/git/mirror/drewdevault.com.git

TOTP-is-easy.md (3447B)


  1. ---
  2. title: TOTP for 2FA is incredibly easy to implement. So what's your excuse?
  3. date: 2022-10-18
  4. ---
  5. [Time-based one-time passwords][0] are one of the more secure approaches to 2FA
  6. — certainly much better than SMS. And it's much easier to implement than
  7. SMS as well. The algorithm is as follows:
  8. [0]: https://en.wikipedia.org/wiki/Time-based_one-time_password
  9. 1. Divide the current Unix timestamp by 30
  10. 1. Encode it as a 64-bit big endian integer
  11. 1. Write the encoded bytes to a SHA-1 HMAC initialized with the TOTP shared key
  12. 1. Let offs = hmac[-1] & 0xF
  13. 1. Let hash = decode hmac[offs .. offs + 4] as a 32-bit big-endian integer
  14. 1. Let code = (hash & 0x7FFFFFFF) % 1000000
  15. 1. Compare this code with the user's code
  16. You'll need a little dependency to generate QR codes with the [otpauth:// URL
  17. scheme][1], a little UI to present the QR code and store the shared secret in
  18. your database, and a quick update to your login flow, and then you're good to
  19. go.
  20. [1]: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
  21. Here's the implementation SourceHut uses in Python. I hereby release this code
  22. into the public domain, or creative commons zero, at your choice:
  23. ```python
  24. import base64
  25. import hashlib
  26. import hmac
  27. import struct
  28. import time
  29. def totp(secret, token):
  30. tm = int(time.time() / 30)
  31. key = base64.b32decode(secret)
  32. for ix in range(-2, 3):
  33. b = struct.pack(">q", tm + ix)
  34. hm = hmac.HMAC(key, b, hashlib.sha1).digest()
  35. offset = hm[-1] & 0x0F
  36. truncatedHash = hm[offset:offset + 4]
  37. code = struct.unpack(">L", truncatedHash)[0]
  38. code &= 0x7FFFFFFF
  39. code %= 1000000
  40. if token == code:
  41. return True
  42. return False
  43. ```
  44. This implementation has a bit of a tolerance added to make clock skew less of an
  45. issue, but that also means that the codes are longer-lived. Feel free to edit
  46. these tolerances if you so desire.
  47. Here's another one written in Hare, also public domain/CC-0.
  48. ```hare
  49. use crypto::hmac;
  50. use crypto::mac;
  51. use crypto::sha1;
  52. use encoding::base32;
  53. use endian;
  54. use time;
  55. // Computes a TOTP code for a given time and key.
  56. export fn totp(when: time::instant, key: []u8) uint = {
  57. const now = time::unix(when) / 30;
  58. const hmac = hmac::sha1(key);
  59. defer mac::finish(&hmac);
  60. let buf: [8]u8 = [0...];
  61. endian::beputu64(buf, now: u64);
  62. mac::write(&hmac, buf);
  63. let mac: [sha1::SIZE]u8 = [0...];
  64. mac::sum(&hmac, mac);
  65. const offs = mac[len(mac) - 1] & 0xF;
  66. const hash = mac[offs..offs+4];
  67. return ((endian::begetu32(hash)& 0x7FFFFFFF) % 1000000): uint;
  68. };
  69. @test fn totp() void = {
  70. const secret = "3N2OTFHXKLR2E3WNZSYQ====";
  71. const key = base32::decodestr(&base32::std_encoding, secret)!;
  72. defer free(key);
  73. const now = time::from_unix(1650183739);
  74. assert(totp(now, key) == 29283);
  75. };
  76. ```
  77. In any language, TOTP is just a couple of dozen lines of code even if there
  78. isn't already a library — and there is probably already a library. You
  79. don't have to store temporary SMS codes in the database, you don't have to worry
  80. about phishing, you don't have to worry about SIM swapping, and you don't have
  81. to sign up for some paid SMS API like Twilio. It's more secure and it's trivial
  82. to implement — so implement it already! Please!
  83. ---
  84. **Update 2022-10-19 @ 07:45 UTC**: A reader pointed out that it's important to
  85. have rate limiting on your TOTP attempts, or else a brute force attack can be
  86. effective. Fair point!