Skip to content

对齐凭据存储策略与威胁模型#277

Merged
appergb merged 13 commits into
Open-Less:mainfrom
H-Chris233:fix/issue-230-credential-storage-model
May 6, 2026
Merged

对齐凭据存储策略与威胁模型#277
appergb merged 13 commits into
Open-Less:mainfrom
H-Chris233:fix/issue-230-credential-storage-model

Conversation

@H-Chris233
Copy link
Copy Markdown
Collaborator

@H-Chris233 H-Chris233 commented May 6, 2026

User description

变更

  • 将供应商凭据从本地明文 credentials.json 迁移到系统凭据库:macOS Keychain、Windows Credential Manager、Linux keyring。
  • 旧版明文 credentials.json 仅作为升级迁移/兼容来源:成功写入系统凭据库后删除;如果 vault 暂不可用或迁移写入失败,则保留旧文件并读取其内容,避免升级用户凭据变空。
  • 兼容迁移旧版按账户名保存的 vault 凭据(如 ark.api_keyvolcengine.app_key),迁移成功后清理旧 per-account entries。
  • 迁移优先级为已有 chunked manifest → 旧版明文 credentials.json → 更旧的 per-account vault entries,避免 stale vault entries 覆盖上一版 Tauri 的当前配置。
  • 读路径在 vault 暂不可读时仍可回退显示旧 JSON;写路径会传播 vault 读取错误,避免用空/旧 fallback 覆写损坏或暂不可读的 vault payload。
  • 旧 plaintext credentials.json 清理失败会在写入/迁移路径上返回错误;已有 vault manifest 的普通读取路径会继续尝试 best-effort 清理并记录警告。
  • 凭据库内保留原有多 provider / active provider 逻辑模型;为避开 Windows Credential Manager 单条 credential blob 限制,实际写入采用 manifest + chunk 条目。
  • chunk 保存使用 generation:先写新 generation 的全部 chunks,再切换 manifest,manifest 写成功后才清理旧 generation,降低半写入导致旧凭据不可读的风险。
  • read_credential / set_credential 增加窗口 label gate,仅允许主窗口访问 raw credential IPC,避免 QA / capsule 等辅助窗口默认暴露。
  • 更新 README、AGENTS/CLAUDE、Settings 提示和 i18n 文案,使 UI / 文档 / 实现与系统凭据库模型一致。

验证

  • cargo check --manifest-path src-tauri/Cargo.toml(通过,保留既有 unused warnings)
  • cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit
  • npm run -s build(通过,保留既有 Vite dynamic/static import warnings)
  • git diff --check

未验证

  • 未在真实 macOS Keychain / Windows Credential Manager / Linux keyring 环境中做手工写入验证。
  • 未手工模拟系统 vault 后端不可用或 chunk 写入中途崩溃的升级迁移流程。

Closes #230


PR Type

Bug fix, Enhancement, Documentation


Description

  • Migrate credentials to the OS vault

    • Legacy JSON becomes migration-only source
    • Removes plaintext secrets after write
  • Restrict raw secret access to main window

  • Chunk vault payloads for Windows limits

  • Update docs, settings copy, and smoke scripts


Diagram Walkthrough

flowchart LR
  A["Legacy JSON / old keyring entries"] -->|"migrate"| B["System credential vault"]
  B -->|"chunked JSON payload"| C["Manifest + chunks"]
  D["Main settings window"] -->|"read/write raw secret"| B
  B -->|"surface policy"| E["Docs, i18n, settings notice"]
Loading

File Walkthrough

Relevant files
Bug fix
5 files
commands.rs
Gate credential IPC to main window                                             
+14/-4   
persistence.rs
Migrate credentials into chunked vault storage                     
+339/-61
windows-real-asr-insertion-smoke.ps1
Update smoke warning for vault-backed credentials               
+1/-1     
windows-runtime-smoke.ps1
Remove outdated credential-required failure                           
+1/-1     
windows-smoke-suite.ps1
Stop forcing credentials in smoke suite                                   
+0/-1     
Enhancement
6 files
en.ts
Add credential storage notice text                                             
+1/-0     
ja.ts
Add credential storage notice text                                             
+1/-0     
ko.ts
Add credential storage notice text                                             
+1/-0     
zh-CN.ts
Add credential storage notice text                                             
+1/-0     
zh-TW.ts
Add credential storage notice text                                             
+1/-0     
Settings.tsx
Surface credential storage notice in settings                       
+3/-0     
Documentation
4 files
AGENTS.md
Document platform vault credential storage                             
+3/-3     
CLAUDE.md
Document platform vault credential storage                             
+3/-3     
README.md
Explain vault-backed credential migration                               
+8/-3     
README.zh.md
Explain vault-backed credential migration                               
+5/-4     
Dependencies
1 files
Cargo.toml
Add platform keyring dependencies                                               
+15/-0   

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

