-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtotp.py
More file actions
64 lines (54 loc) · 1.91 KB
/
Copy pathtotp.py
File metadata and controls
64 lines (54 loc) · 1.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import hmac
import hashlib
import struct
import time
import base64
import re
def clean_secret(secret: str) -> str:
"""Removes spaces, hyphens, and formats secret to uppercase base32."""
if not secret:
return ""
# Strip spaces and non-alphanumeric characters
cleaned = re.sub(r'[^A-Z2-7]', '', secret.upper())
# Add base32 padding if needed
missing_padding = len(cleaned) % 8
if missing_padding:
cleaned += '=' * (8 - missing_padding)
return cleaned
def is_valid_secret(secret: str) -> bool:
"""Checks whether a string is a valid Base32 secret."""
try:
cleaned = clean_secret(secret)
if not cleaned:
return False
base64.b32decode(cleaned, casefold=True)
return True
except Exception:
return False
def generate_totp(secret: str, time_step: int = 30, digits: int = 6) -> tuple[str, int]:
"""
Generates an RFC 6238 TOTP 6-digit token from a Base32 secret.
Returns a tuple of: (formatted_code, seconds_remaining_in_cycle).
Returns ("------", 0) if the secret is invalid or empty.
"""
if not secret:
return ("------", 0)
try:
cleaned = clean_secret(secret)
key = base64.b32decode(cleaned, casefold=True)
except Exception:
return ("------", 0)
now = int(time.time())
time_counter = now // time_step
seconds_remaining = time_step - (now % time_step)
# Pack counter as big-endian 8-byte integer
msg = struct.pack(">Q", time_counter)
# Compute HMAC-SHA1
hmac_hash = hmac.new(key, msg, hashlib.sha1).digest()
# Dynamic truncation
offset = hmac_hash[-1] & 0x0F
binary_code = struct.unpack(">I", hmac_hash[offset:offset+4])[0] & 0x7FFFFFFF
# Generate requested digits
token = binary_code % (10 ** digits)
formatted_code = str(token).zfill(digits)
return (formatted_code, seconds_remaining)