refactor: move Python backend modules to project root
This commit is contained in:
@@ -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")),
|
||||
}
|
||||
Reference in New Issue
Block a user