browser-commander 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jscpd-baseline.json +48 -30
- package/CHANGELOG.md +32 -0
- package/README.md +19 -7
- package/eslint.config.js +1 -1
- package/examples/react-test-app/package-lock.json +890 -0
- package/examples/react-test-app/package.json +4 -4
- package/package.json +20 -17
- package/src/browser/attach/snapshot.js +4 -28
- package/src/browser/browser-cookie-credentials.js +15 -8
- package/src/browser/browser-cookies.js +36 -30
- package/src/browser/browser-profile-files.js +31 -0
- package/src/browser/browser-profiles.js +11 -13
- package/src/browser/browser-sources.js +39 -9
- package/src/browser/browser-sources.json +600 -23
- package/src/browser/connector.js +46 -4
- package/src/browser/launcher.js +26 -1
- package/src/browser/migration/capabilities.json +117 -0
- package/src/browser/migration/chromium-history.sql +22 -0
- package/src/browser/migration/chromium-writers.js +85 -0
- package/src/browser/migration/cookies.js +4 -1
- package/src/browser/migration/data-classes.js +68 -0
- package/src/browser/migration/domains.js +32 -0
- package/src/browser/migration/firefox-history.js +106 -0
- package/src/browser/migration/firefox.js +39 -25
- package/src/browser/migration/history.js +161 -2
- package/src/browser/migration/index.js +229 -83
- package/src/browser/migration/os-crypt-keys.js +1 -5
- package/src/browser/migration/password-metadata.js +95 -0
- package/src/browser/migration/passwords.js +78 -41
- package/src/browser/migration/safari-import.js +100 -0
- package/src/browser/migration/safari.js +110 -0
- package/src/browser/migration/sqlite-snapshot.js +31 -65
- package/src/browser/migration/validation.js +48 -0
- package/src/browser/real-browser.js +21 -3
- package/src/browser/safari-access.js +45 -0
- package/src/browser/safari-cookies.js +39 -36
- package/src/browser/safari-profiles.js +83 -0
- package/src/browser/storage-state.js +27 -0
- package/src/browser/system-browser.js +31 -143
- package/src/browser/webdriver.js +18 -2
- package/src/cli/args.js +7 -0
- package/src/cli/commands.js +12 -6
- package/src/cli/handle-methods.js +19 -2
- package/src/cli/high-level-methods.js +22 -3
- package/src/cli/modules.js +5 -1
- package/src/cli/sessions.js +14 -2
- package/src/tests/index.js +1 -0
- package/tests/e2e/engine-matrix.e2e.test.js +107 -0
- package/tests/e2e/playwright.e2e.test.js +11 -17
- package/tests/e2e/puppeteer.e2e.test.js +13 -17
- package/tests/unit/browser/attach/snapshot.test.js +30 -0
- package/tests/unit/browser/browser-cookies.test.js +35 -0
- package/tests/unit/browser/catalogue-launch.test.js +93 -0
- package/tests/unit/browser/cookie-sources.test.js +92 -17
- package/tests/unit/browser/credential-catalogue.test.js +50 -0
- package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
- package/tests/unit/browser/migration/data-classes.test.js +89 -0
- package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
- package/tests/unit/browser/migration/firefox-history.test.js +130 -0
- package/tests/unit/browser/migration/history-metadata.test.js +74 -0
- package/tests/unit/browser/migration/index.test.js +16 -1
- package/tests/unit/browser/migration/opera-keys.test.js +22 -0
- package/tests/unit/browser/migration/safari-data.test.js +143 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
- package/tests/unit/browser/migration/validation.test.js +63 -0
- package/tests/unit/browser/migration/yandex.test.js +84 -0
- package/tests/unit/browser/real-browser.test.js +19 -0
- package/tests/unit/browser/safari-cookies.test.js +26 -1
- package/tests/unit/browser/safari-profiles.test.js +111 -0
- package/tests/unit/cli/dispatcher.test.js +6 -2
- package/tests/unit/cli/main.test.js +5 -0
- package/tests/unit/cli/selenium-parity.test.js +186 -0
- package/tests/unit/scripts/browser-support.test.js +14 -7
- package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
- package/tests/unit/scripts/migration-support.test.js +31 -0
- package/tests/unit/tests/index.test.js +2 -1
- package/tests/unit/traces/links.test.js +2 -2
|
@@ -7,15 +7,25 @@ import {
|
|
|
7
7
|
browserProfileRoot,
|
|
8
8
|
resolveBrowserProfile,
|
|
9
9
|
} from '../browser-profiles.js';
|
|
10
|
+
import { migrateSafariClass } from './safari-import.js';
|
|
11
|
+
import {
|
|
12
|
+
validateMigrationOptions,
|
|
13
|
+
validateMigrationPaths,
|
|
14
|
+
} from './validation.js';
|
|
10
15
|
import { migrateBookmarks } from './bookmarks.js';
|
|
16
|
+
import { pathExists } from './fs-utils.js';
|
|
11
17
|
import { migrateCookies } from './cookies.js';
|
|
18
|
+
import {
|
|
19
|
+
ADDITIONAL_DATA_CLASSES,
|
|
20
|
+
reportAdditionalClasses,
|
|
21
|
+
} from './data-classes.js';
|
|
12
22
|
import { migrateExtensions } from './extensions.js';
|
|
13
23
|
import {
|
|
14
24
|
migrateFirefoxBookmarks,
|
|
15
25
|
migrateFirefoxPasswords,
|
|
16
26
|
readFirefoxCookies,
|
|
17
|
-
reportFirefoxHistory,
|
|
18
27
|
} from './firefox.js';
|
|
28
|
+
import { migrateFirefoxHistory } from './firefox-history.js';
|
|
19
29
|
import { migrateHistory } from './history.js';
|
|
20
30
|
import { migratePasswords } from './passwords.js';
|
|
21
31
|
import { migratePreferences } from './preferences.js';
|
|
@@ -36,8 +46,8 @@ import {
|
|
|
36
46
|
*
|
|
37
47
|
* Cookies are returned (not written): a running Chromium re-derives its own
|
|
38
48
|
* cookie encryption, so the launcher seeds them over CDP with the existing
|
|
39
|
-
* `seedCookies` path.
|
|
40
|
-
*
|
|
49
|
+
* `seedCookies` path. Supported file imports are written into the target profile;
|
|
50
|
+
* classes without a native writer receive explicit skipped reports.
|
|
41
51
|
*/
|
|
42
52
|
|
|
43
53
|
export const ALL_DATA_CLASSES = Object.freeze([
|
|
@@ -47,6 +57,7 @@ export const ALL_DATA_CLASSES = Object.freeze([
|
|
|
47
57
|
'passwords',
|
|
48
58
|
'preferences',
|
|
49
59
|
'extensions',
|
|
60
|
+
...ADDITIONAL_DATA_CLASSES,
|
|
50
61
|
]);
|
|
51
62
|
|
|
52
63
|
// Classification is driven by the shared catalogue so every Chromium variant
|
|
@@ -62,14 +73,7 @@ function isFirefoxBrowser(browser) {
|
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
function emptyMigrated() {
|
|
65
|
-
return
|
|
66
|
-
cookies: 0,
|
|
67
|
-
bookmarks: 0,
|
|
68
|
-
history: 0,
|
|
69
|
-
passwords: 0,
|
|
70
|
-
preferences: 0,
|
|
71
|
-
extensions: 0,
|
|
72
|
-
};
|
|
76
|
+
return Object.fromEntries(ALL_DATA_CLASSES.map((type) => [type, 0]));
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
async function resolveSourceProfileDir({
|
|
@@ -114,34 +118,6 @@ function mergeReport(report, className, fragment) {
|
|
|
114
118
|
report.warnings.push(...(fragment.warnings ?? []));
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
function reportSafariClasses(report, selected) {
|
|
118
|
-
for (const type of ALL_DATA_CLASSES.filter(
|
|
119
|
-
(name) => name !== 'cookies' && selected.has(name)
|
|
120
|
-
)) {
|
|
121
|
-
report.skipped.push({
|
|
122
|
-
type,
|
|
123
|
-
item: report.source.browser,
|
|
124
|
-
reason:
|
|
125
|
-
type === 'passwords'
|
|
126
|
-
? 'safari-password-export-required'
|
|
127
|
-
: 'safari-class-not-supported',
|
|
128
|
-
detail:
|
|
129
|
-
type === 'passwords'
|
|
130
|
-
? 'Safari passwords live in the Keychain. Export Passwords from Safari or the Passwords app to CSV; CSV import is tracked separately and is not supported yet.'
|
|
131
|
-
: 'Safari currently supports cookie import only; this data class has not been translated.',
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
if (report.cookies.length > 0) {
|
|
135
|
-
report.warnings.push({
|
|
136
|
-
type: 'cookies',
|
|
137
|
-
item: report.source.browser,
|
|
138
|
-
reason: 'safari-samesite-unavailable',
|
|
139
|
-
detail:
|
|
140
|
-
'Cookies.binarycookies does not store SameSite; imported cookies use Lax.',
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
121
|
async function resolvePasswordKeys({
|
|
146
122
|
keys,
|
|
147
123
|
browser,
|
|
@@ -179,7 +155,8 @@ async function resolvePasswordKeys({
|
|
|
179
155
|
* @param {{browser: string, profile: (string|undefined), userDataDir: (string|undefined)}} options.from
|
|
180
156
|
* @param {string} options.to - Target profile directory
|
|
181
157
|
* @param {string[]} [options.include=ALL_DATA_CLASSES]
|
|
182
|
-
* @param {string[]} [options.domains] -
|
|
158
|
+
* @param {string[]} [options.domains] - Per-site host/subdomain filter
|
|
159
|
+
* @param {boolean} [options.includePaymentCards=false] - Separate explicit payment-card consent
|
|
183
160
|
* @param {string} [options.platform=process.platform]
|
|
184
161
|
* @param {string} [options.targetBrowser] - Launching browser channel (key derivation)
|
|
185
162
|
* @param {Object} [options.keys] - Injected password keys {resolveSourceKey, targetKey, targetPrefix, primaryPassword}
|
|
@@ -198,6 +175,8 @@ export async function migrateProfile({
|
|
|
198
175
|
homeDir = os.homedir(),
|
|
199
176
|
environment = process.env,
|
|
200
177
|
runCommand,
|
|
178
|
+
passwordCsv,
|
|
179
|
+
includePaymentCards = false,
|
|
201
180
|
}) {
|
|
202
181
|
if (!from?.browser) {
|
|
203
182
|
throw new TypeError('migrateProfile requires from.browser');
|
|
@@ -205,6 +184,17 @@ export async function migrateProfile({
|
|
|
205
184
|
if (!to) {
|
|
206
185
|
throw new TypeError('migrateProfile requires a target directory (to)');
|
|
207
186
|
}
|
|
187
|
+
validateMigrationOptions({
|
|
188
|
+
include,
|
|
189
|
+
domains,
|
|
190
|
+
targetBrowser,
|
|
191
|
+
to,
|
|
192
|
+
platform,
|
|
193
|
+
homeDir,
|
|
194
|
+
environment,
|
|
195
|
+
classes: ALL_DATA_CLASSES,
|
|
196
|
+
includePaymentCards,
|
|
197
|
+
});
|
|
208
198
|
// An explicit userDataDir names the source, so only an installed-browser
|
|
209
199
|
// import is steered towards the profile holding the requested domains.
|
|
210
200
|
const source = await resolveImportSource({
|
|
@@ -216,6 +206,11 @@ export async function migrateProfile({
|
|
|
216
206
|
runCommand,
|
|
217
207
|
});
|
|
218
208
|
const { browser } = source;
|
|
209
|
+
if (!['chromium', 'firefox', 'safari'].includes(browserFamily(browser))) {
|
|
210
|
+
throw new TypeError(
|
|
211
|
+
`Browser ${browser} supports detection only; its profile format is not supported for migration`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
219
214
|
const profile = from.profile ?? source.profile ?? 'Default';
|
|
220
215
|
const isFirefox = isFirefoxBrowser(browser);
|
|
221
216
|
const sourceProfileDir = await resolveSourceProfileDir({
|
|
@@ -227,6 +222,7 @@ export async function migrateProfile({
|
|
|
227
222
|
environment,
|
|
228
223
|
});
|
|
229
224
|
const selected = new Set(include);
|
|
225
|
+
validateMigrationPaths(sourceProfileDir, to);
|
|
230
226
|
const report = {
|
|
231
227
|
source: { browser, profile, userDataDir: from.userDataDir ?? null },
|
|
232
228
|
target: to,
|
|
@@ -238,6 +234,13 @@ export async function migrateProfile({
|
|
|
238
234
|
if (source.warning) {
|
|
239
235
|
report.warnings.push(source.warning);
|
|
240
236
|
}
|
|
237
|
+
report.skipped.push(
|
|
238
|
+
...reportAdditionalClasses({
|
|
239
|
+
selected,
|
|
240
|
+
profileDir: sourceProfileDir,
|
|
241
|
+
includePaymentCards,
|
|
242
|
+
})
|
|
243
|
+
);
|
|
241
244
|
|
|
242
245
|
if (selected.has('cookies')) {
|
|
243
246
|
if (isFirefox) {
|
|
@@ -263,8 +266,21 @@ export async function migrateProfile({
|
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
if (browserFamily(browser) === 'safari') {
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
return migrateSafariProfile({
|
|
270
|
+
selected,
|
|
271
|
+
passwordCsv,
|
|
272
|
+
keys,
|
|
273
|
+
browser,
|
|
274
|
+
targetBrowser,
|
|
275
|
+
platform,
|
|
276
|
+
sourceProfileDir,
|
|
277
|
+
environment,
|
|
278
|
+
from,
|
|
279
|
+
to,
|
|
280
|
+
domains,
|
|
281
|
+
homeDir,
|
|
282
|
+
report,
|
|
283
|
+
});
|
|
268
284
|
}
|
|
269
285
|
|
|
270
286
|
if (selected.has('bookmarks')) {
|
|
@@ -288,10 +304,15 @@ export async function migrateProfile({
|
|
|
288
304
|
report,
|
|
289
305
|
'history',
|
|
290
306
|
isFirefox
|
|
291
|
-
? await
|
|
307
|
+
? await migrateFirefoxHistory({
|
|
308
|
+
profileDir: sourceProfileDir,
|
|
309
|
+
targetProfileDir: to,
|
|
310
|
+
domains,
|
|
311
|
+
})
|
|
292
312
|
: await migrateHistory({
|
|
293
313
|
sourceProfileDir,
|
|
294
314
|
targetProfileDir: to,
|
|
315
|
+
domains,
|
|
295
316
|
})
|
|
296
317
|
);
|
|
297
318
|
}
|
|
@@ -312,56 +333,181 @@ export async function migrateProfile({
|
|
|
312
333
|
);
|
|
313
334
|
}
|
|
314
335
|
|
|
336
|
+
if (isFirefox) {
|
|
337
|
+
for (const type of ['preferences', 'extensions']) {
|
|
338
|
+
if (selected.has(type)) {
|
|
339
|
+
report.skipped.push({
|
|
340
|
+
type,
|
|
341
|
+
item: sourceProfileDir,
|
|
342
|
+
reason: 'firefox-class-not-supported',
|
|
343
|
+
detail:
|
|
344
|
+
'Firefox preferences and extensions cannot be copied into a Chromium profile.',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
315
350
|
if (selected.has('passwords')) {
|
|
316
|
-
|
|
351
|
+
await migratePasswordClass(report, {
|
|
317
352
|
keys,
|
|
318
353
|
browser,
|
|
319
|
-
targetBrowser
|
|
354
|
+
targetBrowser,
|
|
320
355
|
platform,
|
|
321
356
|
sourceProfileDir,
|
|
322
357
|
environment,
|
|
358
|
+
to,
|
|
359
|
+
isFirefox,
|
|
360
|
+
domains,
|
|
323
361
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return report;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function migrateSafariProfile({
|
|
368
|
+
report,
|
|
369
|
+
browser,
|
|
370
|
+
domains,
|
|
371
|
+
environment,
|
|
372
|
+
from,
|
|
373
|
+
homeDir,
|
|
374
|
+
keys,
|
|
375
|
+
passwordCsv,
|
|
376
|
+
platform,
|
|
377
|
+
selected,
|
|
378
|
+
sourceProfileDir,
|
|
379
|
+
targetBrowser,
|
|
380
|
+
to,
|
|
381
|
+
}) {
|
|
382
|
+
const passwordKeys =
|
|
383
|
+
selected.has('passwords') && passwordCsv
|
|
384
|
+
? await resolvePasswordKeys({
|
|
385
|
+
keys,
|
|
386
|
+
browser,
|
|
387
|
+
targetBrowser: targetBrowser ?? 'chrome',
|
|
344
388
|
platform,
|
|
345
|
-
targetKey: passwordKeys.targetKey,
|
|
346
|
-
targetPrefix: passwordKeys.targetPrefix,
|
|
347
|
-
primaryPassword: passwordKeys.primaryPassword,
|
|
348
|
-
})
|
|
349
|
-
);
|
|
350
|
-
} else {
|
|
351
|
-
mergeReport(
|
|
352
|
-
report,
|
|
353
|
-
'passwords',
|
|
354
|
-
await migratePasswords({
|
|
355
389
|
sourceProfileDir,
|
|
356
|
-
|
|
357
|
-
platform,
|
|
358
|
-
resolveSourceKey: passwordKeys.resolveSourceKey,
|
|
359
|
-
targetKey: passwordKeys.targetKey,
|
|
360
|
-
targetPrefix: passwordKeys.targetPrefix,
|
|
390
|
+
environment,
|
|
361
391
|
})
|
|
362
|
-
|
|
363
|
-
|
|
392
|
+
: null;
|
|
393
|
+
for (const type of ALL_DATA_CLASSES.filter(
|
|
394
|
+
(type) =>
|
|
395
|
+
type !== 'cookies' &&
|
|
396
|
+
!ADDITIONAL_DATA_CLASSES.includes(type) &&
|
|
397
|
+
selected.has(type)
|
|
398
|
+
)) {
|
|
399
|
+
mergeReport(
|
|
400
|
+
report,
|
|
401
|
+
type,
|
|
402
|
+
await migrateSafariClass({
|
|
403
|
+
type,
|
|
404
|
+
profileDir: sourceProfileDir,
|
|
405
|
+
targetProfileDir: to,
|
|
406
|
+
domains,
|
|
407
|
+
passwordCsv,
|
|
408
|
+
passwordKeys,
|
|
409
|
+
platform,
|
|
410
|
+
legacyDir:
|
|
411
|
+
from.userDataDir ||
|
|
412
|
+
path.basename(path.dirname(sourceProfileDir)) === 'Profiles'
|
|
413
|
+
? undefined
|
|
414
|
+
: path.join(
|
|
415
|
+
homeDir,
|
|
416
|
+
'Library',
|
|
417
|
+
browser === 'safari' ? 'Safari' : 'Safari Technology Preview'
|
|
418
|
+
),
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (report.cookies.length) {
|
|
423
|
+
report.warnings.push({
|
|
424
|
+
type: 'cookies',
|
|
425
|
+
item: browser,
|
|
426
|
+
reason: 'safari-samesite-unavailable',
|
|
427
|
+
detail:
|
|
428
|
+
'Cookies.binarycookies does not store SameSite; imported cookies use Lax.',
|
|
429
|
+
});
|
|
364
430
|
}
|
|
365
|
-
|
|
366
431
|
return report;
|
|
367
432
|
}
|
|
433
|
+
|
|
434
|
+
async function migratePasswordClass(
|
|
435
|
+
report,
|
|
436
|
+
{
|
|
437
|
+
browser,
|
|
438
|
+
domains,
|
|
439
|
+
environment,
|
|
440
|
+
isFirefox,
|
|
441
|
+
keys,
|
|
442
|
+
platform,
|
|
443
|
+
sourceProfileDir,
|
|
444
|
+
targetBrowser,
|
|
445
|
+
to,
|
|
446
|
+
}
|
|
447
|
+
) {
|
|
448
|
+
if (
|
|
449
|
+
browser === 'yandex' &&
|
|
450
|
+
(await pathExists(path.join(sourceProfileDir, 'Ya Passman Data')))
|
|
451
|
+
) {
|
|
452
|
+
report.skipped.push({
|
|
453
|
+
type: 'passwords',
|
|
454
|
+
item: 'Ya Passman Data',
|
|
455
|
+
reason: 'yandex-passman-encryption-unsupported',
|
|
456
|
+
detail:
|
|
457
|
+
'Ya Passman Data uses local_encryptor_data and may require a Yandex master password; this extra encryption layer is not supported. Export passwords to a supported format instead.',
|
|
458
|
+
});
|
|
459
|
+
if (!(await pathExists(path.join(sourceProfileDir, 'Login Data')))) {
|
|
460
|
+
return report;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const passwordKeys = await resolvePasswordKeys({
|
|
464
|
+
keys,
|
|
465
|
+
browser,
|
|
466
|
+
targetBrowser: targetBrowser ?? (isFirefox ? 'chrome' : browser),
|
|
467
|
+
platform,
|
|
468
|
+
sourceProfileDir,
|
|
469
|
+
environment,
|
|
470
|
+
});
|
|
471
|
+
if (!passwordKeys?.targetKey) {
|
|
472
|
+
report.skipped.push({
|
|
473
|
+
type: 'passwords',
|
|
474
|
+
item: 'Login Data',
|
|
475
|
+
reason: 'target-key-unavailable',
|
|
476
|
+
});
|
|
477
|
+
report.warnings.push({
|
|
478
|
+
type: 'passwords',
|
|
479
|
+
item: 'Login Data',
|
|
480
|
+
reason: 'target-key-unavailable',
|
|
481
|
+
detail:
|
|
482
|
+
'A target encryption key was not available (on Windows the launcher must generate one and write it into the target Local State); passwords were not migrated.',
|
|
483
|
+
});
|
|
484
|
+
} else if (isFirefox) {
|
|
485
|
+
mergeReport(
|
|
486
|
+
report,
|
|
487
|
+
'passwords',
|
|
488
|
+
await migrateFirefoxPasswords({
|
|
489
|
+
profileDir: sourceProfileDir,
|
|
490
|
+
targetProfileDir: to,
|
|
491
|
+
platform,
|
|
492
|
+
targetKey: passwordKeys.targetKey,
|
|
493
|
+
targetPrefix: passwordKeys.targetPrefix,
|
|
494
|
+
primaryPassword: passwordKeys.primaryPassword,
|
|
495
|
+
domains,
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
mergeReport(
|
|
500
|
+
report,
|
|
501
|
+
'passwords',
|
|
502
|
+
await migratePasswords({
|
|
503
|
+
sourceProfileDir,
|
|
504
|
+
targetProfileDir: to,
|
|
505
|
+
platform,
|
|
506
|
+
resolveSourceKey: passwordKeys.resolveSourceKey,
|
|
507
|
+
targetKey: passwordKeys.targetKey,
|
|
508
|
+
targetPrefix: passwordKeys.targetPrefix,
|
|
509
|
+
domains,
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
1
|
import { deriveChromiumCookieKey } from '../browser-cookie-crypto.js';
|
|
4
2
|
import {
|
|
5
3
|
decryptWindowsDpapi,
|
|
@@ -101,6 +99,4 @@ export async function resolveTargetKey({
|
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
/** Convenience: the Local State path next to a profile directory. */
|
|
104
|
-
export
|
|
105
|
-
return path.join(path.dirname(profileDir), 'Local State');
|
|
106
|
-
}
|
|
102
|
+
export { localStatePathForProfile } from '../browser-profile-files.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { matchesDomains } from './domains.js';
|
|
2
|
+
|
|
3
|
+
/** Filter Login Data associations after pruning logins; never retain source keys. */
|
|
4
|
+
export async function migratePasswordMetadata({
|
|
5
|
+
db,
|
|
6
|
+
domains,
|
|
7
|
+
rewriteValue,
|
|
8
|
+
skipped,
|
|
9
|
+
warnings,
|
|
10
|
+
}) {
|
|
11
|
+
const tables = db
|
|
12
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
13
|
+
.all()
|
|
14
|
+
.map((row) => row.name);
|
|
15
|
+
for (const table of ['password_notes', 'insecure_credentials']) {
|
|
16
|
+
if (tables.includes(table)) {
|
|
17
|
+
db.exec(
|
|
18
|
+
`DELETE FROM ${table} WHERE NOT EXISTS (SELECT 1 FROM logins WHERE logins.id=${table}.parent_id)`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (tables.includes('password_notes')) {
|
|
23
|
+
const rows = db
|
|
24
|
+
.prepare(
|
|
25
|
+
'SELECT password_notes.rowid AS rowid,logins.origin_url,password_notes.value FROM password_notes JOIN logins ON logins.id=password_notes.parent_id'
|
|
26
|
+
)
|
|
27
|
+
.all();
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const result = await rewriteValue(row.value, row.origin_url);
|
|
30
|
+
if (result.reason) {
|
|
31
|
+
db.prepare('DELETE FROM password_notes WHERE rowid=?').run(row.rowid);
|
|
32
|
+
skipped.push({
|
|
33
|
+
type: 'passwords',
|
|
34
|
+
item: `password_notes/${row.rowid}`,
|
|
35
|
+
...result,
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
db.prepare('UPDATE password_notes SET value=? WHERE rowid=?').run(
|
|
39
|
+
result.value,
|
|
40
|
+
row.rowid
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const syncTables = [
|
|
46
|
+
'sync_entities_metadata',
|
|
47
|
+
'sync_model_metadata',
|
|
48
|
+
'incoming_sharing_invitation_sync_entities_metadata',
|
|
49
|
+
'incoming_sharing_invitation_sync_model_metadata',
|
|
50
|
+
];
|
|
51
|
+
for (const table of syncTables) {
|
|
52
|
+
if (tables.includes(table)) {
|
|
53
|
+
resetMetadata(db, table, 'sync-metadata-reset', warnings);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!domains?.length) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (tables.includes('stats')) {
|
|
60
|
+
for (const row of db
|
|
61
|
+
.prepare('SELECT rowid AS rowid,origin_domain FROM stats')
|
|
62
|
+
.all()) {
|
|
63
|
+
if (!matchesDomains(row.origin_domain ?? '', domains)) {
|
|
64
|
+
db.prepare('DELETE FROM stats WHERE rowid=?').run(row.rowid);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const known = new Set([
|
|
69
|
+
'logins',
|
|
70
|
+
'meta',
|
|
71
|
+
'password_notes',
|
|
72
|
+
'insecure_credentials',
|
|
73
|
+
'stats',
|
|
74
|
+
...syncTables,
|
|
75
|
+
]);
|
|
76
|
+
for (const table of tables) {
|
|
77
|
+
if (!known.has(table) && !table.startsWith('sqlite_')) {
|
|
78
|
+
resetMetadata(db, table, 'unsupported-password-metadata', warnings);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resetMetadata(db, table, reason, warnings) {
|
|
84
|
+
// Only the identifier comes from the database; quote it rather than execute it.
|
|
85
|
+
const quoted = `"${table.replaceAll('"', '""')}"`;
|
|
86
|
+
const { changes } = db.prepare(`DELETE FROM ${quoted}`).run();
|
|
87
|
+
if (changes) {
|
|
88
|
+
warnings.push({
|
|
89
|
+
type: 'passwords',
|
|
90
|
+
item: table,
|
|
91
|
+
reason,
|
|
92
|
+
detail: `${changes} copied metadata rows removed`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -6,6 +6,8 @@ import { decryptChromiumCookie } from '../browser-cookie-crypto.js';
|
|
|
6
6
|
import { openSqliteDatabase } from '../browser-cookie-database.js';
|
|
7
7
|
import { withDatabaseSnapshot } from './sqlite-snapshot.js';
|
|
8
8
|
import { encryptChromiumValue } from './chromium-crypto.js';
|
|
9
|
+
import { matchesDomains } from './domains.js';
|
|
10
|
+
import { migratePasswordMetadata } from './password-metadata.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Saved-password migration.
|
|
@@ -49,6 +51,44 @@ function hostFromOrigin(originUrl) {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
async function rewriteSecret(
|
|
55
|
+
value,
|
|
56
|
+
origin,
|
|
57
|
+
{ platform, resolveSourceKey, targetKey, targetPrefix }
|
|
58
|
+
) {
|
|
59
|
+
const encryptedValue = toBuffer(value);
|
|
60
|
+
if (!encryptedValue.length) {
|
|
61
|
+
return { value: encryptedValue };
|
|
62
|
+
}
|
|
63
|
+
const prefix = encryptedValue.subarray(0, 3).toString('ascii');
|
|
64
|
+
if (prefix === 'v20') {
|
|
65
|
+
return { reason: 'app-bound-v20' };
|
|
66
|
+
}
|
|
67
|
+
if (prefix !== 'v10' && prefix !== 'v11') {
|
|
68
|
+
return { reason: 'unsupported-encryption' };
|
|
69
|
+
}
|
|
70
|
+
let plaintext;
|
|
71
|
+
try {
|
|
72
|
+
plaintext = decryptChromiumCookie({
|
|
73
|
+
encryptedValue,
|
|
74
|
+
host: hostFromOrigin(origin),
|
|
75
|
+
databaseVersion: 0,
|
|
76
|
+
platform,
|
|
77
|
+
key: await resolveSourceKey(prefix),
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { reason: 'decrypt-failed', detail: error.message };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
value: encryptChromiumValue({
|
|
84
|
+
plaintext,
|
|
85
|
+
key: targetKey,
|
|
86
|
+
platform,
|
|
87
|
+
prefix: targetPrefix,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
/**
|
|
53
93
|
* Migrate saved passwords into the target profile's `Login Data`.
|
|
54
94
|
*
|
|
@@ -69,6 +109,7 @@ export async function migratePasswords({
|
|
|
69
109
|
resolveSourceKey,
|
|
70
110
|
targetKey,
|
|
71
111
|
targetPrefix = platform === 'win32' ? 'v10' : 'v11',
|
|
112
|
+
domains,
|
|
72
113
|
}) {
|
|
73
114
|
const sourcePath = path.join(sourceProfileDir, 'Login Data');
|
|
74
115
|
if (!(await pathExists(sourcePath))) {
|
|
@@ -112,56 +153,52 @@ export async function migratePasswords({
|
|
|
112
153
|
const update = db.prepare(
|
|
113
154
|
'UPDATE logins SET password_value = ? WHERE rowid = ?'
|
|
114
155
|
);
|
|
156
|
+
const remove = db.prepare('DELETE FROM logins WHERE rowid = ?');
|
|
157
|
+
const skip = (row, reason, detail) => {
|
|
158
|
+
remove.run(row.rowid);
|
|
159
|
+
skipped.push({
|
|
160
|
+
type: 'passwords',
|
|
161
|
+
item: row.origin_url ?? '(unknown)',
|
|
162
|
+
reason,
|
|
163
|
+
...(detail ? { detail } : {}),
|
|
164
|
+
});
|
|
165
|
+
};
|
|
115
166
|
for (const row of rows) {
|
|
116
|
-
|
|
117
|
-
|
|
167
|
+
if (!matchesDomains(row.origin_url ?? '', domains)) {
|
|
168
|
+
remove.run(row.rowid);
|
|
118
169
|
continue;
|
|
119
170
|
}
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
skipped.push({
|
|
123
|
-
type: 'passwords',
|
|
124
|
-
item: row.origin_url ?? '(unknown)',
|
|
125
|
-
reason: 'app-bound-v20',
|
|
126
|
-
});
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (prefix !== 'v10' && prefix !== 'v11') {
|
|
130
|
-
skipped.push({
|
|
131
|
-
type: 'passwords',
|
|
132
|
-
item: row.origin_url ?? '(unknown)',
|
|
133
|
-
reason: 'unsupported-encryption',
|
|
134
|
-
});
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
let plaintext;
|
|
138
|
-
try {
|
|
139
|
-
const key = await resolveSourceKey(prefix);
|
|
140
|
-
plaintext = decryptChromiumCookie({
|
|
141
|
-
encryptedValue,
|
|
142
|
-
host: hostFromOrigin(row.origin_url),
|
|
143
|
-
databaseVersion: 0,
|
|
144
|
-
platform,
|
|
145
|
-
key,
|
|
146
|
-
});
|
|
147
|
-
} catch (error) {
|
|
148
|
-
skipped.push({
|
|
149
|
-
type: 'passwords',
|
|
150
|
-
item: row.origin_url ?? '(unknown)',
|
|
151
|
-
reason: 'decrypt-failed',
|
|
152
|
-
detail: error.message,
|
|
153
|
-
});
|
|
171
|
+
const encryptedValue = toBuffer(row.password_value);
|
|
172
|
+
if (encryptedValue.length === 0) {
|
|
154
173
|
continue;
|
|
155
174
|
}
|
|
156
|
-
const
|
|
157
|
-
plaintext,
|
|
158
|
-
key: targetKey,
|
|
175
|
+
const result = await rewriteSecret(encryptedValue, row.origin_url, {
|
|
159
176
|
platform,
|
|
160
|
-
|
|
177
|
+
resolveSourceKey,
|
|
178
|
+
targetKey,
|
|
179
|
+
targetPrefix,
|
|
161
180
|
});
|
|
162
|
-
|
|
181
|
+
if (result.reason) {
|
|
182
|
+
skip(row, result.reason, result.detail);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
update.run(result.value, row.rowid);
|
|
163
186
|
migrated += 1;
|
|
164
187
|
}
|
|
188
|
+
await migratePasswordMetadata({
|
|
189
|
+
db,
|
|
190
|
+
domains,
|
|
191
|
+
skipped,
|
|
192
|
+
warnings,
|
|
193
|
+
rewriteValue: (value, origin) =>
|
|
194
|
+
rewriteSecret(value, origin, {
|
|
195
|
+
platform,
|
|
196
|
+
resolveSourceKey,
|
|
197
|
+
targetKey,
|
|
198
|
+
targetPrefix,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
db.exec('VACUUM');
|
|
165
202
|
} finally {
|
|
166
203
|
db.close();
|
|
167
204
|
}
|