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> 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 | 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, 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) }) }