|
| 1 | +import base64 |
| 2 | +import datetime |
| 3 | +import hashlib |
| 4 | +import hmac |
| 5 | +import time |
| 6 | +from typing import Any, List, Optional |
| 7 | + |
| 8 | + |
| 9 | +class TOTP: |
| 10 | + """ |
| 11 | + Base class for OTP handlers. |
| 12 | + """ |
| 13 | + def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None, |
| 14 | + issuer: Optional[str] = None) -> None: |
| 15 | + self.digits = digits |
| 16 | + self.digest = digest |
| 17 | + self.secret = s |
| 18 | + self.name = name or 'Secret' |
| 19 | + self.issuer = issuer |
| 20 | + self.interval = 30 |
| 21 | + |
| 22 | + def generate_otp(self, input: int) -> str: |
| 23 | + """ |
| 24 | + :param input: the HMAC counter value to use as the OTP input. |
| 25 | + Usually either the counter, or the computed integer based on the Unix timestamp |
| 26 | + """ |
| 27 | + if input < 0: |
| 28 | + raise ValueError('input must be positive integer') |
| 29 | + hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest) |
| 30 | + hmac_hash = bytearray(hasher.digest()) |
| 31 | + offset = hmac_hash[-1] & 0xf |
| 32 | + code = ((hmac_hash[offset] & 0x7f) << 24 | |
| 33 | + (hmac_hash[offset + 1] & 0xff) << 16 | |
| 34 | + (hmac_hash[offset + 2] & 0xff) << 8 | |
| 35 | + (hmac_hash[offset + 3] & 0xff)) |
| 36 | + str_code = str(code % 10 ** self.digits) |
| 37 | + while len(str_code) < self.digits: |
| 38 | + str_code = '0' + str_code |
| 39 | + return str_code |
| 40 | + |
| 41 | + def byte_secret(self) -> bytes: |
| 42 | + secret = self.secret |
| 43 | + missing_padding = len(secret) % 8 |
| 44 | + if missing_padding != 0: |
| 45 | + secret += '=' * (8 - missing_padding) |
| 46 | + return base64.b32decode(secret, casefold=True) |
| 47 | + |
| 48 | + @staticmethod |
| 49 | + def int_to_bytestring(i: int, padding: int = 8) -> bytes: |
| 50 | + """ |
| 51 | + Turns an integer to the OATH specified |
| 52 | + bytestring, which is fed to the HMAC |
| 53 | + along with the secret |
| 54 | + """ |
| 55 | + result = bytearray() |
| 56 | + while i != 0: |
| 57 | + result.append(i & 0xFF) |
| 58 | + i >>= 8 |
| 59 | + # It's necessary to convert the final result from bytearray to bytes |
| 60 | + # because the hmac functions in python 2.6 and 3.3 don't work with |
| 61 | + # bytearray |
| 62 | + return bytes(bytearray(reversed(result)).rjust(padding, b'\0')) |
| 63 | + |
| 64 | + def code(self): |
| 65 | + """ |
| 66 | + Generate TOTP code |
| 67 | + """ |
| 68 | + now = datetime.datetime.now() |
| 69 | + timecode = int(time.mktime(now.timetuple()) / self.interval) |
| 70 | + return self.generate_otp(timecode) |
| 71 | + |
| 72 | + |
| 73 | +class TOTPMixin: |
| 74 | + |
| 75 | + def totp_generate_seed(self) -> str: |
| 76 | + """ |
| 77 | + Generate 2FA TOTP seed |
| 78 | +
|
| 79 | + Returns |
| 80 | + ------- |
| 81 | + str |
| 82 | + TOTP seed (token, secret key) |
| 83 | + """ |
| 84 | + result = self.private_request( |
| 85 | + "accounts/generate_two_factor_totp_key/", |
| 86 | + data=self.with_default_data({}) |
| 87 | + ) |
| 88 | + return result["totp_seed"] |
| 89 | + |
| 90 | + def totp_enable(self, verification_code: str) -> List[str]: |
| 91 | + """ |
| 92 | + Enable TOTP 2FA |
| 93 | +
|
| 94 | + Parameters |
| 95 | + ---------- |
| 96 | + verification_code: str |
| 97 | + 2FA verification code |
| 98 | +
|
| 99 | + Returns |
| 100 | + ------- |
| 101 | + List[str] |
| 102 | + Backup codes |
| 103 | + """ |
| 104 | + result = self.private_request( |
| 105 | + "accounts/enable_totp_two_factor", |
| 106 | + data=self.with_default_data({'verification_code': verification_code}) |
| 107 | + ) |
| 108 | + return result["backup_codes"] |
| 109 | + |
| 110 | + def totp_disable(self) -> bool: |
| 111 | + """ |
| 112 | + Disable TOTP 2FA |
| 113 | +
|
| 114 | + Returns |
| 115 | + ------- |
| 116 | + bool |
| 117 | + """ |
| 118 | + result = self.private_request( |
| 119 | + "accounts/disable_totp_two_factor", |
| 120 | + data=self.with_default_data({}) |
| 121 | + ) |
| 122 | + return result["status"] == "ok" |
| 123 | + |
| 124 | + def totp_generate_code(self, seed: str) -> str: |
| 125 | + """ |
| 126 | + Disable TOTP 2FA |
| 127 | +
|
| 128 | + Parameters |
| 129 | + ---------- |
| 130 | + seed: str |
| 131 | + TOTP seed (token, secret key) |
| 132 | +
|
| 133 | + Returns |
| 134 | + ------- |
| 135 | + str |
| 136 | + TOTP code |
| 137 | + """ |
| 138 | + return TOTP(seed).code() |
0 commit comments