Skip to content

Commit 40385ef

Browse files
fix(desktop): count cookies the import could not decrypt
Rows `decryptChromiumValue` could not read were dropped without a trace, so an import that recovered a fraction of the database still reported a clean success. They now reach the user as part of the skipped total. This matters most on Linux, where records written under a keyring-derived `v11` key are unreadable unless that secret is reachable — see the note in `ChromiumKeys`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4a9fec5 commit 40385ef

2 files changed

Lines changed: 24 additions & 5 deletions

File tree

apps/desktop/src/preview/BrowserImport/BrowserImport.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export const make = Effect.gen(function* BrowserImportMake() {
144144
});
145145
}
146146

147-
const cookies = yield* readChromiumCookies({
147+
const read = yield* readChromiumCookies({
148148
cookieDatabasePath: cookieDatabasePath(definition, paths, requestedProfile.directory),
149149
keychainService: definition.keychainService,
150150
keychainAccount: definition.keychainAccount,
@@ -172,8 +172,10 @@ export const make = Effect.gen(function* BrowserImportMake() {
172172
// Written one at a time rather than in parallel: Chromium's cookie store
173173
// serialises writes anyway, and a rejected cookie should only cost itself.
174174
let imported = 0;
175-
let skipped = 0;
176-
for (const cookie of cookies) {
175+
// Rows the reader could not decrypt are already lost cookies, so they
176+
// count as skipped rather than vanishing from the tally.
177+
let skipped = read.undecryptable;
178+
for (const cookie of read.cookies) {
177179
const written = yield* Effect.tryPromise({
178180
try: () =>
179181
session.cookies.set({

apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,16 @@ const decryptValue = (encrypted: Uint8Array, key: Buffer, domain: string): strin
236236
}
237237
};
238238

239+
/**
240+
* What a reader produces: the cookies it could recover, and how many stored
241+
* rows it could not. The count reaches the user as part of the skipped total
242+
* rather than disappearing.
243+
*/
244+
export interface CookieReadResult {
245+
readonly cookies: ReadonlyArray<ChromiumCookie>;
246+
readonly undecryptable: number;
247+
}
248+
239249
export interface ChromiumCookieSource {
240250
readonly cookieDatabasePath: string;
241251
readonly keychainService: string;
@@ -302,9 +312,16 @@ export const readChromiumCookies = Effect.fn("ChromiumCookies.readChromiumCookie
302312
);
303313

304314
const cookies: ChromiumCookie[] = [];
315+
// Counted, not swallowed. A row we hold no usable key for is a cookie the
316+
// user does not get, and reporting an import that quietly dropped most of
317+
// its rows as a clean success is the worst of the options.
318+
let undecryptable = 0;
305319
for (const row of rows) {
306320
const value = decryptValue(row.encrypted_value, key, row.host_key);
307-
if (value === null) continue;
321+
if (value === null) {
322+
undecryptable += 1;
323+
continue;
324+
}
308325
const secure = row.is_secure === 1;
309326
const scope = cookieScope(row.host_key, row.path, secure);
310327
cookies.push({
@@ -319,5 +336,5 @@ export const readChromiumCookies = Effect.fn("ChromiumCookies.readChromiumCookie
319336
sameSite: sameSiteFromColumn(row.samesite),
320337
});
321338
}
322-
return cookies satisfies ReadonlyArray<ChromiumCookie>;
339+
return { cookies, undecryptable } satisfies CookieReadResult;
323340
});

0 commit comments

Comments
 (0)