Skip to content

fix(config): 多 Windows 账户共用一份安装时不再改写存量密文 - #729

Open
qiyinxi wants to merge 1 commit into
AUTO-MAS-Project:devfrom
qiyinxi:fix/dpapi-machine-scope-20260913
Open

qiyinxi wants to merge 1 commit into
AUTO-MAS-Project:devfrom
qiyinxi:fix/dpapi-machine-scope-20260913

Conversation

@qiyinxi

@qiyinxi qiyinxi commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

同一台电脑上有多个 Windows 账户轮流启动同一份安装时,换账户后已保存的账号密码会变成一串密文文本,每次都要重新填写。

  1. 根因:DPAPI 默认把密钥绑定到当前 Windows 账户,而一份安装只有一份 config/ConfigItem.setValue 用「能否解密」来区分明文和密文,于是换账户启动后,本账户解不开的密文被当成明文又加密了一层,再顺着 load() 的脏标记写回配置文件,原密码不可恢复。
  2. 加密改用机器作用域CRYPTPROTECT_LOCAL_MACHINE),同一台机器的各账户都能解开。解密侧不动:作用域写在密文里,CryptUnprotectData 与 .NET ProtectedData 都会忽略调用方传入的作用域。
  3. 改按 DPAPI blob 结构区分存量密文与用户新填的明文,不再二次加密;解不开的密文落盘原样保留、读取时给占位提示,setValue 的相等性判断也不再因解密失败抛错,用户可以直接覆盖重填。
  4. 存量密文不受影响:旧的用户作用域密文继续正常解密,也不会被自动重写。密文格式受 SRA 约束(app/task/HSR/tools/sra_runtime.py 把 MAS 的密文原样写进 SRA 的 startGame 配置、由 SRA 自己解),不能加版本前缀或 entropy 来做自动迁移,因此跨账户共享需要用户重新填写一次。

本地验证

Windows 11 / Python 3.12 / 本分支 worktree 的 .venv

回归测试

  • python -m pytest tests:1053 passed, 4 skipped
  • python -m pytest tests --collect-only -q:退出码 0
  • ruff checkruff format --diff:本 PR 改动的文件全绿(仓库基线另有 9 处既有告警,均在本 PR 未触及的文件里)
  • 新增 20 条用例,覆盖存量密文加载不被改写、解不开的密文不被二次加密、落盘口径原样返回、读取口径给占位提示、可覆盖重填、加密确实带机器作用域标志

全字段端到端

反射取出全部 25 个声明 EncryptValidator 的字段(各专项 Info.PasswordHSR Info.IdDirect.SRAConfig/M7AConfig、三处 MirrorChyanCDK、7 个签到 token、3 个通知密钥等),每个字段走真实的 connect()(读文件 → load() → 脏则写回)后检查落盘内容,对比修复前后:

场景 修复前 修复后
升级,同 Windows 账户 25/25 25/25
换 Windows 账户 0/25 25/25
机器作用域密文重载 25/25 25/25
空值 25/25 25/25
新安装写入 25/25 25/25
换账户后重填 25/25 25/25

「解不开的密文」用了三种构造:DPAPI 真实生成但读取时不提供对应 entropy(未经任何手工改动)、真实密文抹掉 master key GUID、以及密文被截断,三者走的代码路径一致、结果一致;手工把明文填进配置文件的情况也照常加密后正常读回。

另单独驱动 MultipleConfig(UserData)这一层,覆盖 HSR 用户的账号与密码:修复前落盘被改写、toDict(if_decrypt=False) 取到的也已变质(会一起带进 SRA 配置),修复后全部通过。

兼容性

  • 旧的用户作用域密文在新代码下原样保留并正常解密;新写的机器作用域密文在旧代码下也能读出,回滚安全。
  • 用户作用域 / 机器作用域密文 × .NET CurrentUser / LocalMachine scope 四种组合均解密成功,确认改动对 SRA 侧透明;getValue(if_decrypt=False) 交给 SRA 的密文与配置文件逐字一致。