PR Reviewer Guide 🔍

(Review updated until commit 0505c2f)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

230 - Partially compliant

Compliant requirements:

  • Credential storage is moved to the OS credential vault.
  • Legacy JSON is treated as migration-only and new writes avoid plaintext secrets.
  • read_credential and set_credential are restricted to the main window.
  • README, AGENTS/CLAUDE, settings text, and i18n strings are updated to the vault model.

Non-compliant requirements:

  • Legacy plaintext cleanup is not guaranteed on the normal save path because delete failures are only logged.

Requires further human verification:

  • End-to-end migration behavior on supported macOS, Windows, and Linux setups.
  • Confirmation that the intended caller for raw credential IPC remains the main window in the actual UI flow.
⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 Security concerns

Sensitive information persistence:
the migration path can leave the legacy plaintext credentials.json behind if deletion fails, so secrets may remain on disk after a purportedly successful vault migration.

⚡ Recommended focus areas for review

Partial Write Corruption

Chunk data is rewritten in place before the manifest is updated. If a later chunk write fails, the old manifest still points to the same chunk names, but some of those chunks now contain the new payload. The next read can then fail to decode the mixed data and make the stored credentials unreadable.

for (index, chunk) in chunks.iter().enumerate() {
    let account = chunk_account(None, index);
    keyring_entry_for(&account)?
        .set_password(chunk)
        .with_context(|| format!("write system credential vault chunk {index}"))?;
Cleanup Gap

The migration cleanup is best-effort only, so a locked or read-only legacy credentials.json can survive a successful vault write. That leaves plaintext secrets on disk even though the code reports success, which defeats the promised one-time migration.

remove_legacy_credentials_file_best_effort();
Ok(())

@H-Chris233 H-Chris233 force-pushed the fix/issue-230-credential-storage-model branch from e7beea5 to 6296e3d Compare May 6, 2026 00:42
@H-Chris233 H-Chris233 changed the title 对齐凭据存储策略与威胁模型 将凭据存储迁移到系统凭据库 May 6, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6296e3d0ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +338 to +340
let json = serde_json::to_string(&cleaned).context("encode credentials failed")?;
keyring_entry()?
.set_password(&json)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid storing the full provider JSON in one Windows credential

On Windows this set_password call writes the entire multi-provider JSON into a single Windows Credential Manager blob; that backend is limited to 2560 bytes per credential blob. Once a user has configured several LLM/ASR presets, long API keys, or extra headers, the serialized root can exceed that limit and every subsequent credential save/migration fails with a vault write error, leaving the settings page unable to persist credentials. Splitting accounts into separate keyring entries or using a Windows-safe chunking strategy avoids this Windows-only regression.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已处理:保留逻辑上的 provider JSON payload,但实际写入系统凭据库时改为 manifest + bounded chunks,避免 Windows Credential Manager 单条 credential blob 限制。这里只兼容本 PR 之前的明文 credentials.json 迁移,不兼容本 PR 过程中未发布过的单条 keyring JSON 临时格式。验证:cargo check、目标单测、npm build、git diff --check。

@github-actions github-actions Bot changed the title 将凭据存储迁移到系统凭据库 对齐凭据存储策略与威胁模型 May 6, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 6296e3d

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 0a1bdf2

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a83a8d2f94

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +371 to 374
fn load_keyring_credentials() -> Result<Option<CredsRoot>> {
let Some(json_or_manifest) = get_keyring_password(KEYRING_CREDENTIALS_ACCOUNT)? else {
return Ok(None);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Migrate existing per-account vault credentials

When upgrading an install that already has Keychain/credential-vault entries under the documented CredentialAccount names (for example ark.api_key) but no new credentials.v1 manifest yet, this returns Ok(None) and load_credentials falls through to the legacy JSON/default path. That silently ignores the existing vault credentials even though CredentialAccount::keyring_account still advertises those accounts as upgrade-readable, so affected users have to re-enter provider settings. Please add a fallback that reads the old per-account vault entries and writes the new chunked manifest.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已处理:没有 credentials.v1 manifest 时会先读取旧 per-account vault entries(如 ark.api_key / volcengine.app_key / asr.model),组装为当前 CredsRoot,成功写入新的 chunked manifest 后清理旧 entries。另将 chunk 写入改为 generation 方案,先写新 generation chunks,再切换 manifest,降低 partial write 混合旧/新 chunk 的风险。验证:cargo check、目标单测、npm build、git diff --check。

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit a83a8d2

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 508eaf40a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +427 to +434
let legacy_vault = load_legacy_keyring_credentials();
if !legacy_vault.providers.asr.is_empty() || !legacy_vault.providers.llm.is_empty() {
if let Err(e) = save_credentials(&legacy_vault) {
log::warn!("[vault] legacy vault credential migration failed: {e}");
} else {
remove_legacy_keyring_credentials();
}
return legacy_vault;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prefer the current JSON credentials over stale vault entries

When an existing Tauri user has the current %APPDATA%/OpenLess/credentials.json or ~/.openless/credentials.json plus leftover per-account Keychain entries from an older Swift install, this branch migrates and returns the old vault values before ever reading the plaintext JSON that the previous release actually used. That can replace the user's current multi-provider settings with stale Swift-era credentials and make ASR/LLM appear unconfigured or use old keys; this is fresh evidence after the earlier migration comment because the new fallback now exists, but it runs before the legacy JSON source.

Useful? React with 👍 / 👎.

Comment on lines +421 to +423
Err(e) => {
log::warn!("[vault] read {} failed: {}", path.display(), e);
return CredsRoot::default();
log::warn!("[vault] system credential read failed: {e}");
return load_legacy_credentials().unwrap_or_default();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid overwriting the vault after a failed vault read

If a chunked vault payload exists but one chunk is missing or temporarily unreadable, this error path returns the legacy JSON/default root; the mutating callers (set, remove, set_active_*) then save that partial/default root as a new manifest, which can permanently discard the rest of the user's credentials. In this scenario the read failure should be propagated to write callers rather than treated as an empty credential set.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 508eaf4

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 3b15c21

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76c1ef63f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread openless-all/app/src-tauri/Cargo.toml Outdated
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies.keyring]
version = "3.6.3"
default-features = false
features = ["linux-native"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a persistent Linux credential backend

When this is built for the Linux target, features = ["linux-native"] selects keyring's keyutils-only store; the keyring docs describe that store as in-memory and not persistent across reboots (https://docs.rs/keyring/3.6.3/keyring/keyutils/). Since this PR now removes the legacy JSON file after a successful vault write, Linux users can lose all configured provider credentials after reboot/key expiry with no fallback to reload them; use one of the persistent Secret Service-backed features or keep a Linux reload fallback before deleting plaintext.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 76c1ef6

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1536d5fe0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

match get_keyring_password(legacy_account) {
Ok(Some(value)) => write_account(&mut root, *account, Some(value)),
Ok(None) => {}
Err(e) => log::warn!("[vault] read legacy vault {legacy_account} failed: {e}"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Abort legacy vault migration on entry read errors

When upgrading an install with no chunked manifest and no plaintext JSON fallback, this branch logs a read failure for one old per-account vault entry and keeps migrating any other entries that did read successfully; migrate_legacy_sources_for_update then saves that partial root and calls remove_legacy_keyring_credentials(), which can delete the unread legacy secret. In the scenario where one Keychain/Credential Manager entry is temporarily inaccessible or corrupt but another entry is readable, this silently drops the failed entry instead of preserving the old vault state for a retry.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 1536d5f

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@H-Chris233 H-Chris233 force-pushed the fix/issue-230-credential-storage-model branch from b6f92ed to b5f281d Compare May 6, 2026 06:35
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5f281da35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
}

remove_legacy_credentials_file_best_effort();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update the smoke suite credential source

When a Windows dev/test machine has been migrated, this removes %APPDATA%\OpenLess\credentials.json after the first successful vault save. I checked openless-all/app/scripts/windows-smoke-suite.ps1: it still runs windows-runtime-smoke.ps1 with RequireCredentials = true, and that script only checks credentials.json before launching the app, so the default smoke suite fails even though the credentials now live in Credential Manager. Please update the smoke scripts to query the vault or stop requiring the legacy JSON file.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已处理:\n- windows-smoke-suite 不再默认传入 RequireCredentials,避免迁移后因缺失 legacy credentials.json 直接失败。\n- windows-runtime-smoke / windows-real-regression 在 RequireCredentials 分支改为 warning(不阻断),明确说明当前凭据来源为 OS credential vault。\n\n提交:c06db84\n验证:git diff --check(通过);脚本冲突标记检查(通过)。

H-Chris233 added 10 commits May 6, 2026 14:39
Provider credentials should follow the platform credential-store threat model instead of continuing normal plaintext JSON writes. The vault now stores the existing multi-provider credentials payload in the OS credential store, migrates the legacy JSON file only when the vault has no entry, removes the legacy file after a successful write, and keeps raw credential IPC restricted to the main settings window.

Constraint: User explicitly chose Keychain/Credential Manager/Linux keyring for issue Open-Less#230.

Constraint: Preserve the existing provider schema and active-provider routing with minimal frontend churn.

Rejected: Keep plaintext JSON as the documented product choice | user rejected this direction.

Rejected: Split every credential field into separate vault entries | larger migration surface and risks losing active provider state.

Confidence: medium

Scope-risk: moderate

Directive: Security-model and storage-strategy choices must be confirmed with the user before implementation.

Tested: cargo check --manifest-path src-tauri/Cargo.toml; npm run -s build; git diff --check; grep for stale plaintext/Keychain fallback wording.

Not-tested: Manual migration on macOS Keychain, Windows Credential Manager, or Linux desktop keyring.

Related: Open-Less#230
The OS vault migration keeps provider state as one logical JSON payload, but Windows Credential Manager limits each credential blob. Store the logical payload as a manifest plus bounded chunks so long API keys, multiple providers, or extra headers do not make future saves fail on Windows.

Constraint: Windows Credential Manager generic credential blob is capped and keyring stores passwords as UTF-16 on Windows.

Rejected: Split each provider field into independent vault entries | broader schema rewrite and more migration surface than needed for this review fix.

Confidence: high

Scope-risk: narrow

Directive: Keep the legacy plaintext credentials.json path migration-only; do not add compatibility for transient PR-internal vault formats unless the release shipped them.

Tested: cargo check --manifest-path src-tauri/Cargo.toml

Tested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit

Tested: npm run -s build

Tested: git diff --check

Not-tested: Live Windows Credential Manager write with oversized real provider payload.

Related: Open-Less#230
Users upgrading from the plaintext credentials file should not lose access to existing settings just because the OS vault backend is temporarily unavailable or migration cannot be written. Keep the old file migration-only, but return its contents as the compatibility source when vault reads or writes fail.

Constraint: credentials.json remains a legacy compatibility source only, not the normal write target.

Rejected: Return defaults on vault read/write errors | hides existing upgrade data and makes configured providers appear empty.

Confidence: high

Scope-risk: narrow

Directive: Do not reintroduce plaintext writes; fallback reads are only for pre-PR legacy migration compatibility.

Tested: cargo check --manifest-path src-tauri/Cargo.toml

Tested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit

Tested: npm run -s build

Tested: git diff --check

Not-tested: Manual vault-unavailable upgrade run on Windows/macOS/Linux.

Related: Open-Less#230
Credential vault saves now write a fresh generation of chunks before switching the manifest, so failed chunk writes or crashes do not overwrite chunks still referenced by the previous manifest. The same change also migrates documented per-account vault entries into the chunked manifest before falling back to legacy JSON.

Constraint: Windows Credential Manager requires chunking, and legacy credentials may exist either as plaintext JSON or documented per-account vault entries.

Rejected: Overwrite stable chunk names before manifest update | can mix old and new payloads after partial writes.

Confidence: high

Scope-risk: narrow

Directive: Treat the manifest as the commit pointer; write new-generation chunks before updating it and delete old chunks only after manifest write succeeds.

Tested: cargo check --manifest-path src-tauri/Cargo.toml

Tested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit

Tested: npm run -s build

Tested: git diff --check

Not-tested: Manual crash injection during OS vault writes.

Related: Open-Less#230
The migration path must prefer the current legacy JSON source over older per-account vault leftovers, clean those leftovers after a valid manifest is available, and avoid write operations overwriting a damaged or unreadable vault payload with fallback defaults.

Constraint: credentials.json is migration-only, but it was the previous Tauri source of truth.\nRejected: Use read fallback for mutating updates | can overwrite unreadable vault data with defaults.\nConfidence: high\nScope-risk: narrow\nDirective: Mutating credential operations must propagate chunked vault read errors instead of saving fallback roots.\nTested: cargo check --manifest-path src-tauri/Cargo.toml\nTested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit\nTested: npm run -s build\nTested: git diff --check\nNot-tested: Manual migration with simultaneous JSON and stale per-account vault entries.\nRelated: Open-Less#230
Vault migration should not silently report success when credentials were written to the OS vault but the previous plaintext credentials file could not be removed. Propagating that cleanup error on write/update paths makes the remaining plaintext exposure visible instead of treating the migration as complete.

Constraint: credentials.json is allowed only as a migration source, not a durable fallback after a successful vault write.\nRejected: Keep deletion best-effort only | can leave plaintext credentials indefinitely with no user-visible failure.\nConfidence: high\nScope-risk: narrow\nDirective: Do not hide legacy plaintext deletion failures on credential write paths.\nTested: cargo check --manifest-path src-tauri/Cargo.toml\nTested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit\nTested: npm run -s build\nTested: git diff --check\nNot-tested: Manual permission-denied legacy file cleanup on installed apps.\nRelated: Open-Less#230
Linux credential storage must survive reboot after the legacy plaintext file is removed. The keyring linux-native feature uses kernel keyutils, so the Linux build now enables the persistent Secret Service-backed combo and the migration read fallback also considers older per-account vault entries when a current manifest is unreadable.

Constraint: credentials.json must be removed after successful vault migration.\nConstraint: keyring linux-native/keyutils is an in-memory cache across reboot.\nRejected: Keep plaintext JSON as Linux fallback | violates the vault storage boundary.\nConfidence: high\nScope-risk: moderate\nDirective: Linux credential storage must remain persistent if legacy plaintext cleanup is enforced.\nTested: cargo check --manifest-path src-tauri/Cargo.toml\nTested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit\nTested: npm run -s build\nTested: git diff --check\nNot-tested: Manual Secret Service read/write on a desktop Linux keyring session.\nRelated: Open-Less#230
Legacy per-account vault migration must be all-or-nothing. If one old entry is temporarily unreadable, saving the readable subset and deleting the original entries can silently drop the failed secret, so update-path migration now aborts on any legacy vault read error while the non-mutating read fallback remains conservative.

Constraint: migration cleanup deletes old per-account vault entries after saving the chunked manifest.\nRejected: Migrate the readable subset | can discard an unreadable legacy secret before retry.\nConfidence: high\nScope-risk: narrow\nDirective: Do not remove legacy per-account vault entries unless every legacy account read completed without error.\nTested: cargo check --manifest-path src-tauri/Cargo.toml\nTested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit\nTested: npm run -s build\nTested: git diff --check\nNot-tested: Manual transient per-account vault read failure during upgrade.\nRelated: Open-Less#230
When the current legacy credentials.json source wins migration, older per-account vault entries should not remain as stale fallback data. After the JSON payload is successfully written to the chunked vault and plaintext cleanup succeeds, the migration now removes those older per-account entries as well.

Constraint: credentials.json is the previous Tauri source of truth and must win over stale Swift-era vault entries.\nRejected: Leave per-account entries for fallback | keeps stale secrets reachable after successful migration.\nConfidence: high\nScope-risk: narrow\nDirective: Clean old per-account vault entries after any successful authoritative migration source is saved.\nTested: cargo check --manifest-path src-tauri/Cargo.toml\nTested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit\nTested: npm run -s build\nTested: git diff --check\nNot-tested: Manual upgrade with both credentials.json and stale per-account vault entries.\nRelated: Open-Less#230
Credential writes should not fail only because an old migration source cannot be deleted. Once current vault data is readable and writable, legacy plaintext cleanup remains best-effort and logged so users can still save or remove credentials even if credentials.json is locked or read-only.\n\nThe rebase also brought the new Japanese and Korean locale files onto this branch, so the credential storage notice key is filled there to keep the frontend type contract intact.\n\nConstraint: credentials.json is obsolete migration data after vault write.\nRejected: Propagate legacy JSON deletion failures on every vault save | blocks credential updates despite a valid vault.\nConfidence: high\nScope-risk: narrow\nDirective: Do not make credential updates depend on deleting legacy plaintext files.\nTested: cargo check --manifest-path src-tauri/Cargo.toml\nTested: cargo test --manifest-path src-tauri/Cargo.toml credential_payload_chunks_stay_under_windows_blob_limit\nTested: npm run -s build\nTested: git diff --check\nNot-tested: Manual locked/read-only credentials.json cleanup.\nRelated: Open-Less#230
@H-Chris233 H-Chris233 force-pushed the fix/issue-230-credential-storage-model branch from b5f281d to deb1196 Compare May 6, 2026 06:43
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit b5f281d

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit deb1196

Windows smoke scripts should no longer fail solely because credentials.json was migrated into the OS credential vault. The default suite now runs runtime startup without the obsolete JSON gate, and explicit legacy JSON checks warn instead of blocking vault-backed test machines.\n\nConstraint: Provider credentials now live in the OS credential vault after migration.\nRejected: Query Windows Credential Manager from smoke scripts | larger platform-specific parsing surface for a smoke gate that only needs to avoid the obsolete JSON blocker.\nConfidence: high\nScope-risk: narrow\nDirective: Do not make Windows smoke defaults depend on migrated plaintext credentials.json.\nTested: git diff --check\nTested: Python sanity check for conflict markers in edited PowerShell scripts\nNot-tested: PowerShell parser unavailable in this Linux environment.\nRelated: Open-Less#230
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c06db84d3e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +86 to +87
if ($RequireCredentials -and (-not $credentialStatus.VolcengineConfigured -or -not $credentialStatus.ArkConfigured)) {
throw "Real regression requires configured Volcengine ASR and Ark LLM credentials."
Write-Warning "Legacy credentials.json is incomplete; continuing because the app uses the OS credential vault."
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the credential gate in real regressions

When this script is run with -RequireCredentials on a clean Windows machine that has neither legacy JSON nor vault credentials, this branch now only warns and continues. In the -InsertionFallback/real-ASR path that can exercise OpenLess' missing-Volcengine mock fallback instead of real ASR, so the regression can stop proving that configured credentials are present. Please either query Credential Manager here or keep -RequireCredentials as a hard abort when credentials cannot be confirmed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已处理:恢复了 windows-real-regression.ps1 中 -RequireCredentials 的硬门禁(缺少凭据时直接 throw),避免“看起来跑过了 real regression 但其实没有真实凭据”的假阳性。\n\n同时保留上一个修复:默认 smoke suite 不再强制 RequireCredentials,避免仅因 legacy credentials.json 被迁移就阻断基础启动冒烟。\n\n提交:1d04131

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit c06db84

Real regression runs that explicitly pass -RequireCredentials should still fail fast when credentials cannot be confirmed. This keeps the real-ASR signal strict while leaving the default runtime smoke suite decoupled from legacy credentials.json.\n\nConstraint: default smoke suite must not depend on migrated plaintext JSON.\nRejected: Keep warning-only behavior in real regression mode | allows credential-missing runs to appear as valid real-ASR regressions.\nConfidence: high\nScope-risk: narrow\nDirective: Keep -RequireCredentials in windows-real-regression as a hard gate unless credential-vault probing is added.\nTested: git diff --check\nNot-tested: Running Windows PowerShell regression scripts in Linux environment.\nRelated: Open-Less#230
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 1d04131

用户实测:首次允许 Keychain 后仍反复弹窗十几次。

根因:
1. save_credentials 每次都生成新 UUID 作为 chunk account 命名前缀
   (credentials.v1.chunk.<NEW_UUID>.0/.1)。macOS Keychain 的「始终允许」
   绑定到 (binary 签名, entry 名),每次保存设置都让 chunk entry 名变 →
   之前 ACL 全失效 → 重新弹。
2. load_credentials / load_credentials_for_update 每次都调
   remove_legacy_keyring_credentials() —— 对 9 个旧 stable account
   各做一次 keyring delete。delete 在 macOS 上仍触发 ACL 检查,
   每次 load 都弹 9 次「OpenLess 想删除 X」。

合计每次 IPC 触发的 load 12 次弹窗,跟实测吻合。

修复(数据安全优先):

A. chunk account 命名稳定化
   - chunk_account(generation: Option<&str>, idx) 接受可选 generation
   - save_credentials 总用 None(稳定 credentials.v1.chunk.{idx})
   - 新 manifest 不写 generation 字段(serde skip_serializing_if=None)
   - 旧 manifest 仍可读(generation: Option<String>,含 UUID 时按旧格式
     读)→ 一次 save 后无缝迁移到新格式 + 删除旧 UUID chunks

B. partial-write 安全
   - 仍保留「先写所有 chunks,再写 manifest」顺序
   - chunks 数量减少时主动删除 idx >= new count 的旧 entries
   - 旧 UUID chunks 保留删除路径(迁移用)

C. legacy keyring delete 仅在 migration 路径触发
   - load_credentials / load_credentials_for_update 不再调用
     remove_legacy_keyring_credentials()
   - migrate_legacy_sources_for_update 内部仍调用一次 → 首次成功迁移
     就清干净;之后 load 不再尝试 delete 不存在的 entries
   - legacy 文件 delete 不需 ACL,best-effort 调用保留

D. 不影响 Windows
   - Windows Credential Manager 没有 ACL 弹窗概念
   - 仍走 chunked storage(2560 byte 限制需要分块)

修复后体感:
- 首次启动迁移:~3 次弹(manifest + 2 chunks),用户点 Always Allow
- 之后启动 / 保存设置:0 次弹(ACL 长期有效)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0505c2fc54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +534 to +538
for (index, chunk) in chunks.iter().enumerate() {
let account = chunk_account(None, index);
keyring_entry_for(&account)?
.set_password(chunk)
.with_context(|| format!("write system credential vault chunk {index}"))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the old chunk set until the manifest switches

When updating an already-migrated vault, these writes reuse the stable credentials.v1.chunk.{index} accounts before the manifest is replaced. If any later chunk write or the manifest write fails (for example a Keychain ACL denial/cancel or transient Credential Manager error), the old manifest is still present but now points at a mix of new and old chunk contents; with the legacy JSON removed after the previous successful save, subsequent reads fail to decode the vault and credential updates/readbacks are effectively broken. Use a staged generation or otherwise avoid overwriting the currently-referenced chunks until the new manifest can be committed.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 0505c2f

@appergb appergb merged commit 095e312 into Open-Less:main May 6, 2026
2 checks passed
appergb pushed a commit that referenced this pull request May 6, 2026
主要修复
─────────
- fix(windows): TSF DLL 静态链接 CRT,防 host 进程(QQ 等)私有
  MSVCP140 劫持导致切微软拼音崩溃 0xc0000005(PR #287 + #289 + Codex
  P1 review)。
- fix(windows): 移除 DisableThreadLibraryCalls — /MT 下 CRT 需要
  thread attach/detach 通知做 per-thread TLS 清理,禁用反而触发崩溃。
- fix(recording): 录音条出现时 mic 已 capture,按 Option 不再吞开头
  字。emit Recording 加 race 检查避免短按时覆盖 stop/cancel UI 信号。
- fix(startup): splash 透明背景 + 卡片化,避免长启动时左半白屏。
- fix(vault): keyring chunks 用稳定 account name —— macOS Keychain
  Always-Allow 不再因 UUID 轮换失效;不再每次 load 都尝试删 9 个
  legacy entries 触发 ACL 弹窗(与 PR #277 一起)。

新功能
─────────
- feat(security): 凭据从 plaintext JSON 迁移到平台 credential vault
  (macOS Keychain / Windows Credential Manager / Linux libsecret)。
  Windows 2560 byte 限制下用 chunked storage;partial-write safe。
  legacy 多源迁移完成后自动清理(PR #277)。

工程
─────────
- chore(ci): 暂时搁置 macos-13(Intel mac)matrix —— GH Actions
  runner pool 紧张到每次 dispatch queue 1-2h(已观测 4 次)。Apple
  Silicon 用户 dmg 仍发;Intel mac 用户保留 v1.2.20 dmg + Rosetta
  说明(issue #299)。
- chore(devex): CI 加 macOS 矩阵 + 5-way 版本号一致性校验 +
  scripts/bump-version.sh 一行同步 5 处版本号。.gitignore 加
  promo-openless-v2 / node_modules / dist / target 全局兜底(PR #294)。
- docs: docs/audit-2026-05-06.md 工程审计基线,覆盖架构 / UI bug /
  多端一致性 / IPC 1:1 校验 / P0/P1/P2 改进路径。
- chore: 9 个改进 issue (#295-#303) 跟踪 audit 列出的所有 P1/P2/P3
  问题。
@H-Chris233 H-Chris233 deleted the fix/issue-230-credential-storage-model branch May 9, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(credentials): 凭据存储实现与 Keychain 威胁模型不一致

2 participants