Skip to content

fix(encryption): rebuild unusable user key - #3627

Merged
Lokins577 merged 2 commits into
PCL-Community:devfrom
whitecat346:fix/3581
Sep 30, 2026
Merged

Lokins577 merged 2 commits into
PCL-Community:devfrom
whitecat346:fix/3581

Conversation

@whitecat346

@whitecat346 whitecat346 commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

本 PR 使用 DS V41 Flash 分析问题;部分代码使用 DS V41 Flash
Close #3581 (与 #2687 根因相同)。

  • EncryptHelper 不再缓存失败的密钥加载。无法使用的 UserKey.bin 会连同其 HRESULT 一起记录日志,保留为 UserKey.bin.broken-<timestamp>,并由新的 v2 密钥替换,因此启动器会自行恢复,而不再依赖“删除 UserKey.bin”的变通方法。
  • 旧版 v1(CAPI DPAPI)密钥在解密成功后迁移到 v2(CNG DPAPI)——这正是 refactor(crypto): 换用 CNG DPAPI #2962 原本的意图,但从未应用于现有文件。
  • 新 blob 在写入前会验证能否往返,且瞬时 I/O 错误绝不会轮换密钥。

已验证:dotnet test PCL.Core.Test --filter EncryptHelperKeyTest,以及手动复现(损坏 %APPDATA%\PCLCE\UserKey.bin。不再出现“写入档案列表失败”,配置文件在重启后仍保留)。

注意:使用丢失密钥加密的数据无法恢复,因此这些用户需要重新登录一次。

Sourcery 摘要

在确保数据安全的同时,使用户密钥加载能够从暂时性文件错误中自动恢复。

错误修复:

  • 通过隔离损坏的密钥并生成替代密钥,从不可用的用户密钥文件中恢复,避免反复重用加载失败的密钥。
  • 防止暂时性的密钥文件 I/O 错误触发密钥轮换,并保留现有密钥以便重试。
  • 将成功加载的旧版 v1 用户密钥迁移到 v2 CNG DPAPI 格式。

增强功能:

  • 在持久化新的加密密钥 blob 之前,通过一次保护/解除保护往返操作验证其有效性,并改进密钥加载诊断信息。

测试:

  • 增加对 v1 密钥迁移、损坏或不受支持的密钥恢复,以及文件锁定行为的覆盖测试。
Original summary in English

Sourcery 摘要

在防止瞬时存储错误导致数据丢失的同时,使用户密钥加载具备恢复能力。

错误修复:

  • 通过隔离损坏的文件并生成替代密钥,从无法使用的用户密钥文件中恢复。
  • 在发生瞬时密钥文件 I/O 故障时保留现有密钥并重试,而不是轮换密钥。
  • 将成功加载的旧版 v1 用户密钥迁移到 v2 CNG DPAPI 格式。

增强功能:

  • 在持久化新受保护的密钥数据之前,通过一次保护/解除保护往返操作对其进行验证,并改进密钥加载诊断信息。

测试:

  • 增加对旧版密钥迁移、无法使用的密钥恢复以及文件锁定行为的覆盖。
Original summary in English

Summary by Sourcery

Make user-key loading recoverable while preventing data loss from transient storage errors.

Bug Fixes:

  • Recover from unusable user key files by quarantining the broken file and generating a replacement key.
  • Preserve existing keys and retry after transient key-file I/O failures instead of rotating them.
  • Migrate successfully loaded legacy v1 user keys to the v2 CNG DPAPI format.

Enhancements:

  • Validate newly protected key data with a protect/unprotect round trip before persisting it and improve key-loading diagnostics.

Tests:

  • Add coverage for legacy key migration, unusable-key recovery, and locked-file behavior.

