diff --git a/apps/desktop/src/main/browser/browser-profile.ts b/apps/desktop/src/main/browser/browser-profile.ts index 397001b..2ddbe81 100644 --- a/apps/desktop/src/main/browser/browser-profile.ts +++ b/apps/desktop/src/main/browser/browser-profile.ts @@ -171,9 +171,13 @@ function readSourceKey(userDataDir: string): SourceKeyInfo { } } -/** DPAPI 解出源浏览器的 AES 密钥(PowerShell 调 CryptUnprotectData;用户级,无需管理员) */ +/** DPAPI 解出源浏览器的 AES 密钥(PowerShell 调 CryptUnprotectData;用户级,无需管理员)。 + * Chromium 的 encrypted_key = base64("DPAPI" + 密文 blob):CryptUnprotectData 只接受 + * 剥掉 5 字节前缀后的 blob,前缀判断必须在解码后的字节上做——base64 文本本身 + * 永远不以 "DPAPI" 开头(实测带前缀调用必然 CryptographicException: 数据无效)。 */ function dpapiUnprotectKey(encodedKey: string): Promise { - const blob = encodedKey.startsWith('DPAPI') ? encodedKey.slice(5) : encodedKey + const bytes = Buffer.from(encodedKey, 'base64') + const blob = bytes.subarray(0, 5).toString('utf8') === 'DPAPI' ? bytes.subarray(5).toString('base64') : encodedKey const script = `$d=[Convert]::FromBase64String('${blob}');` + 'Add-Type -AssemblyName System.Security;' + @@ -183,9 +187,12 @@ function dpapiUnprotectKey(encodedKey: string): Promise { 'powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { timeout: 20_000 }, - (err, stdout) => { + (err, stdout, stderr) => { if (err || !stdout.trim()) { - log.warn('dpapi unprotect failed', { err: err instanceof Error ? err.message : String(err ?? 'empty output') }) + log.warn('dpapi unprotect failed', { + err: err instanceof Error ? err.message : String(err ?? 'empty output'), + stderr: stderr ? String(stderr).slice(0, 200) : undefined + }) resolve(null) return }