refactor: move Python backend modules to project root

This commit is contained in:
2026-08-26 10:40:42 +02:00
parent b1065a4188
commit e4a93cdc12
8 changed files with 12 additions and 7 deletions
+107
View File
@@ -0,0 +1,107 @@
"""
TOTP (Time-based One-Time Password) generation for Bitwarden vault items.
"""
import hashlib
import hmac
import struct
import time
from typing import Optional
# Try to use pyotp if available, otherwise use manual implementation
try:
import pyotp
HAS_PYOTP = True
except ImportError:
HAS_PYOTP = False
def generate_totp(secret: str, period: int = 30, digits: int = 6) -> str:
"""Generate a TOTP code from a secret."""
if not secret:
return ""
# Clean the secret (remove spaces, convert to uppercase)
secret = secret.replace(" ", "").upper()
# Try to parse as otpauth:// URI
if secret.startswith("otpauth://"):
parsed = _parse_otpauth_uri(secret)
if parsed:
secret = parsed["secret"]
period = parsed.get("period", period)
digits = parsed.get("digits", digits)
if HAS_PYOTP:
totp = pyotp.TOTP(secret, interval=period, digits=digits)
return totp.now()
return _generate_totp_manual(secret, period, digits)
def get_totp_remaining_seconds(period: int = 30) -> int:
"""Get seconds remaining until current TOTP code expires."""
return period - (int(time.time()) % period)
def _generate_totp_manual(secret: str, period: int, digits: int) -> str:
"""Manual TOTP implementation when pyotp is not available."""
# Decode base32 secret
_BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
secret = secret.upper()
# Remove padding
padding_needed = (8 - len(secret) % 8) % 8
secret += "=" * padding_needed
# Decode base32
binary = b""
for char in secret:
if char == "=":
continue
try:
val = _BASE32_CHARS.index(char)
except ValueError:
continue
binary += struct.pack(">B", val)
# Time counter
counter = int(time.time()) // period
counter_bytes = struct.pack(">Q", counter)
# HMAC-SHA1
hmac_result = hmac.new(binary, counter_bytes, hashlib.sha1).digest()
# Dynamic truncation
offset = hmac_result[-1] & 0x0F
truncated = struct.unpack(
">I", hmac_result[offset : offset + 4]
)[0]
truncated &= 0x7FFFFFFF
# Generate code
code = truncated % (10 ** digits)
return str(code).zfill(digits)
def _parse_otpauth_uri(uri: str) -> Optional[dict]:
"""Parse an otpauth:// URI."""
# otpauth://totp/Label?secret=XXX&issuer=XXX&period=30&digits=6
if not uri.startswith("otpauth://"):
return None
parts = uri.split("?", 1)
if len(parts) < 2:
return None
params = {}
for param in parts[1].split("&"):
key, _, value = param.partition("=")
params[key] = value
secret = params.get("secret", "")
return {
"secret": secret,
"issuer": params.get("issuer", ""),
"period": int(params.get("period", "30")),
"digits": int(params.get("digits", "6")),
}