security(v3.2.0): W4 凭证不嵌 URL + W5 OTA Copy 替代 Move

W4: clone URL 不再含明文凭证, 改为先 Cache-GitCredentials
    (credential manager) 再用原始 URL clone, git 自动取凭证
W5: OTA 原子替换 Move-Item → Copy-Item + Remove
    (04-27 Move 数据丢失教训, Copy 中途失败可完整回滚)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
bookworm 2026-04-28 00:37:35 +08:00
parent df3a105f92
commit 6ac69b93ac
3 changed files with 14 additions and 16 deletions

Binary file not shown.

View File

@ -1660,9 +1660,8 @@ elseif (Test-Path $ClaudeDir) {
Log-Warn "匿名克隆失败, 弹凭证对话框重试..." Log-Warn "匿名克隆失败, 弹凭证对话框重试..."
$cred = Show-GiteaCredentialDialog $cred = Show-GiteaCredentialDialog
if ($cred) { if ($cred) {
$cloneUrl = $GitUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@" Cache-GitCredentials $cred
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000 $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000
if (Test-CloneComplete) { Cache-GitCredentials $cred }
} }
} }
if (Test-CloneComplete) { if (Test-CloneComplete) {
@ -1676,15 +1675,13 @@ elseif (Test-Path $ClaudeDir) {
} }
else { else {
Log-Info "首次安装, 匿名克隆配置仓库..." Log-Info "首次安装, 匿名克隆配置仓库..."
# v3.0.1 先匿名, 失败才弹凭证
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (匿名)" 180000 $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (匿名)" 180000
if (-not (Test-CloneComplete)) { if (-not (Test-CloneComplete)) {
Log-Warn "匿名克隆失败, 弹凭证对话框重试..." Log-Warn "匿名克隆失败, 弹凭证对话框重试..."
$cred = Show-GiteaCredentialDialog $cred = Show-GiteaCredentialDialog
if ($cred) { if ($cred) {
$cloneUrl = $GitUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@" Cache-GitCredentials $cred
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000 $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000
if (Test-CloneComplete) { Cache-GitCredentials $cred }
} }
} }
if (Test-CloneComplete) { if (Test-CloneComplete) {
@ -1718,9 +1715,8 @@ if (Test-Path (Join-Path $BootDir ".git")) {
Log-Warn "匿名克隆失败, 弹凭证对话框重试..." Log-Warn "匿名克隆失败, 弹凭证对话框重试..."
$cred = Show-GiteaCredentialDialog $cred = Show-GiteaCredentialDialog
if ($cred) { if ($cred) {
$bootCloneUrl = $BootUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@" Cache-GitCredentials $cred
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $bootCloneUrl, $BootDir) "克隆 boot 仓库 (认证)" 180000 $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $BootUrl, $BootDir) "克隆 boot 仓库 (认证)" 180000
if (Test-Path (Join-Path $BootDir "crypto-helper.js")) { Cache-GitCredentials $cred }
} }
} }
if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) { if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) {

View File

@ -167,12 +167,12 @@ function Invoke-OtaSync ($cred, $cfg, $remoteVersion) {
$stageDir = Join-Path $env:TEMP "bw-ota-$([Guid]::NewGuid().ToString().Substring(0,8))" $stageDir = Join-Path $env:TEMP "bw-ota-$([Guid]::NewGuid().ToString().Substring(0,8))"
# 1. Clone (basic auth user:pass) # 1. Clone (凭证通过 credential manager 传递, 不嵌入 URL)
Write-Ota "下载 $ref ..." Write-Ota "下载 $ref ..."
$escapedUser = [Uri]::EscapeDataString($cred.user) $plainUrl = "https://${host_}/${repoPath}.git"
$escapedPass = [Uri]::EscapeDataString($cred.pass) $credInput = "protocol=https`nhost=${host_}`nusername=$($cred.user)`npassword=$($cred.pass)`n`n"
$cloneUrl = "https://${escapedUser}:${escapedPass}@${host_}/${repoPath}.git" $credInput | & git credential approve 2>$null
$cloneArgs = @('-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $cloneUrl, $stageDir) $cloneArgs = @('-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $plainUrl, $stageDir)
& git @cloneArgs 2>&1 | Out-Null & git @cloneArgs 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "clone 失败 (exit $LASTEXITCODE)" } if ($LASTEXITCODE -ne 0) { throw "clone 失败 (exit $LASTEXITCODE)" }
$gitDir = Join-Path $stageDir '.git' $gitDir = Join-Path $stageDir '.git'
@ -276,9 +276,11 @@ process.exit(crypto.verify(null,d,k,s)?0:1);
} }
} }
try { try {
Move-Item -Path $stageDir -Destination $ClaudeRoot -ErrorAction Stop Copy-Item -Path $stageDir -Destination $ClaudeRoot -Recurse -Force -ErrorAction Stop
Remove-Item -Path $stageDir -Recurse -Force -ErrorAction SilentlyContinue
} catch { } catch {
if ($bakFull -and (Test-Path $bakFull)) { if ($bakFull -and (Test-Path $bakFull)) {
Remove-Item -Path $ClaudeRoot -Recurse -Force -ErrorAction SilentlyContinue
Rename-Item -Path $bakFull -NewName (Split-Path -Leaf $ClaudeRoot) Rename-Item -Path $bakFull -NewName (Split-Path -Leaf $ClaudeRoot)
Write-OtaWarn "替换失败, 已回滚到原版本" Write-OtaWarn "替换失败, 已回滚到原版本"
} }