57 lines
2.0 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hmac
import struct
import time
# 160 bits, as recommended by HOTP RFC 4226, section 4, R6.
# Google Auth uses 80 bits by default but supports 160.
TOTP_SECRET_SIZE = 160
# The algorithm (and key URI format) allows customising these parameters but
# google authenticator doesn't support it
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
ALGORITHM = 'sha1'
DIGITS = 6
TIMESTEP = 30
class TOTP:
def __init__(self, key):
self._key = key
def match(self, code, t=None, window=TIMESTEP, timestep=TIMESTEP):
"""
:param code: authenticator code to check against this key
:param int t: current timestamp (seconds)
:param int window: fuzz window to account for slow fingers, network
latency, desynchronised clocks, ..., every code
valid between t-window an t+window is considered
valid
"""
if t is None:
t = time.time()
low = int((t - window) / timestep)
high = int((t + window) / timestep) + 1
return next((
counter for counter in range(low, high)
if hotp(self._key, counter) == code
), None)
def hotp(secret, counter):
# C is the 64b counter encoded in big-endian
C = struct.pack(">Q", counter)
mac = hmac.new(secret, msg=C, digestmod=ALGORITHM).digest()
# the data offset is the last nibble of the hash
offset = mac[-1] & 0xF
# code is the 4 bytes at the offset interpreted as a 31b big-endian uint
# (31b to avoid sign concerns). This effectively limits digits to 9 and
# hard-limits it to 10: each digit is normally worth 3.32 bits but the
# 10th is only worth 1.1 (9 digits encode 29.9 bits).
code = struct.unpack_from('>I', mac, offset)[0] & 0x7FFFFFFF
r = code % (10 ** DIGITS)
# NOTE: use text / bytes instead of int?
return r