#!python3 #coding:ascii import argparse import base64 import hashlib import hmac import os import secrets import time import threading from urllib.parse import urlparse URLS = [ 'otpauth://totp/user@company.domain?secret=BBBASE32' ] def get_totp_timestamp(timestamp:int=None): """https://www.rfc-editor.org/rfc/rfc6238""" t = timestamp or int(time.time()) T = t // 30 return T def compute_hotp(secret, factor:int): """https://www.rfc-editor.org/rfc/rfc4226""" C = factor.to_bytes(8, 'big') mac = hmac.digest(secret, C, 'sha1') offset = mac[19] & 0xf result = mac[offset:offset+4] result = int.from_bytes(result, 'big') result = (result & 0x7fffffff) % 1_000_000 return result def hotp_to_pin(hotp:int): h = str(hotp).zfill(6) h = h[:3] + ' ' + h[3:] return h def decode_url(totp_url): url = urlparse(totp_url) query = {} for arg in url.query.split('&'): k, v = arg.split('=', 1) k = k.lower() query[k] = v path = url.path.split('/')[-1] return (path, query) def _cli(): def _show_time_remaining(n): for i in range(n, 0, -1): print(f' Time remaining: {i:2}', end='\r', flush=True) time.sleep(1) while True: os.system('cls') totps = {} tm = get_totp_timestamp() for url in URLS: provider, query = decode_url(url) secret = query.get('secret') if not secret: continue secret = base64.b32decode(secret.upper()) totp = hotp_to_pin(compute_hotp(secret, tm)) totps[provider] = totp width = max([len(p) for p in totps.keys()]) fmt = f'{{:{width}}}\t{{}}' print() print('\n'.join( fmt.format(k,v) for k,v in totps.items() ), end='\n\n') time_remaining = 30 - int(time.time() % 30) threading.Thread( target=_show_time_remaining, args=(time_remaining,), daemon=True, ).start() time.sleep(time_remaining) def cli(): try: _cli() except (KeyboardInterrupt, EOFError): pass finally: print() # restore newline feed after \r def test(): """Test cases taken from RFCs""" print('----------HTOP----------') secret = bytes.fromhex('3132333435363738393031323334353637383930') for i in range(10): print( i, hmac.digest(secret, i.to_bytes(8, 'big'), 'sha1').hex(), compute_hotp(secret, i), ) print('------------TOTP Timestamp--------------') print(59, hex(get_totp_timestamp(59)), 1) print(1111111109, hex(get_totp_timestamp(1111111109)), '23523EC') print(1234567890 , hex(get_totp_timestamp(1234567890 )), '273EF07') print('----------6-Digit TOTP---------------') secret = b'12345678901234567890' print(59, compute_hotp(secret, get_totp_timestamp(59)), '287082') print(1111111109, compute_hotp(secret, get_totp_timestamp(1111111109)), '081804') print(1234567890, compute_hotp(secret, get_totp_timestamp(1234567890)), '005924') def main(): cli() raise SystemExit # TODO: add encrypted URL cache parser = argparse.ArgumentParser() mode = parser.add_mutually_exclusive_group() mode.add_argument('-a', '--add-url', type=str, desc='Add a new TOTP url') mode.add_argument('-d', '--delete-url', type=int, desc='Delete a known URL by its ID') mode.add_argument('-p', '--password', type=str, desc='Password on the database') mode.add_argument('-s', '--set-password', type=str, desc="Set password. Leave blank for interactive prompt") mode.add_argument('-c', '--cli', action='store_true', desc='Show TOTPs in CLI application') args = parser.parse_arguments() if __name__ == '__main__': main()