otp.py 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. # -*- coding: utf-8 -*-
  2. """
  3. # HOTP/TOTP support
  4. # Copyright (c) 2019-2023 Michael Büsch <m@bues.ch>
  5. # Licensed under the GNU/GPL version 2 or later.
  6. """
  7. import time
  8. from base64 import b32decode
  9. import binascii
  10. import hmac
  11. import hashlib
  12. __all__ = [
  13. "OtpError",
  14. "hotp",
  15. "totp",
  16. ]
  17. class OtpError(Exception):
  18. """HOTP/TOTP exception.
  19. """
  20. def hotp(key, counter, nrDigits=6, hmacHash="SHA1"):
  21. """HOTP - An HMAC-Based One-Time Password Algorithm.
  22. key: The HOTP key. Either raw bytes or a base32 encoded string.
  23. counter: The HOTP counter integer.
  24. nrDigits: The number of digits to return. Can be 1 to 8.
  25. hmacHash: The name string of the hashing algorithm.
  26. Returns the calculated HOTP token string.
  27. """
  28. if isinstance(key, str):
  29. try:
  30. key = b32decode(key.encode("UTF-8"), casefold=True)
  31. except (binascii.Error, UnicodeError):
  32. raise OtpError("Invalid key.")
  33. if not (0 <= counter <= (2 ** 64) - 1):
  34. raise OtpError("Invalid counter.")
  35. if not (1 <= nrDigits <= 8):
  36. raise OtpError("Invalid number of digits.")
  37. try:
  38. hmacHash = hmacHash.replace("-", "")
  39. hmacHash = hmacHash.replace("_", "")
  40. hmacHash = hmacHash.replace(" ", "")
  41. hmacHash = hmacHash.upper().strip()
  42. hmacHash = {
  43. "SHA1" : hashlib.sha1,
  44. "SHA256" : hashlib.sha256,
  45. "SHA512" : hashlib.sha512,
  46. }[hmacHash]
  47. except KeyError:
  48. raise OtpError("Invalid HMAC hash type.")
  49. counter = counter.to_bytes(length=8, byteorder="big", signed=False)
  50. h = bytearray(hmac.new(key, counter, hmacHash).digest())
  51. offset = h[19] & 0xF
  52. h[offset] &= 0x7F
  53. hSlice = int.from_bytes(h[offset:offset+4], byteorder="big", signed=False)
  54. otp = hSlice % (10 ** nrDigits)
  55. fmt = "%0" + str(nrDigits) + "d"
  56. return fmt % otp
  57. def totp(key, nrDigits=6, hmacHash="SHA1", t=None):
  58. """TOTP - Time-Based One-Time Password Algorithm.
  59. nrDigits: The number of digits to return. Can be 1 to 8.
  60. hmacHash: The name string of the hashing algorithm.
  61. t: Optional; the time in seconds. Uses time.time(), if not given.
  62. Returns the calculated TOTP token string.
  63. """
  64. if t is None:
  65. t = time.time()
  66. t = (int(round(t)) // 30) - 1
  67. return hotp(key, t, nrDigits, hmacHash)