feat(vault): 凭据库(密码+2FA 动态码)与 MASTER_KEY 密钥托管
credentials 表为登录凭据唯一事实源(站点×登录方式,含 oauth/2FA);账号密码读写重定向凭据层并幂等迁移历史数据;TOTP 按 RFC6238 零依赖自实现,绑定需当前动态码校验;Key Escrow 防 MASTER_KEY 遗失;前端新增凭据库视图与账号 2FA 联动。64 pytest + 16 浏览器端到端验证通过。
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""TOTP(RFC 6238)自实现测试
|
||||
|
||||
官方向量锚定正确性(RFC 6238 附录 B:SHA1、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 剩 1s;T=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)
|
||||
Reference in New Issue
Block a user