"""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)