Files
vps-manager/tests/test_totp.py
T
gouki d711ad5827 feat(vault): 凭据库(密码+2FA 动态码)与 MASTER_KEY 密钥托管
credentials 表为登录凭据唯一事实源(站点×登录方式,含 oauth/2FA);账号密码读写重定向凭据层并幂等迁移历史数据;TOTP 按 RFC6238 零依赖自实现,绑定需当前动态码校验;Key Escrow 防 MASTER_KEY 遗失;前端新增凭据库视图与账号 2FA 联动。64 pytest + 16 浏览器端到端验证通过。
2026-09-05 15:54:41 +00:00

113 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""TOTPRFC 6238)自实现测试
官方向量锚定正确性(RFC 6238 附录 BSHA1、20 字节 ASCII secret
"12345678901234567890"),覆盖:码生成、剩余秒数、录入校验窗口、
otpauth URI 解析/重建、随机 secret 可用性。
运行:.venv/bin/pytest tests/test_totp.py -v
"""
import pytest
from app.core import totp
# RFC 6238 附录 B 官方向量 secret 的 base32 形式
RFC_SECRET_B32 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
# (时刻 T, 官方 8 位码);本实现为 6 位 = 8 位码取模 10^6 补零
RFC_VECTORS = [
(59, "94287082"),
(1111111109, "07081804"),
(1111111111, "14050471"),
(1234567890, "89005924"),
(2000000000, "69279037"),
(20000000000, "65353130"),
]
@pytest.mark.parametrize("ts,code8", RFC_VECTORS)
def test_rfc6238_official_vectors(ts, code8):
"""逐条对齐 RFC 6238 附录 B 官方向量(6 位截断)"""
code6, _ = totp.totp_at(RFC_SECRET_B32, ts=ts)
assert code6 == str(int(code8) % 10**6).zfill(6)
def test_expires_in():
"""剩余秒数:T=59 窗口已过 29s 剩 1sT=60 新窗口剩 30s"""
_, left = totp.totp_at(RFC_SECRET_B32, ts=59)
assert left == 1
_, left = totp.totp_at(RFC_SECRET_B32, ts=60)
assert left == 30
def test_verify_window_tolerance():
"""录入校验容忍 ±1 步进时钟偏差,隔两个窗口必须失败"""
code, _ = totp.totp_at(RFC_SECRET_B32, ts=59) # counter=1
assert totp.verify(RFC_SECRET_B32, code, ts=59)
assert totp.verify(RFC_SECRET_B32, code, ts=61) # counter=2,窗口含 1
assert totp.verify(RFC_SECRET_B32, code, ts=80) # counter=2
assert not totp.verify(RFC_SECRET_B32, code, ts=150) # counter=5,超出窗口
def test_verify_rejects_bad_input():
assert not totp.verify(RFC_SECRET_B32, "12345") # 位数不对
assert not totp.verify(RFC_SECRET_B32, "abcdef") # 非数字
assert not totp.verify(RFC_SECRET_B32, "000000", ts=59) # 错误码
assert not totp.verify(RFC_SECRET_B32, "", ts=59)
def test_b32decode_tolerant():
"""小写/空白/缺填充都应正确解码"""
raw = totp.b32decode(RFC_SECRET_B32)
assert totp.b32decode(RFC_SECRET_B32.lower()) == raw
assert totp.b32decode("gezdg nbvgy 3tqoj qgezd gnbvg y3tqo jq") == raw
assert totp.b32decode(RFC_SECRET_B32.rstrip("=")) == raw
def test_parse_otpauth_uri_full():
uri = (
"otpauth://totp/GitHub:me@example.com"
"?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&period=30&digits=6"
)
p = totp.parse_otpauth_uri(uri)
assert p["secret"] == "JBSWY3DPEHPK3PXP"
assert p["issuer"] == "GitHub"
assert p["account"] == "me@example.com"
def test_parse_otpauth_uri_label_only():
"""无 issuer 参数时从 Label 前缀提取,account 需 URL 解码"""
p = totp.parse_otpauth_uri("otpauth://totp/Google:me%40gmail.com?secret=JBSWY3DPEHPK3PXP")
assert p["issuer"] == "Google"
assert p["account"] == "me@gmail.com"
def test_parse_otpauth_uri_rejects():
with pytest.raises(ValueError):
totp.parse_otpauth_uri("https://example.com") # 非 otpauth 协议
with pytest.raises(ValueError):
totp.parse_otpauth_uri("otpauth://hotp/x?secret=JBSWY3DPEHPK3PXP") # 非 TOTP
with pytest.raises(ValueError):
totp.parse_otpauth_uri("otpauth://totp/x?issuer=y") # 缺 secret
with pytest.raises(ValueError):
totp.parse_otpauth_uri("otpauth://totp/x?secret=JBSWY3DPEHPK3PXP&digits=8") # 非 6 位
with pytest.raises(ValueError):
totp.parse_otpauth_uri("otpauth://totp/x?secret=JBSWY3DPEHPK3PXP&period=60") # 非 30s
def test_build_otpauth_uri_roundtrip():
"""重建的 URI 可被解析回同样的 secret/issuer/account(换机导出场景)"""
uri = totp.build_otpauth_uri("JBSWY3DPEHPK3PXP", "GitHub", "me@example.com")
p = totp.parse_otpauth_uri(uri)
assert p["secret"] == "JBSWY3DPEHPK3PXP"
assert p["issuer"] == "GitHub"
assert p["account"] == "me@example.com"
def test_random_secret_usable():
"""随机 secret 生成的码应能通过自身校验(录入闭环)"""
s = totp.random_secret()
code, left = totp.totp_at(s)
assert len(code) == 6 and code.isdigit()
assert 1 <= left <= 30
assert totp.verify(s, code)