feat(vault): 凭据库(密码+2FA 动态码)与 MASTER_KEY 密钥托管

credentials 表为登录凭据唯一事实源(站点×登录方式,含 oauth/2FA);账号密码读写重定向凭据层并幂等迁移历史数据;TOTP 按 RFC6238 零依赖自实现,绑定需当前动态码校验;Key Escrow 防 MASTER_KEY 遗失;前端新增凭据库视图与账号 2FA 联动。64 pytest + 16 浏览器端到端验证通过。
This commit is contained in:
gouki
2026-09-05 15:54:41 +00:00
parent 2fbb658126
commit d711ad5827
26 changed files with 2529 additions and 35 deletions
+55
View File
@@ -0,0 +1,55 @@
"""登录凭据 Schema(密码库)
凭证语义约定(与账号一致):
- password / otp_secretCreate 时传入即加密存储;Update 时 None 表示不修改、'' 表示清除。
- 2FA 绑定必须携带当前 6 位动态码(otp_code)做服务端校验,防 secret 手误成废条目。
- Read 永不返回明文/密文,只给 has_password / has_otp 布尔标记;明文经专用 reveal 接口。
"""
from datetime import datetime
from typing import Optional
from sqlmodel import SQLModel
from app.models.credential import LoginType
class CredentialBase(SQLModel):
site: str
username: Optional[str] = None
login_type: LoginType = LoginType.PASSWORD
oauth_provider: Optional[str] = None
url: Optional[str] = None
note: Optional[str] = None
class CredentialCreate(CredentialBase):
password: Optional[str] = None # 明文传入,加密存储
otp_secret: Optional[str] = None # base32 secret 或 otpauth:// URI
otp_code: Optional[str] = None # 当前 6 位动态码(otp_secret 提供时必填)
class CredentialUpdate(SQLModel):
site: Optional[str] = None
username: Optional[str] = None
login_type: Optional[LoginType] = None
oauth_provider: Optional[str] = None
url: Optional[str] = None
note: Optional[str] = None
password: Optional[str] = None # None=不修改,''=清除,非空=重加密
class CredentialRead(CredentialBase):
id: int
has_password: bool = False
has_otp: bool = False
duplicate: bool = False # 同 (site, username) 已存在其它条目(录入提示用)
account_id: Optional[int] = None # 反向关联的平台账号(展示"来自平台账号")
account_name: Optional[str] = None
created_at: datetime
updated_at: datetime
class OtpBindRequest(SQLModel):
secret: str # base32 secret 或 otpauth:// URI
code: str # 当前 6 位动态码(服务端校验通过才落库)