🤖 Generated with Claude Code

DPAPI 默认把密钥绑定到当前 Windows 账户,而一份安装只有一份 config/。
同一台机器换个账户启动后,load() 把本账户解不开的密文当成明文又加密一层,
再顺着脏标记写回配置文件,账号密码从此变成一串密文文本且无法恢复。

- 加密改用机器作用域,同机各账户都能解开;解密侧无需改动,作用域写在密文
  里,CryptUnprotectData 与 .NET ProtectedData 都会忽略调用方传入的作用域
- 配置项按 DPAPI 结构而非「能否解密」区分存量密文与用户新填的明文
- 解不开的密文落盘原样保留、读取给占位提示,并允许用户直接覆盖重填

存量密文不受影响:旧的用户作用域密文继续正常解密,也不会被自动重写;
需要跨账户共享时由用户重新填写一次。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @qiyinxi, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 20 hours and 57 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 13, 2026

Copy link
Copy Markdown

审查者指南

本 PR 将新生成的 DPAPI 密文切换为机器作用域,并在配置层按 blob 结构区分密文与明文,从而避免跨账户启动时改写存量密文;不可解密的密文原样落盘、读取时显示占位提示且可被用户重新填写,同时保留旧用户作用域密文的解密兼容性。

跨账户加载密钥及防止覆盖的时序图

sequenceDiagram
    participant AccountA as Windows账户A
    participant Config as 配置项
    participant File as 配置文件
    participant AccountB as Windows账户B
    participant DPAPI as DPAPI

    AccountA->>Config: setValue(明文)
    Config->>DPAPI: dpapi_encrypt(明文)
    DPAPI-->>Config: 机器作用域 DPAPI blob
    Config->>File: 保存(blob)

    AccountB->>Config: 加载(blob)
    Config->>Config: looks_like_dpapi_blob(blob)
    Config->>DPAPI: dpapi_decrypt(blob)
    DPAPI-->>Config: 已解密的密钥
    Config-->>AccountB: 密钥值

    AccountB->>Config: setValue(新明文)
    Config->>Config: looks_like_dpapi_blob(新明文)
    Config->>DPAPI: dpapi_encrypt(新明文)
    DPAPI-->>Config: 机器作用域 DPAPI blob
    Config->>File: 保存(新 blob)
Loading

保留不可读 DPAPI 密钥的流程图

flowchart TD
    A[已加载配置值] --> B{looks_like_dpapi_blob}
    B -->|否| C[dpapi_encrypt 明文]
    B -->|是| D{dpapi_decrypt 成功}
    D -->|是| E[使用已解密的密钥]
    D -->|否| F[保持 blob 不变]
    F --> G[返回 UNREADABLE_SECRET_PLACEHOLDER 以供显示]
    G --> H[用户输入替换值]
    H --> C
Loading

文件级变更

变更 详情 文件
改用 DPAPI 机器作用域加密,保持旧密文兼容并确保同机不同 Windows 账户可共享新保存的密文。
  • 为加密调用传入 CRYPTPROTECT_LOCAL_MACHINE 标志。
  • 保持解密路径不变,使用户作用域和机器作用域密文均可读取。
  • 补充机器作用域、旧用户作用域及跨作用域解密回归测试。
app/utils/platform/windows/secret.py
tests/platform/test_secret_machine_scope.py
通过识别 DPAPI blob 结构而非解密结果,防止不可解密的存量密文被二次加密或自动写回。
  • 新增严格的 DPAPI blob 头部识别与公共导出。
  • ConfigItem.setValue 对结构合法的密文原样保存,仅对明文执行加密。
  • 解密失败时保留落盘原值,并以占位文本提供读取结果。
  • 允许用户覆盖无法解密的密文,且相等性判断不再因解密异常失败。
app/models/ConfigBase.py
app/utils/platform/common/secret.py
app/utils/platform/windows/secret.py
app/utils/security.py
app/utils/__init__.py
tests/models/test_config_item_secret.py
增加变更说明,记录多账户共用安装时密文被改写问题的修复。
  • 新增对应 changelog 条目。
