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
+7 -2
View File
@@ -206,7 +206,8 @@ class Account(SQLModel, table=True):
资产的 Asset.account_id 字段外键引用本表,重命名账号不影响资产归属。
唯一性:(platform, name) 联合唯一——同一邮箱/用户名可跨平台复用。
凭证层:登录用户名/密码(网站登录)+ API 配置 JSON(SDK 同步用),
凭证层:登录凭据(用户名/密码/2FA)统一存 credentials 表(唯一事实源),
本表通过 credential_id 关联;API 配置 JSON(SDK 同步用)仍留在本表。
平台本身不再持有凭证(Provider.api_config_encrypted 已弃用,仅留历史值)。
"""
@@ -222,7 +223,11 @@ class Account(SQLModel, table=True):
remark: Optional[str] = Field(default=None, description="备注")
login_user: Optional[str] = Field(default=None, description="登录用户名/邮箱")
login_password_encrypted: Optional[str] = Field(
default=None, description="加密的登录密码(Fernet"
default=None, description="(已弃用)登录密码已迁至 credentials 表,仅留历史值"
)
credential_id: Optional[int] = Field(
default=None, foreign_key="credentials.id", index=True,
description="关联登录凭据 ID(登录密码唯一事实源在 credentials 表)"
)
api_config_encrypted: Optional[str] = Field(
default=None, description="加密的 API 配置 JSONaccess_key/secret/api_key 等,供 SDK 同步)"
+66
View File
@@ -0,0 +1,66 @@
"""登录凭据模型(密码库)
Credential 是登录凭据的唯一事实源,粒度 = 站点 × 登录方式:
- 同一邮箱注册多个站点 = 多条记录(username 重复是常态而非冗余)
- 密码相同也各存一份(改密逐站发生,不做共享联动)
- 授权登录(OAuth)是正常条目:password 为空 + login_type=oauth
- 2FATOTP secret)挂在凭据上,是登录凭据的一部分
平台账号(Account)通过 credential_id 关联本表,账号密码读写全部重定向到此;
普通网站/邮箱等游离凭据直接建在本表,与平台/资产体系解耦。
敏感字段(password / otp secret)用 Fernet 加密存储(MASTER_KEY 不入库),
Read schema 永不返回明文/密文,只给 has_password / has_otp 布尔标记。
"""
from datetime import datetime
from enum import Enum
from typing import Optional
from sqlmodel import Field, SQLModel
from app.core.timeutils import utcnow
class LoginType(str, Enum):
"""登录方式"""
PASSWORD = "password" # 用户名 + 密码
OAUTH = "oauth" # 授权登录(Google/Apple/GitHub 等,本站无密码)
OTHER = "other" # 其他(魔法链接、硬件 key 等)
class Credential(SQLModel, table=True):
"""登录凭据(密码库条目)
不设 (site, username) 库级唯一约束:SQLite 对 NULL 不友好,且同站多账号合法;
重复录入由服务层提示(允许继续)。
"""
__tablename__ = "credentials"
id: Optional[int] = Field(default=None, primary_key=True)
site: str = Field(index=True, description="站点/服务名,如 GitHub、阿里云")
username: Optional[str] = Field(
default=None, index=True, description="登录用户名/邮箱"
)
login_type: LoginType = Field(
default=LoginType.PASSWORD, index=True, description="登录方式"
)
oauth_provider: Optional[str] = Field(
default=None, description="授权登录来源:google/apple/github/wechat 等"
)
password_encrypted: Optional[str] = Field(
default=None, description="加密的登录密码(Fernet);oauth 登录为空"
)
otp_secret_encrypted: Optional[str] = Field(
default=None, description="加密的 TOTP base32 secret2FA),动态码由服务端生成"
)
url: Optional[str] = Field(default=None, description="登录页地址")
note: Optional[str] = Field(default=None, description="备注")
created_at: datetime = Field(default_factory=utcnow, description="创建时间")
updated_at: datetime = Field(
default_factory=utcnow,
sa_column_kwargs={"onupdate": utcnow},
description="更新时间",
)