mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
"""TOTP management utilities for SeedPass."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from typing import Union
|
|
from urllib.parse import quote
|
|
from urllib.parse import urlparse, parse_qs, unquote
|
|
|
|
import qrcode
|
|
|
|
import pyotp
|
|
|
|
from utils import key_derivation
|
|
|
|
|
|
class TotpManager:
|
|
"""Helper methods for TOTP secrets and codes."""
|
|
|
|
@staticmethod
|
|
def derive_secret(seed: Union[str, bytes], index: int) -> str:
|
|
"""Derive a TOTP secret from a seed or raw key and index."""
|
|
return key_derivation.derive_totp_secret(seed, index)
|
|
|
|
@classmethod
|
|
def current_code(
|
|
cls, seed: Union[str, bytes], index: int, timestamp: int | None = None
|
|
) -> str:
|
|
"""Return the TOTP code for the given seed/key and index."""
|
|
secret = cls.derive_secret(seed, index)
|
|
totp = pyotp.TOTP(secret)
|
|
if timestamp is None:
|
|
return totp.now()
|
|
return totp.at(timestamp)
|
|
|
|
@staticmethod
|
|
def current_code_from_secret(secret: str, timestamp: int | None = None) -> str:
|
|
"""Return the TOTP code for a raw secret."""
|
|
totp = pyotp.TOTP(secret)
|
|
return totp.now() if timestamp is None else totp.at(timestamp)
|
|
|
|
@staticmethod
|
|
def parse_otpauth(uri: str) -> tuple[str, str, int, int]:
|
|
"""Parse an otpauth URI and return (label, secret, period, digits)."""
|
|
if not uri.startswith("otpauth://"):
|
|
raise ValueError("Not an otpauth URI")
|
|
parsed = urlparse(uri)
|
|
label = unquote(parsed.path.lstrip("/"))
|
|
qs = parse_qs(parsed.query)
|
|
secret = qs.get("secret", [""])[0].upper()
|
|
period = int(qs.get("period", ["30"])[0])
|
|
digits = int(qs.get("digits", ["6"])[0])
|
|
if not secret:
|
|
raise ValueError("Missing secret in URI")
|
|
return label, secret, period, digits
|
|
|
|
@staticmethod
|
|
def make_otpauth_uri(
|
|
label: str, secret: str, period: int = 30, digits: int = 6
|
|
) -> str:
|
|
"""Construct an otpauth:// URI for use with authenticator apps."""
|
|
label_enc = quote(label)
|
|
return f"otpauth://totp/{label_enc}?secret={secret}&period={period}&digits={digits}"
|
|
|
|
@staticmethod
|
|
def time_remaining(period: int = 30, timestamp: int | None = None) -> int:
|
|
"""Return seconds remaining until the current TOTP period resets."""
|
|
if timestamp is None:
|
|
timestamp = int(time.time())
|
|
return period - (timestamp % period)
|
|
|
|
@classmethod
|
|
def print_progress_bar(cls, period: int = 30) -> None:
|
|
"""Print a simple progress bar for the current TOTP period."""
|
|
remaining = cls.time_remaining(period)
|
|
total = period
|
|
bar_len = 20
|
|
while remaining > 0:
|
|
progress = total - remaining
|
|
filled = int(bar_len * progress / total)
|
|
bar = "[" + "#" * filled + "-" * (bar_len - filled) + "]"
|
|
sys.stdout.write(f"\r{bar} {remaining:2d}s")
|
|
sys.stdout.flush()
|
|
time.sleep(1)
|
|
remaining -= 1
|
|
sys.stdout.write("\n")
|
|
sys.stdout.flush()
|
|
|
|
@staticmethod
|
|
def print_qr_code(uri: str) -> None:
|
|
"""Display a QR code representing the provided URI in the terminal."""
|
|
qr = qrcode.QRCode(border=1)
|
|
qr.add_data(uri)
|
|
qr.make()
|
|
qr.print_ascii(invert=True)
|