authentik.stages.authenticator.oath

OATH helpers

  1"""OATH helpers"""
  2
  3import hmac
  4from hashlib import sha1
  5from struct import pack
  6from time import time
  7
  8
  9def hotp(key: bytes, counter: int, digits=6) -> int:
 10    """
 11    Implementation of the HOTP algorithm from `RFC 4226
 12    <http://tools.ietf.org/html/rfc4226#section-5>`_.
 13
 14    :param bytes key: The shared secret. A 20-byte string is recommended.
 15    :param int counter: The password counter.
 16    :param int digits: The number of decimal digits to generate.
 17
 18    :returns: The HOTP token.
 19    :rtype: int
 20
 21    >>> key = b'12345678901234567890'
 22    >>> for c in range(10):
 23    ...     hotp(key, c)
 24    755224
 25    287082
 26    359152
 27    969429
 28    338314
 29    254676
 30    287922
 31    162583
 32    399871
 33    520489
 34    """
 35    msg = pack(b">Q", counter)
 36    hs = hmac.new(key, msg, sha1).digest()
 37    hs = list(iter(hs))
 38
 39    offset = hs[19] & 0x0F
 40    bin_code = (
 41        (hs[offset] & 0x7F) << 24 | hs[offset + 1] << 16 | hs[offset + 2] << 8 | hs[offset + 3]
 42    )
 43    return bin_code % pow(10, digits)
 44
 45
 46def totp(key: bytes, step=30, t0=0, digits=6, drift=0) -> int:
 47    """
 48    Implementation of the TOTP algorithm from `RFC 6238
 49    <http://tools.ietf.org/html/rfc6238#section-4>`_.
 50
 51    :param bytes key: The shared secret. A 20-byte string is recommended.
 52    :param int step: The time step in seconds. The time-based code changes
 53        every ``step`` seconds.
 54    :param int t0: The Unix time at which to start counting time steps.
 55    :param int digits: The number of decimal digits to generate.
 56    :param int drift: The number of time steps to add or remove. Delays and
 57        clock differences might mean that you have to look back or forward a
 58        step or two in order to match a token.
 59
 60    :returns: The TOTP token.
 61    :rtype: int
 62
 63    >>> key = b'12345678901234567890'
 64    >>> now = int(time())
 65    >>> for delta in range(0, 200, 20):
 66    ...     totp(key, t0=(now-delta))
 67    755224
 68    755224
 69    287082
 70    359152
 71    359152
 72    969429
 73    338314
 74    338314
 75    254676
 76    287922
 77    """
 78    return TOTP(key, step, t0, digits, drift).token()
 79
 80
 81class TOTP:
 82    """
 83    An alternate TOTP interface.
 84
 85    This provides access to intermediate steps of the computation. This is a
 86    living object: the return values of ``t`` and ``token`` will change along
 87    with other properties and with the passage of time.
 88
 89    :param bytes key: The shared secret. A 20-byte string is recommended.
 90    :param int step: The time step in seconds. The time-based code changes
 91        every ``step`` seconds.
 92    :param int t0: The Unix time at which to start counting time steps.
 93    :param int digits: The number of decimal digits to generate.
 94    :param int drift: The number of time steps to add or remove. Delays and
 95        clock differences might mean that you have to look back or forward a
 96        step or two in order to match a token.
 97
 98    >>> key = b'12345678901234567890'
 99    >>> totp = TOTP(key)
100    >>> totp.time = 0
101    >>> totp.t()
102    0
103    >>> totp.token()
104    755224
105    >>> totp.time = 30
106    >>> totp.t()
107    1
108    >>> totp.token()
109    287082
110    >>> totp.verify(287082)
111    True
112    >>> totp.verify(359152)
113    False
114    >>> totp.verify(359152, tolerance=1)
115    True
116    >>> totp.drift
117    1
118    >>> totp.drift = 0
119    >>> totp.verify(359152, tolerance=1, min_t=3)
120    False
121    >>> totp.drift
122    0
123    >>> del totp.time
124    >>> totp.t0 = int(time()) - 60
125    >>> totp.t()
126    2
127    >>> totp.token()
128    359152
129    """
130
131    def __init__(self, key: bytes, step=30, t0=0, digits=6, drift=0):
132        self.key = key
133        self.step = step
134        self.t0 = t0
135        self.digits = digits
136        self.drift = drift
137        self._time = None
138
139    def token(self):
140        """The computed TOTP token."""
141        return hotp(self.key, self.t(), digits=self.digits)
142
143    def t(self):
144        """The computed time step."""
145        return ((int(self.time) - self.t0) // self.step) + self.drift
146
147    @property
148    def time(self):
149        """
150        The current time.
151
152        By default, this returns time.time() each time it is accessed. If you
153        want to generate a token at a specific time, you can set this property
154        to a fixed value instead. Deleting the value returns it to its 'live'
155        state.
156
157        """
158        return self._time if (self._time is not None) else time()
159
160    @time.setter
161    def time(self, value):
162        self._time = value
163
164    @time.deleter
165    def time(self):
166        self._time = None
167
168    def verify(self, token, tolerance=0, min_t=None):
169        """
170        A high-level verification helper.
171
172        :param int token: The provided token.
173        :param int tolerance: The amount of clock drift you're willing to
174            accommodate, in steps. We'll look for the token at t values in
175            [t - tolerance, t + tolerance].
176        :param int min_t: The minimum t value we'll accept. As a rule, this
177            should be one larger than the largest t value of any previously
178            accepted token.
179        :rtype: bool
180
181        Iff this returns True, `self.drift` will be updated to reflect the
182        drift value that was necessary to match the token.
183
184        """
185        drift_orig = self.drift
186        verified = False
187
188        for offset in range(-tolerance, tolerance + 1):
189            self.drift = drift_orig + offset
190            if (min_t is not None) and (self.t() < min_t):
191                continue
192            if self.token() == token:
193                verified = True
194                break
195        else:
196            self.drift = drift_orig
197
198        return verified
def hotp(key: bytes, counter: int, digits=6) -> int:
10def hotp(key: bytes, counter: int, digits=6) -> int:
11    """
12    Implementation of the HOTP algorithm from `RFC 4226
13    <http://tools.ietf.org/html/rfc4226#section-5>`_.
14
15    :param bytes key: The shared secret. A 20-byte string is recommended.
16    :param int counter: The password counter.
17    :param int digits: The number of decimal digits to generate.
18
19    :returns: The HOTP token.
20    :rtype: int
21
22    >>> key = b'12345678901234567890'
23    >>> for c in range(10):
24    ...     hotp(key, c)
25    755224
26    287082
27    359152
28    969429
29    338314
30    254676
31    287922
32    162583
33    399871
34    520489
35    """
36    msg = pack(b">Q", counter)
37    hs = hmac.new(key, msg, sha1).digest()
38    hs = list(iter(hs))
39
40    offset = hs[19] & 0x0F
41    bin_code = (
42        (hs[offset] & 0x7F) << 24 | hs[offset + 1] << 16 | hs[offset + 2] << 8 | hs[offset + 3]
43    )
44    return bin_code % pow(10, digits)

Implementation of the HOTP algorithm from RFC 4226 <http://tools.ietf.org/html/rfc4226#section-5>_.

:param bytes key: The shared secret. A 20-byte string is recommended. :param int counter: The password counter. :param int digits: The number of decimal digits to generate.

:returns: The HOTP token. :rtype: int

>>> key = b'12345678901234567890'
>>> for c in range(10):
...     hotp(key, c)
755224
287082
359152
969429
338314
254676
287922
162583
399871
520489
def totp(key: bytes, step=30, t0=0, digits=6, drift=0) -> int:
47def totp(key: bytes, step=30, t0=0, digits=6, drift=0) -> int:
48    """
49    Implementation of the TOTP algorithm from `RFC 6238
50    <http://tools.ietf.org/html/rfc6238#section-4>`_.
51
52    :param bytes key: The shared secret. A 20-byte string is recommended.
53    :param int step: The time step in seconds. The time-based code changes
54        every ``step`` seconds.
55    :param int t0: The Unix time at which to start counting time steps.
56    :param int digits: The number of decimal digits to generate.
57    :param int drift: The number of time steps to add or remove. Delays and
58        clock differences might mean that you have to look back or forward a
59        step or two in order to match a token.
60
61    :returns: The TOTP token.
62    :rtype: int
63
64    >>> key = b'12345678901234567890'
65    >>> now = int(time())
66    >>> for delta in range(0, 200, 20):
67    ...     totp(key, t0=(now-delta))
68    755224
69    755224
70    287082
71    359152
72    359152
73    969429
74    338314
75    338314
76    254676
77    287922
78    """
79    return TOTP(key, step, t0, digits, drift).token()

Implementation of the TOTP algorithm from RFC 6238 <http://tools.ietf.org/html/rfc6238#section-4>_.

:param bytes key: The shared secret. A 20-byte string is recommended. :param int step: The time step in seconds. The time-based code changes every step seconds. :param int t0: The Unix time at which to start counting time steps. :param int digits: The number of decimal digits to generate. :param int drift: The number of time steps to add or remove. Delays and clock differences might mean that you have to look back or forward a step or two in order to match a token.

:returns: The TOTP token. :rtype: int

>>> key = b'12345678901234567890'
>>> now = int(time())
>>> for delta in range(0, 200, 20):
...     totp(key, t0=(now-delta))
755224
755224
287082
359152
359152
969429
338314
338314
254676
287922
class TOTP:
 82class TOTP:
 83    """
 84    An alternate TOTP interface.
 85
 86    This provides access to intermediate steps of the computation. This is a
 87    living object: the return values of ``t`` and ``token`` will change along
 88    with other properties and with the passage of time.
 89
 90    :param bytes key: The shared secret. A 20-byte string is recommended.
 91    :param int step: The time step in seconds. The time-based code changes
 92        every ``step`` seconds.
 93    :param int t0: The Unix time at which to start counting time steps.
 94    :param int digits: The number of decimal digits to generate.
 95    :param int drift: The number of time steps to add or remove. Delays and
 96        clock differences might mean that you have to look back or forward a
 97        step or two in order to match a token.
 98
 99    >>> key = b'12345678901234567890'
100    >>> totp = TOTP(key)
101    >>> totp.time = 0
102    >>> totp.t()
103    0
104    >>> totp.token()
105    755224
106    >>> totp.time = 30
107    >>> totp.t()
108    1
109    >>> totp.token()
110    287082
111    >>> totp.verify(287082)
112    True
113    >>> totp.verify(359152)
114    False
115    >>> totp.verify(359152, tolerance=1)
116    True
117    >>> totp.drift
118    1
119    >>> totp.drift = 0
120    >>> totp.verify(359152, tolerance=1, min_t=3)
121    False
122    >>> totp.drift
123    0
124    >>> del totp.time
125    >>> totp.t0 = int(time()) - 60
126    >>> totp.t()
127    2
128    >>> totp.token()
129    359152
130    """
131
132    def __init__(self, key: bytes, step=30, t0=0, digits=6, drift=0):
133        self.key = key
134        self.step = step
135        self.t0 = t0
136        self.digits = digits
137        self.drift = drift
138        self._time = None
139
140    def token(self):
141        """The computed TOTP token."""
142        return hotp(self.key, self.t(), digits=self.digits)
143
144    def t(self):
145        """The computed time step."""
146        return ((int(self.time) - self.t0) // self.step) + self.drift
147
148    @property
149    def time(self):
150        """
151        The current time.
152
153        By default, this returns time.time() each time it is accessed. If you
154        want to generate a token at a specific time, you can set this property
155        to a fixed value instead. Deleting the value returns it to its 'live'
156        state.
157
158        """
159        return self._time if (self._time is not None) else time()
160
161    @time.setter
162    def time(self, value):
163        self._time = value
164
165    @time.deleter
166    def time(self):
167        self._time = None
168
169    def verify(self, token, tolerance=0, min_t=None):
170        """
171        A high-level verification helper.
172
173        :param int token: The provided token.
174        :param int tolerance: The amount of clock drift you're willing to
175            accommodate, in steps. We'll look for the token at t values in
176            [t - tolerance, t + tolerance].
177        :param int min_t: The minimum t value we'll accept. As a rule, this
178            should be one larger than the largest t value of any previously
179            accepted token.
180        :rtype: bool
181
182        Iff this returns True, `self.drift` will be updated to reflect the
183        drift value that was necessary to match the token.
184
185        """
186        drift_orig = self.drift
187        verified = False
188
189        for offset in range(-tolerance, tolerance + 1):
190            self.drift = drift_orig + offset
191            if (min_t is not None) and (self.t() < min_t):
192                continue
193            if self.token() == token:
194                verified = True
195                break
196        else:
197            self.drift = drift_orig
198
199        return verified

An alternate TOTP interface.

This provides access to intermediate steps of the computation. This is a living object: the return values of t and token will change along with other properties and with the passage of time.

:param bytes key: The shared secret. A 20-byte string is recommended. :param int step: The time step in seconds. The time-based code changes every step seconds. :param int t0: The Unix time at which to start counting time steps. :param int digits: The number of decimal digits to generate. :param int drift: The number of time steps to add or remove. Delays and clock differences might mean that you have to look back or forward a step or two in order to match a token.

>>> key = b'12345678901234567890'
>>> totp = TOTP(key)
>>> totp.time = 0
>>> totp.t()
0
>>> totp.token()
755224
>>> totp.time = 30
>>> totp.t()
1
>>> totp.token()
287082
>>> totp.verify(287082)
True
>>> totp.verify(359152)
False
>>> totp.verify(359152, tolerance=1)
True
>>> totp.drift
1
>>> totp.drift = 0
>>> totp.verify(359152, tolerance=1, min_t=3)
False
>>> totp.drift
0
>>> del totp.time
>>> totp.t0 = int(time()) - 60
>>> totp.t()
2
>>> totp.token()
359152
TOTP(key: bytes, step=30, t0=0, digits=6, drift=0)
132    def __init__(self, key: bytes, step=30, t0=0, digits=6, drift=0):
133        self.key = key
134        self.step = step
135        self.t0 = t0
136        self.digits = digits
137        self.drift = drift
138        self._time = None
key
step
t0
digits
drift
def token(self):
140    def token(self):
141        """The computed TOTP token."""
142        return hotp(self.key, self.t(), digits=self.digits)

The computed TOTP token.

def t(self):
144    def t(self):
145        """The computed time step."""
146        return ((int(self.time) - self.t0) // self.step) + self.drift

The computed time step.

time
148    @property
149    def time(self):
150        """
151        The current time.
152
153        By default, this returns time.time() each time it is accessed. If you
154        want to generate a token at a specific time, you can set this property
155        to a fixed value instead. Deleting the value returns it to its 'live'
156        state.
157
158        """
159        return self._time if (self._time is not None) else time()

The current time.

By default, this returns time.time() each time it is accessed. If you want to generate a token at a specific time, you can set this property to a fixed value instead. Deleting the value returns it to its 'live' state.

def verify(self, token, tolerance=0, min_t=None):
169    def verify(self, token, tolerance=0, min_t=None):
170        """
171        A high-level verification helper.
172
173        :param int token: The provided token.
174        :param int tolerance: The amount of clock drift you're willing to
175            accommodate, in steps. We'll look for the token at t values in
176            [t - tolerance, t + tolerance].
177        :param int min_t: The minimum t value we'll accept. As a rule, this
178            should be one larger than the largest t value of any previously
179            accepted token.
180        :rtype: bool
181
182        Iff this returns True, `self.drift` will be updated to reflect the
183        drift value that was necessary to match the token.
184
185        """
186        drift_orig = self.drift
187        verified = False
188
189        for offset in range(-tolerance, tolerance + 1):
190            self.drift = drift_orig + offset
191            if (min_t is not None) and (self.t() < min_t):
192                continue
193            if self.token() == token:
194                verified = True
195                break
196        else:
197            self.drift = drift_orig
198
199        return verified

A high-level verification helper.

:param int token: The provided token. :param int tolerance: The amount of clock drift you're willing to accommodate, in steps. We'll look for the token at t values in [t - tolerance, t + tolerance]. :param int min_t: The minimum t value we'll accept. As a rule, this should be one larger than the largest t value of any previously accepted token. :rtype: bool

Iff this returns True, self.drift will be updated to reflect the drift value that was necessary to match the token.