A DPAPI-protected UserKey.bin can become permanently undecryptable, e.g. after
an OS reinstall that keeps user data, a profile migration, a password reset or a
Microsoft account switch. CryptUnprotectData then fails with NTE_BAD_KEY_STATE
("Key not valid for use in specified state"), and since the failure was rethrown
from Lazy<byte[]> it got cached for the whole process: every encrypt/decrypt
failed and profiles could never be saved (PCL-Community#3581).

- Cache the user key only on success, so a failure is retried on the next access
  instead of being remembered forever.
- Rebuild an unusable key file: log the HRESULT, keep the broken blob as
  UserKey.bin.broken-<timestamp>, then generate and store a fresh v2 key.
- Migrate v1 (CAPI DPAPI) keys to v2 (CNG DPAPI) on successful decrypt, so
  existing users converge instead of staying on the legacy path forever.
- Verify the blob round-trips before writing it, so a key that cannot be read
  back is never persisted (would otherwise re-generate a key on every launch).
- Leave transient I/O failures (IOException, UnauthorizedAccessException)
  untouched: they are retried rather than treated as a broken key.

Data protected by the lost key is unrecoverable, so affected users must log in
again once; the launcher itself now recovers without manually deleting the file.

Adds EncryptHelperKeyTest covering migration, rebuild and the no-rotate-on-I/O
rule.
@whitecat346 whitecat346 self-assigned this Sep 25, 2026
@pcl-ce-automation pcl-ce-automation Bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 size: L PR 大小评估:大型 labels Sep 25, 2026
@sourcery-ai

sourcery-ai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

审查者指南

EncryptHelper 现在能够安全地从不可用的 UserKey.bin 文件中恢复:将其隔离,并替换为经过验证的 v2 CNG 保护密钥;同时迁移有效的 v1 密钥、重试失败的加载操作,并在暂时性 I/O 错误期间保留密钥;相关测试已覆盖这些场景。

UserKey 恢复与迁移时序图

sequenceDiagram
    participant Caller
    participant EncryptHelper
    participant UserKeyFile
    participant CNGDPAPI
    participant Log

    Caller->>EncryptHelper: EncryptionKey
    alt existing v2 key
        EncryptHelper->>UserKeyFile: Read key
        EncryptHelper->>CNGDPAPI: Unprotect
        CNGDPAPI-->>EncryptHelper: key
    else existing v1 key
        EncryptHelper->>UserKeyFile: Read key
        EncryptHelper->>CNGDPAPI: ProtectedData.Unprotect
        CNGDPAPI-->>EncryptHelper: legacyKey
        EncryptHelper->>CNGDPAPI: CngProtectedData.Protect
        EncryptHelper->>CNGDPAPI: CngProtectedData.Unprotect
        EncryptHelper->>UserKeyFile: _WriteKeyFile version 2
        EncryptHelper-->>Caller: legacyKey
    else unusable key file
        EncryptHelper->>Log: LogWrapper.Error
        EncryptHelper->>CNGDPAPI: _TryProtect
        CNGDPAPI->>CNGDPAPI: round-trip validation
        EncryptHelper->>UserKeyFile: _QuarantineBrokenKeyFile
        EncryptHelper->>UserKeyFile: _WriteKeyFile version 2
        EncryptHelper->>Log: LogWrapper.Warn
        EncryptHelper-->>Caller: new key
    else transient I/O failure
        EncryptHelper-->>Caller: IOException or UnauthorizedAccessException
        Caller->>EncryptHelper: EncryptionKey retry
    end
Loading

已验证用户密钥创建流程图

flowchart TD
    A[Create random 32-byte key] --> B[_TryProtect with CNG DPAPI]
    B --> C{Round-trip succeeds?}
    C -- No --> D[Fail without replacing UserKey.bin]
    C -- Yes --> E[_QuarantineBrokenKeyFile]
    E --> F[_WriteKeyFile version 2]
    F --> G[Return key and preserve it for future startup]
Loading

文件级变更

变更 详细信息 文件
重新设计用户密钥加载逻辑,使其能够从不可用的密钥文件中恢复,同时不会缓存失败结果。
  • 仅缓存成功加载的密钥,并在失败后重试。
  • 将加密或格式错误归类为不可用,同时继续向上传播暂时性 I/O 和访问错误。
  • 记录 HRESULT,为损坏的文件添加带时间戳的隔离副本,并生成替代的 v2 密钥。
PCL.Core/Utils/Secret/EncryptHelper.cs
为用户加密密钥添加迁移和持久化保护机制。
  • 将可解密的 v1 CAPI DPAPI 密钥迁移为 v2 CNG DPAPI。
  • 在写入新保护的密钥数据前进行往返测试。
  • 使用带刷新和替换操作的临时文件写入;如果持久化失败,则避免轮换密钥。
PCL.Core/Utils/Secret/EncryptHelper.cs
添加隔离测试,覆盖密钥迁移、恢复和暂时性文件锁定。
  • 验证 v1 密钥能够迁移,并在缓存重置后仍可读取。
  • 验证格式错误或不受支持的密钥文件会被隔离并重新生成。
  • 验证被锁定的密钥文件会引发 I/O 错误,但不会轮换密钥。
PCL.Core.Test/Utils/Secret/EncryptHelperKeyTest.cs

针对相关 issue 的评估

Issue 目标 已解决 说明
#3581 修复正版登录后因 UserKey.bin 无法解密或损坏而导致档案无法写入的问题,并使启动器能够自动恢复。 ✅
#3581 在确认用户密钥确实不可用时记录错误、保留损坏的密钥文件,并生成经过往返验证的新 v2 密钥;同时避免因暂时性 I/O 错误而错误轮换密钥。 ✅
#3581 支持旧版 v1 CAPI DPAPI 用户密钥的正常读取,并在解密成功后迁移为 v2 CNG DPAPI 格式,以保持原有加密数据可用。 ✅

可能相关的 issue


提示和命令

与 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

EncryptHelper now safely recovers from unusable UserKey.bin files by quarantining and replacing them with validated v2 CNG-protected keys, migrates valid v1 keys, retries failed loads, and preserves keys across transient I/O errors; focused tests cover these scenarios.

Sequence diagram for UserKey recovery and migration

sequenceDiagram
    participant Caller
    participant EncryptHelper
    participant UserKeyFile
    participant CNGDPAPI
    participant Log

    Caller->>EncryptHelper: EncryptionKey
    alt existing v2 key
        EncryptHelper->>UserKeyFile: Read key
        EncryptHelper->>CNGDPAPI: Unprotect
        CNGDPAPI-->>EncryptHelper: key
    else existing v1 key
        EncryptHelper->>UserKeyFile: Read key
        EncryptHelper->>CNGDPAPI: ProtectedData.Unprotect
        CNGDPAPI-->>EncryptHelper: legacyKey
        EncryptHelper->>CNGDPAPI: CngProtectedData.Protect
        EncryptHelper->>CNGDPAPI: CngProtectedData.Unprotect
        EncryptHelper->>UserKeyFile: _WriteKeyFile version 2
        EncryptHelper-->>Caller: legacyKey
    else unusable key file
        EncryptHelper->>Log: LogWrapper.Error
        EncryptHelper->>CNGDPAPI: _TryProtect
        CNGDPAPI->>CNGDPAPI: round-trip validation
        EncryptHelper->>UserKeyFile: _QuarantineBrokenKeyFile
        EncryptHelper->>UserKeyFile: _WriteKeyFile version 2
        EncryptHelper->>Log: LogWrapper.Warn
        EncryptHelper-->>Caller: new key
    else transient I/O failure
        EncryptHelper-->>Caller: IOException or UnauthorizedAccessException
        Caller->>EncryptHelper: EncryptionKey retry
    end
Loading

Flow diagram for validated user key creation

flowchart TD
    A[Create random 32-byte key] --> B[_TryProtect with CNG DPAPI]
    B --> C{Round-trip succeeds?}
    C -- No --> D[Fail without replacing UserKey.bin]
    C -- Yes --> E[_QuarantineBrokenKeyFile]
    E --> F[_WriteKeyFile version 2]
    F --> G[Return key and preserve it for future startup]
Loading

File-Level Changes

Change Details Files
Reworked user-key loading to recover from unusable key files without caching failures.
  • Cache only successfully loaded keys and retry after failures.
  • Classify cryptographic/format errors as unusable while propagating transient I/O and access errors.
  • Log the HRESULT, quarantine the broken file with a timestamp, and generate a replacement v2 key.
PCL.Core/Utils/Secret/EncryptHelper.cs
Added migration and persistence safeguards for user encryption keys.
  • Migrate decryptable v1 CAPI DPAPI keys to v2 CNG DPAPI.
  • Round-trip-test newly protected key blobs before writing them.
  • Use temporary-file writes with flush and replacement, while avoiding rotation when persistence fails.
PCL.Core/Utils/Secret/EncryptHelper.cs
Added isolated tests covering key migration, recovery, and transient file locking.
  • Verify v1 keys migrate and remain readable after cache reset.
  • Verify malformed or unsupported key files are quarantined and rebuilt.
  • Verify a locked key file raises the I/O error without rotating the key.
PCL.Core.Test/Utils/Secret/EncryptHelperKeyTest.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#3581 修复正版登录后因 UserKey.bin 无法解密或损坏而导致档案无法写入的问题,并使启动器能够自动恢复。 ✅
#3581 在确认用户密钥确实不可用时记录错误、保留损坏的密钥文件,并生成经过往返验证的新 v2 密钥;同时避免因暂时性 I/O 错误而错误轮换密钥。 ✅
#3581 支持旧版 v1 CAPI DPAPI 用户密钥的正常读取,并在解密成功后迁移为 v2 CNG DPAPI 格式,以保持原有加密数据可用。 ✅

Possibly linked issues


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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

您好——我发现了 1 个问题

面向 AI 代理的提示
请处理本次代码审查中的评论:

## 单独评论

### 评论 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="216-220" />
<code_context>
+        if (!_TryProtect(key, out var storeData))
+            throw new InvalidOperationException("本机 DPAPI 不可用,无法创建用户密钥");
+        _QuarantineBrokenKeyFile(keyFile); // 已确认能重建,才动原文件
+        if (!_WriteKeyFile(keyFile, storeData))
+            LogWrapper.Error("Encryption", $"用户密钥写入失败,本次运行仅使用内存密钥:{keyFile}");
+        else
+            LogWrapper.Warn("Encryption", "用户密钥已重建(version 2)。旧密钥保护的数据无法恢复,需要重新登录。");
+        return key;
+    }
+
</code_context>
<issue_to_address>
**问题(bug_risk):** 当不可用的密钥被隔离,但 `_WriteKeyFile` 失败时,`_CreateKey` 会返回新生成的内存密钥,而 `EncryptionKey` 会将其缓存。在该次运行期间使用此密钥加密的任何机密信息,在重启后都无法解密,因为对应的密钥从未被持久化保存。

