(url, offset=None)
| 20 | |
| 21 | |
| 22 | def get_totp_code(url, offset=None): # type: (str, Optional[int]) -> Optional[Tuple[str, int, int]] |
| 23 | comp = parse.urlparse(url) |
| 24 | if comp.scheme == 'otpauth': |
| 25 | secret = None |
| 26 | algorithm = 'SHA1' |
| 27 | digits = 6 |
| 28 | period = 30 |
| 29 | for k, v in parse.parse_qsl(comp.query): |
| 30 | if k == 'secret': |
| 31 | secret = v |
| 32 | elif k == 'algorithm': |
| 33 | algorithm = v |
| 34 | elif k == 'digits': |
| 35 | digits = int(v) |
| 36 | elif k == 'period': |
| 37 | period = int(v) |
| 38 | if secret: |
| 39 | tm_base = int(datetime.datetime.now().timestamp()) |
| 40 | tm = tm_base / period |
| 41 | if isinstance(offset, int): |
| 42 | tm += offset |
| 43 | alg = algorithm.lower() |
| 44 | if alg in hashlib.__dict__: |
| 45 | reminder = len(secret) % 8 |
| 46 | if reminder in {2, 4, 5, 7}: |
| 47 | padding = '=' * (8 - reminder) |
| 48 | secret += padding |
| 49 | key = bytes(b32decode(secret)) |
| 50 | msg = int(tm).to_bytes(8, byteorder='big') |
| 51 | hash = hashlib.__dict__[alg] |
| 52 | hm = hmac.new(key, msg=msg, digestmod=hash) |
| 53 | digest = hm.digest() |
| 54 | offset = digest[-1] & 0x0f |
| 55 | base = bytearray(digest[offset:offset + 4]) |
| 56 | base[0] = base[0] & 0x7f |
| 57 | code_int = int.from_bytes(base, byteorder='big') |
| 58 | code = str(code_int % (10 ** digits)) |
| 59 | if len(code) < digits: |
| 60 | code = code.rjust(digits, '0') |
| 61 | return code, period - (tm_base % period), period |
| 62 | else: |
| 63 | raise Exception(f'Unsupported hash algorithm: {algorithm}') |
| 64 | |
| 65 | |
| 66 | class Record: |
no test coverage detected