Files
orca/src/main/browser/browser-cookie-sqlite.ts
T
2026-09-01 02:07:33 -07:00

148 lines
4.0 KiB
TypeScript

const CHROMIUM_EPOCH_OFFSET = 11644473600n
export function chromiumTimestampToUnix(chromiumTs: bigint | number | string): number {
if (!chromiumTs || chromiumTs === 0n || chromiumTs === 0 || chromiumTs === '0') {
return 0
}
try {
const ts =
typeof chromiumTs === 'bigint'
? chromiumTs
: BigInt(typeof chromiumTs === 'number' ? Math.round(chromiumTs) : chromiumTs)
if (ts === 0n) {
return 0
}
return Math.max(Number(ts / 1000000n - CHROMIUM_EPOCH_OFFSET), 0)
} catch {
return 0
}
}
// Why: each platform protects the Chromium key differently: macOS/Linux PBKDF2→AES-128-CBC, Windows DPAPI→AES-256-GCM.
export type EncryptionKeyResult =
| {
mode: 'aes-128-cbc'
keysByVersion: Partial<Record<'v10' | 'v11', Buffer>>
keyringUnavailable?: boolean
}
| { mode: 'aes-256-gcm'; key: Buffer }
export type ChromiumCookieColumnInfo = {
name: string
type?: string
notnull?: number | bigint
dflt_value?: unknown
}
export function parseSqliteDefaultValue(
raw: unknown,
type: string
): string | number | Buffer | null {
if (raw === null || raw === undefined) {
return null
}
if (typeof raw !== 'string') {
return typeof raw === 'number' || typeof raw === 'bigint' ? Number(raw) : String(raw)
}
const trimmed = raw.trim()
if (!trimmed || trimmed.toUpperCase() === 'NULL') {
return null
}
if (/^X''$/i.test(trimmed) || type.includes('BLOB')) {
return Buffer.alloc(0)
}
if (
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
(trimmed.startsWith('"') && trimmed.endsWith('"'))
) {
return trimmed.slice(1, -1).replaceAll("''", "'")
}
if (type.includes('INT')) {
const numeric = Number(trimmed)
return Number.isFinite(numeric) ? numeric : 0
}
return trimmed
}
export function normalizeSqliteCookieValue(
value: unknown
): string | number | bigint | Buffer | null {
if (value instanceof Uint8Array) {
return Buffer.from(value)
}
if (value === undefined || value === null) {
return null
}
if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'string') {
return value
}
return String(value)
}
export function isSqliteNotNull(column: ChromiumCookieColumnInfo): boolean {
return Number(column.notnull ?? 0) !== 0
}
export function fallbackChromiumCookieColumnValue(
column: ChromiumCookieColumnInfo,
sourceRow: Record<string, unknown>
): string | number | bigint | Buffer | null {
const type = (column.type ?? '').toUpperCase()
const defaultValue = parseSqliteDefaultValue(column.dflt_value, type)
if (defaultValue !== null) {
return defaultValue
}
if (!isSqliteNotNull(column)) {
return null
}
switch (column.name) {
case 'value':
case 'encrypted_value':
return Buffer.alloc(0)
case 'top_frame_site_key':
return ''
case 'source_port':
return -1
case 'last_update_utc':
return normalizeSqliteCookieValue(sourceRow.creation_utc) ?? 0
default:
if (type.includes('BLOB')) {
return Buffer.alloc(0)
}
if (type.includes('INT')) {
return 0
}
return ''
}
}
export function buildChromiumCookieInsertParams(
targetColumns: ChromiumCookieColumnInfo[],
sourceRow: Record<string, unknown>,
decryptedValue: Buffer
): (string | number | bigint | Buffer | null)[] {
return targetColumns.map((column) => {
if (column.name === 'encrypted_value') {
return Buffer.alloc(0)
}
if (column.name === 'value') {
return decryptedValue
}
const sourceHasColumn = Object.hasOwn(sourceRow, column.name)
const sourceValue = sourceHasColumn ? normalizeSqliteCookieValue(sourceRow[column.name]) : null
if (sourceValue !== null) {
return sourceValue
}
if (sourceHasColumn && !isSqliteNotNull(column)) {
return null
}
// Why: cookie columns drift across Chrome/Electron versions; missing NOT NULL columns need Chromium defaults, not NULL.
return fallbackChromiumCookieColumnValue(column, sourceRow)
})
}