Skip to content

Instantly share code, notes, and snippets.

@0xpizza
Last active January 13, 2023 05:56
Show Gist options
  • Save 0xpizza/00b9d6c36006188c7e76403db8a72835 to your computer and use it in GitHub Desktop.
Save 0xpizza/00b9d6c36006188c7e76403db8a72835 to your computer and use it in GitHub Desktop.
#!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()
@gdomst
Copy link

gdomst commented Jan 4, 2023

Doesn't work

Traceback (most recent call last):
  File "totp.py", line 146, in <module>
    main()
  File "totp.py", line 128, in main
    cli()
  File "totp.py", line 94, in cli
    _cli()
  File "totp.py", line 72, in _cli
    secret = base64.b32decode(secret.upper())
  File "/usr/lib/python3.7/base64.py", line 205, in b32decode
    raise binascii.Error('Incorrect padding')
binascii.Error: Incorrect padding

@0xpizza
Copy link
Author

0xpizza commented Jan 13, 2023

Okay, just for you I put in an actual base32 encoded secret. Enjoy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment