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