mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 07:48:57 +00:00
Add TotpManager and TOTP utilities
This commit is contained in:
61
src/password_manager/totp.py
Normal file
61
src/password_manager/totp.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""TOTP management utilities for SeedPass."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import pyotp
|
||||||
|
|
||||||
|
from utils import key_derivation
|
||||||
|
|
||||||
|
|
||||||
|
class TotpManager:
|
||||||
|
"""Helper methods for TOTP secrets and codes."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def derive_secret(seed: str, index: int) -> str:
|
||||||
|
"""Derive a TOTP secret from a BIP39 seed and index."""
|
||||||
|
return key_derivation.derive_totp_secret(seed, index)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def current_code(cls, seed: str, index: int, timestamp: int | None = None) -> str:
|
||||||
|
"""Return the TOTP code for the given seed 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 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()
|
@@ -18,3 +18,4 @@ websockets>=15.0.0
|
|||||||
tomli
|
tomli
|
||||||
hypothesis
|
hypothesis
|
||||||
mutmut==2.4.4
|
mutmut==2.4.4
|
||||||
|
pyotp>=2.8.0
|
||||||
|
@@ -152,3 +152,19 @@ def derive_index_key_seed_only(seed: str) -> bytes:
|
|||||||
def derive_index_key(seed: str) -> bytes:
|
def derive_index_key(seed: str) -> bytes:
|
||||||
"""Derive the index encryption key."""
|
"""Derive the index encryption key."""
|
||||||
return derive_index_key_seed_only(seed)
|
return derive_index_key_seed_only(seed)
|
||||||
|
|
||||||
|
|
||||||
|
def derive_totp_secret(seed: str, index: int) -> str:
|
||||||
|
"""Derive a base32-encoded TOTP secret from a BIP39 seed."""
|
||||||
|
try:
|
||||||
|
from local_bip85 import BIP85
|
||||||
|
|
||||||
|
seed_bytes = Bip39SeedGenerator(seed).Generate()
|
||||||
|
bip85 = BIP85(seed_bytes)
|
||||||
|
entropy = bip85.derive_entropy(index=index, bytes_len=10, app_no=2)
|
||||||
|
secret = base64.b32encode(entropy).decode("utf-8")
|
||||||
|
logger.debug(f"Derived TOTP secret for index {index}: {secret}")
|
||||||
|
return secret
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to derive TOTP secret: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
Reference in New Issue
Block a user