**触发条件:** 重建损坏的密钥文件时,遇到临时性或权限相关的写入失败。

**建议修复:** 不要返回并缓存内存密钥,而应向上传播写入失败;或者保留旧文件,并在替换密钥持久化存储成功之前禁止加密。

```suggestion
        if (!_WriteKeyFile(keyFile, storeData))
            throw new IOException($"用户密钥写入失败:{keyFile}");
        LogWrapper.Warn("Encryption", "用户密钥已重建(version 2)。旧密钥保护的数据无法恢复,需要重新登录。");
        return key;
```
</issue_to_address>

Sourcery 对开源项目免费——如果您喜欢我们的审查结果,请考虑分享它们 ✨
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="PCL.Core/Utils/Secret/EncryptHelper.cs" line_range="216-220" />
<code_context>
+        if (!_TryProtect(key, out var storeData))
+            throw new InvalidOperationException("本机 DPAPI 不可用,无法创建用户密钥");
+        _QuarantineBrokenKeyFile(keyFile); // 已确认能重建,才动原文件
+        if (!_WriteKeyFile(keyFile, storeData))
+            LogWrapper.Error("Encryption", $"用户密钥写入失败,本次运行仅使用内存密钥:{keyFile}");
+        else
+            LogWrapper.Warn("Encryption", "用户密钥已重建(version 2)。旧密钥保护的数据无法恢复,需要重新登录。");
+        return key;
+    }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** When an unusable key is quarantined but `_WriteKeyFile` fails, `_CreateKey` returns the newly generated in-memory key and `EncryptionKey` caches it. Any secrets encrypted during that run cannot be decrypted after restart because the corresponding key was never persisted.