changelog.d/fix-dpapi-machine-scope-20260913.fix.md

提示和命令

与 Sourcery 交互

  • 触发新的审查: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 根据审查评论创建 GitHub issue: 回复审查评论,请求 Sourcery 根据该评论创建 issue。你也可以回复审查评论并使用 @sourcery-ai issue,根据该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title,随时(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文中任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary,随时(重新)生成摘要。
  • 生成审查者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理了所有评论且不想再看到它们,这会很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。如果你想从新的审查开始,这尤其有用——别忘了评论 @sourcery-ai review 以触发新的审查!

自定义使用体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

本 PR 将新生成的 DPAPI 密文切换为机器作用域,并在配置层按 blob 结构区分密文与明文,从而避免跨账户启动时改写存量密文;不可解密的密文原样落盘、读取时显示占位提示且可被用户重新填写,同时保留旧用户作用域密文的解密兼容性。

Sequence diagram for cross-account secret loading and overwrite protection

sequenceDiagram
    participant AccountA as WindowsAccountA
    participant Config as ConfigItem
    participant File as ConfigFile
    participant AccountB as WindowsAccountB
    participant DPAPI as DPAPI

    AccountA->>Config: setValue(plaintext)
    Config->>DPAPI: dpapi_encrypt(plaintext)
    DPAPI-->>Config: machine-scope DPAPI blob
    Config->>File: save(blob)

    AccountB->>Config: load(blob)
    Config->>Config: looks_like_dpapi_blob(blob)
    Config->>DPAPI: dpapi_decrypt(blob)
    DPAPI-->>Config: decrypted secret
    Config-->>AccountB: secret value

    AccountB->>Config: setValue(new plaintext)
    Config->>Config: looks_like_dpapi_blob(new plaintext)
    Config->>DPAPI: dpapi_encrypt(new plaintext)
    DPAPI-->>Config: machine-scope DPAPI blob
    Config->>File: save(new blob)
Loading

Flow diagram for preserving unreadable DPAPI secrets

flowchart TD
    A[Config value loaded] --> B{looks_like_dpapi_blob}
    B -->|No| C[dpapi_encrypt plaintext]
    B -->|Yes| D{dpapi_decrypt succeeds}
    D -->|Yes| E[Use decrypted secret]
    D -->|No| F[Keep blob unchanged]
    F --> G[Return UNREADABLE_SECRET_PLACEHOLDER for display]
    G --> H[User enters replacement]
    H --> C
Loading

File-Level Changes

Change Details Files
改用 DPAPI 机器作用域加密,保持旧密文兼容并确保同机不同 Windows 账户可共享新保存的密文。
  • 为加密调用传入 CRYPTPROTECT_LOCAL_MACHINE 标志。
  • 保持解密路径不变,使用户作用域和机器作用域密文均可读取。
  • 补充机器作用域、旧用户作用域及跨作用域解密回归测试。
app/utils/platform/windows/secret.py
tests/platform/test_secret_machine_scope.py
通过识别 DPAPI blob 结构而非解密结果,防止不可解密的存量密文被二次加密或自动写回。
  • 新增严格的 DPAPI blob 头部识别与公共导出。
  • ConfigItem.setValue 对结构合法的密文原样保存,仅对明文执行加密。
  • 解密失败时保留落盘原值,并以占位文本提供读取结果。
  • 允许用户覆盖无法解密的密文,且相等性判断不再因解密异常失败。
app/models/ConfigBase.py
app/utils/platform/common/secret.py
app/utils/platform/windows/secret.py
app/utils/security.py
app/utils/__init__.py
tests/models/test_config_item_secret.py
增加变更说明,记录多账户共用安装时密文被改写问题的修复。
  • 新增对应 changelog 条目。
changelog.d/fix-dpapi-machine-scope-20260913.fix.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant