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
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
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
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
140 def token(self): 141 """The computed TOTP token.""" 142 return hotp(self.key, self.t(), digits=self.digits)
The computed TOTP token.
144 def t(self): 145 """The computed time step.""" 146 return ((int(self.time) - self.t0) // self.step) + self.drift
The computed time step.
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.
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.