**Triggers:** When rebuilding a corrupted key file encounters a transient or permission-related write failure.

**Suggested fix:** Propagate the write failure instead of returning and caching an in-memory key, or retain the old file and prevent encryption until the replacement is durably stored.

```suggestion
        if (!_WriteKeyFile(keyFile, storeData))
            throw new IOException($"用户密钥写入失败:{keyFile}");
        LogWrapper.Warn("Encryption", "用户密钥已重建(version 2)。旧密钥保护的数据无法恢复,需要重新登录。");
        return key;
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread PCL.Core/Utils/Secret/EncryptHelper.cs
Comment thread PCL.Core/Utils/Secret/EncryptHelper.cs Outdated
Co-authored-by: NoClassDefFoundError <rnmddos@163.com>
@Chiloven945
Chiloven945 requested a review from a team September 30, 2026 12:42
@pcl-ce-automation pcl-ce-automation Bot added 🕑 等待合并 已处理完毕,正在等待代码合并入主分支 and removed 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 labels Sep 30, 2026
@Lokins577
Lokins577 merged commit 86c96a4 into PCL-Community:dev Sep 30, 2026
3 checks passed
@pcl-ce-automation pcl-ce-automation Bot added 👌 完成 相关问题已修复或功能已实现,计划在下次版本更新时正式上线 and removed 🕑 等待合并 已处理完毕,正在等待代码合并入主分支 labels Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: L PR 大小评估:大型 👌 完成 相关问题已修复或功能已实现,计划在下次版本更新时正式上线

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C#]: 正版登录无法写入档案

3 participants