fix(browser): node:sqlite 的 BLOB 是裸 Uint8Array,Cookie 解密前必须转 Buffer
This commit is contained in:
@@ -206,13 +206,16 @@ function dpapiUnprotectKey(encodedKey: string): Promise<Buffer | null> {
|
||||
})
|
||||
}
|
||||
|
||||
/** 解密单条 Chrome Cookie(v10/v11 均为 AES-256-GCM:3 字节版本 + 12 字节 nonce + 密文 + 16 字节 tag) */
|
||||
function decryptChromeBlob(encrypted: Buffer, key: Buffer): string {
|
||||
const version = encrypted.subarray(0, 3).toString('utf8')
|
||||
/** 解密单条 Chrome Cookie(v10/v11 均为 AES-256-GCM:3 字节版本 + 12 字节 nonce + 密文 + 16 字节 tag)。
|
||||
* 注意 node:sqlite 返回的 BLOB 是裸 Uint8Array 而非 Buffer——其 toString() 会忽略
|
||||
* 编码参数、返回逗号拼接的数字,必须先转成 Buffer 才能按 utf8 读版本头。 */
|
||||
function decryptChromeBlob(encrypted: Uint8Array, key: Buffer): string {
|
||||
const blob = Buffer.isBuffer(encrypted) ? encrypted : Buffer.from(encrypted)
|
||||
const version = blob.subarray(0, 3).toString('utf8')
|
||||
if (version !== 'v10' && version !== 'v11') throw new Error(`不支持的 Cookie 加密版本: ${version}`)
|
||||
const nonce = encrypted.subarray(3, 15)
|
||||
const tag = encrypted.subarray(encrypted.length - 16)
|
||||
const data = encrypted.subarray(15, encrypted.length - 16)
|
||||
const nonce = blob.subarray(3, 15)
|
||||
const tag = blob.subarray(blob.length - 16)
|
||||
const data = blob.subarray(15, blob.length - 16)
|
||||
const decipher = createDecipheriv('aes-256-gcm', key, nonce)
|
||||
decipher.setAuthTag(tag)
|
||||
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
|
||||
@@ -284,7 +287,7 @@ function applyStagedLocalStorage(staging: string, targetPartitionDir: string): v
|
||||
}
|
||||
|
||||
type SourceCookieRead =
|
||||
| { kind: 'ok'; cookies: BrowserCookieSeed[]; skipped: number }
|
||||
| { kind: 'ok'; cookies: BrowserCookieSeed[]; v20: number; undecryptable: number }
|
||||
| { kind: 'unsupported'; note: string }
|
||||
|
||||
/**
|
||||
@@ -317,14 +320,16 @@ async function readSourceCookies(root: string, staging: string): Promise<SourceC
|
||||
)
|
||||
.all()
|
||||
const cookies: BrowserCookieSeed[] = []
|
||||
let skipped = 0
|
||||
let v20 = 0
|
||||
let undecryptable = 0
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const r = row as {
|
||||
host_key: string
|
||||
name: string
|
||||
value: string
|
||||
encrypted_value: Buffer | null
|
||||
/** node:sqlite 的 BLOB 是裸 Uint8Array(非 Buffer),用前必须转换 */
|
||||
encrypted_value: Uint8Array | null
|
||||
path: string
|
||||
is_secure: number
|
||||
is_httponly: number
|
||||
@@ -333,10 +338,16 @@ async function readSourceCookies(root: string, staging: string): Promise<SourceC
|
||||
}
|
||||
let value = r.value
|
||||
if (!value && r.encrypted_value && r.encrypted_value.length > 3) {
|
||||
value = decryptChromeBlob(r.encrypted_value, key)
|
||||
const encrypted = Buffer.from(r.encrypted_value)
|
||||
// v20 = 应用绑定加密(Chrome 127+ 逐步启用),第三方无法解密,单独计数便于如实提示
|
||||
if (encrypted.subarray(0, 3).toString('utf8') === 'v20') {
|
||||
v20++
|
||||
continue
|
||||
}
|
||||
value = decryptChromeBlob(encrypted, key)
|
||||
}
|
||||
if (!value) {
|
||||
skipped++
|
||||
undecryptable++
|
||||
continue
|
||||
}
|
||||
cookies.push({
|
||||
@@ -351,10 +362,10 @@ async function readSourceCookies(root: string, staging: string): Promise<SourceC
|
||||
...(mapSameSite(r.samesite) ? { sameSite: mapSameSite(r.samesite) } : {})
|
||||
})
|
||||
} catch {
|
||||
skipped++
|
||||
undecryptable++
|
||||
}
|
||||
}
|
||||
return { kind: 'ok', cookies, skipped }
|
||||
return { kind: 'ok', cookies, v20, undecryptable }
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
@@ -427,8 +438,11 @@ async function importBrowserProfileInner(sourceId: string, profileDir: string):
|
||||
if (sourceRead.kind === 'ok') {
|
||||
const result = await browser.importCookies(sourceRead.cookies)
|
||||
injected = result.imported
|
||||
const skipped = result.failed + sourceRead.skipped
|
||||
if (skipped > 0) cookieNote = `源 Cookie ${injected} 条已注入,${skipped} 条无法解密已跳过。`
|
||||
const skipNotes: string[] = []
|
||||
if (sourceRead.v20 > 0) skipNotes.push(`${sourceRead.v20} 条为应用绑定加密(v20)无法迁移`)
|
||||
const failedCount = result.failed + sourceRead.undecryptable
|
||||
if (failedCount > 0) skipNotes.push(`${failedCount} 条解密失败已跳过`)
|
||||
if (skipNotes.length > 0) cookieNote = `源 Cookie ${injected} 条已注入,${skipNotes.join(',')}。`
|
||||
} else {
|
||||
cookieNote = sourceRead.note
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user