browser-commander 0.24.0 → 0.26.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 +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +7 -4
- package/package.json +4 -1
- 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 +610 -25
- package/src/browser/launcher.js +14 -0
- 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 +18 -1
- package/src/browser/safari-access.js +45 -0
- package/src/browser/safari-cookies.js +39 -36
- package/src/browser/safari-launch.js +42 -0
- package/src/browser/safari-profiles.js +83 -0
- package/src/browser/safari-support.js +98 -0
- package/src/browser/storage-state.js +28 -8
- package/src/browser/system-browser.js +31 -143
- package/src/browser/webdriver.js +64 -19
- package/src/cli/args.js +2 -0
- package/src/cli/commands.js +2 -0
- package/src/cli/high-level-methods.js +2 -0
- package/src/cli/sessions.js +5 -0
- package/src/core/webdriver-page.js +62 -0
- package/src/exports.js +6 -0
- package/src/traces/recorder.js +5 -0
- package/tests/e2e/safari.e2e.test.js +104 -0
- 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/browser/safari.test.js +192 -0
- package/tests/unit/browser/webdriver.test.js +48 -3
- package/tests/unit/cli/dispatcher.test.js +5 -1
- package/tests/unit/cli/main.test.js +5 -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
|
@@ -1,17 +1,78 @@
|
|
|
1
1
|
// feature-parity: sources.cookie-listing@native-typed
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
4
7
|
|
|
5
8
|
import {
|
|
6
9
|
listCookieSources,
|
|
7
10
|
resolveImportSource,
|
|
8
11
|
} from '../../../src/browser/browser-cookies.js';
|
|
12
|
+
import { migrateProfile } from '../../../src/browser/migration/index.js';
|
|
9
13
|
import { writeFirefoxProfile } from '../../helpers/migration-fixtures.js';
|
|
10
14
|
import { useTempDirectories } from '../../helpers/temp-directory.js';
|
|
11
15
|
|
|
12
16
|
const makeDirectory = useTempDirectories();
|
|
13
17
|
|
|
14
18
|
describe('listCookieSources', () => {
|
|
19
|
+
it('matches only whole hosts and subdomains in both SQLite families', async () => {
|
|
20
|
+
const hosts = [
|
|
21
|
+
'.github.com',
|
|
22
|
+
'api.GITHUB.COM.',
|
|
23
|
+
'notgithub.com',
|
|
24
|
+
'.github.com.attacker.test',
|
|
25
|
+
'.gitXhub.com',
|
|
26
|
+
];
|
|
27
|
+
for (const browser of ['firefox', 'chrome']) {
|
|
28
|
+
const homeDir = await makeDirectory('bc-source-domains-');
|
|
29
|
+
let file;
|
|
30
|
+
if (browser === 'firefox') {
|
|
31
|
+
const profile = await writeFirefoxProfile(
|
|
32
|
+
homeDir,
|
|
33
|
+
hosts.map((host, index) => ({
|
|
34
|
+
host,
|
|
35
|
+
name: `${index}`,
|
|
36
|
+
value: 'secret',
|
|
37
|
+
}))
|
|
38
|
+
);
|
|
39
|
+
file = path.join(profile, 'cookies.sqlite');
|
|
40
|
+
} else {
|
|
41
|
+
const profile = path.join(homeDir, '.config/google-chrome/Default');
|
|
42
|
+
await mkdir(profile, { recursive: true });
|
|
43
|
+
file = path.join(profile, 'Cookies');
|
|
44
|
+
const database = new BetterSqlite3(file);
|
|
45
|
+
try {
|
|
46
|
+
database.exec('CREATE TABLE cookies (host_key TEXT, value TEXT)');
|
|
47
|
+
for (const host of hosts) {
|
|
48
|
+
database
|
|
49
|
+
.prepare('INSERT INTO cookies VALUES (?, ?)')
|
|
50
|
+
.run(host, 'secret');
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
database.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const before = await readFile(file);
|
|
57
|
+
const sources = await listCookieSources({
|
|
58
|
+
domains: ['GITHUB.COM.', 'git_hub.com', '%github.com'],
|
|
59
|
+
platform: 'linux',
|
|
60
|
+
homeDir,
|
|
61
|
+
environment: {},
|
|
62
|
+
});
|
|
63
|
+
assert.equal(sources.length, 1);
|
|
64
|
+
assert.equal(sources[0].browser, browser);
|
|
65
|
+
assert.equal(sources[0].cookies, hosts.length);
|
|
66
|
+
assert.deepEqual(sources[0].byDomain, {
|
|
67
|
+
'GITHUB.COM.': 2,
|
|
68
|
+
'git_hub.com': 0,
|
|
69
|
+
'%github.com': 0,
|
|
70
|
+
});
|
|
71
|
+
assert.ok(!JSON.stringify(sources).includes('secret'));
|
|
72
|
+
assert.deepEqual(await readFile(file), before);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
15
76
|
it('reports cookie counts per profile without reading values', async () => {
|
|
16
77
|
const temporaryDirectory = await makeDirectory('bc-src-');
|
|
17
78
|
const profilePath = await writeFirefoxProfile(temporaryDirectory, [
|
|
@@ -92,23 +153,37 @@ describe('resolveImportSource', () => {
|
|
|
92
153
|
});
|
|
93
154
|
});
|
|
94
155
|
|
|
95
|
-
it('falls back
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
156
|
+
it('falls back when the default holds unrelated or lookalike domains', async () => {
|
|
157
|
+
for (const host of ['.other.test', '.notgithub.com']) {
|
|
158
|
+
const homeDir = await makeDirectory('bc-import-fallback-');
|
|
159
|
+
await writeFirefoxProfile(homeDir, [{ ...otherCookie[0], host }]);
|
|
160
|
+
await writeFirefoxProfile(homeDir, githubCookie, {
|
|
161
|
+
root: '.librewolf',
|
|
162
|
+
name: 'default',
|
|
163
|
+
});
|
|
164
|
+
const source = await resolve(homeDir);
|
|
165
|
+
assert.equal(source.browser, 'librewolf');
|
|
166
|
+
assert.equal(source.profile, 'default');
|
|
167
|
+
assert.equal(source.warning.reason, 'default-browser-fallback');
|
|
168
|
+
assert.equal(source.warning.item, 'librewolf');
|
|
169
|
+
assert.match(
|
|
170
|
+
source.warning.detail,
|
|
171
|
+
/default browser \(firefox\) holds no cookies for github\.com; imported from librewolf/
|
|
172
|
+
);
|
|
173
|
+
const report = await migrateProfile({
|
|
174
|
+
from: { browser: 'auto' },
|
|
175
|
+
to: path.join(homeDir, 'target'),
|
|
176
|
+
include: ['cookies'],
|
|
177
|
+
domains: ['github.com'],
|
|
178
|
+
platform: 'linux',
|
|
179
|
+
homeDir,
|
|
180
|
+
environment: {},
|
|
181
|
+
runCommand: firefoxIsDefault,
|
|
182
|
+
});
|
|
183
|
+
assert.equal(report.source.browser, 'librewolf');
|
|
184
|
+
assert.equal(report.migrated.cookies, 1);
|
|
185
|
+
assert.equal(report.cookies[0].domain, '.github.com');
|
|
186
|
+
}
|
|
112
187
|
});
|
|
113
188
|
|
|
114
189
|
it('falls back when the system default cannot be determined', async () => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { BROWSER_SOURCES } from '../../../src/browser/browser-sources.js';
|
|
4
|
+
import { readSafeStoragePassword } from '../../../src/browser/browser-cookie-credentials.js';
|
|
5
|
+
|
|
6
|
+
describe('catalogue credential identities and Keychain diagnostics', () => {
|
|
7
|
+
it('requests every declared browser service rather than a four-browser table', async () => {
|
|
8
|
+
for (const browser of BROWSER_SOURCES.filter(
|
|
9
|
+
(entry) => entry.safeStorage
|
|
10
|
+
)) {
|
|
11
|
+
for (const name of [browser.id, ...(browser.aliases ?? [])]) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const password = await readSafeStoragePassword({
|
|
14
|
+
browser: name,
|
|
15
|
+
platform: 'darwin',
|
|
16
|
+
environment: {},
|
|
17
|
+
runCredentialCommand: async (command, args) => {
|
|
18
|
+
calls.push([command, args]);
|
|
19
|
+
return 'synthetic-password';
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
assert.equal(password, 'synthetic-password');
|
|
23
|
+
assert.deepEqual(calls, [
|
|
24
|
+
[
|
|
25
|
+
'security',
|
|
26
|
+
['find-generic-password', '-w', '-s', browser.safeStorage.service],
|
|
27
|
+
],
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const outcome of ['denied', 'empty']) {
|
|
34
|
+
it(`identifies the item and retry path when Keychain returns ${outcome}`, async () => {
|
|
35
|
+
await assert.rejects(
|
|
36
|
+
readSafeStoragePassword({
|
|
37
|
+
browser: 'chrome',
|
|
38
|
+
platform: 'darwin',
|
|
39
|
+
runCredentialCommand: async () => {
|
|
40
|
+
if (outcome === 'denied') {
|
|
41
|
+
throw new Error('security exited with code 36');
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
/Chrome Safe Storage.*Keychain.*allow.*retry.*refresh=true/s
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { it } from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { openSqliteDatabase } from '../../../src/browser/browser-cookie-database.js';
|
|
8
|
+
import { readBrowserCookiesWithDependencies } from '../../../src/browser/browser-cookies.js';
|
|
9
|
+
import { migrateProfile } from '../../../src/browser/migration/index.js';
|
|
10
|
+
|
|
11
|
+
const fixture = await readFile(
|
|
12
|
+
new URL(
|
|
13
|
+
'../../../../tests/fixtures/firefox-cookie-expiry.sql',
|
|
14
|
+
import.meta.url
|
|
15
|
+
),
|
|
16
|
+
'utf8'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
for (const version of [0, 15, 16, 17]) {
|
|
20
|
+
for (const migration of [false, true]) {
|
|
21
|
+
it(`normalizes Firefox schema ${version} expiry through ${migration ? 'migration' : 'installed reading'}`, async () => {
|
|
22
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'bc-ff-expiry-'));
|
|
23
|
+
try {
|
|
24
|
+
const root = path.join(homeDir, '.mozilla', 'firefox');
|
|
25
|
+
const source = path.join(root, 'expiry.default-release');
|
|
26
|
+
await mkdir(source, { recursive: true });
|
|
27
|
+
await writeFile(
|
|
28
|
+
path.join(root, 'profiles.ini'),
|
|
29
|
+
'[Profile0]\nName=default-release\nIsRelative=1\nPath=expiry.default-release\nDefault=1\n'
|
|
30
|
+
);
|
|
31
|
+
const file = path.join(source, 'cookies.sqlite');
|
|
32
|
+
const database = await openSqliteDatabase(file);
|
|
33
|
+
try {
|
|
34
|
+
database.exec(fixture);
|
|
35
|
+
database.exec(`PRAGMA user_version = ${version}`);
|
|
36
|
+
if (version >= 16) {
|
|
37
|
+
database.exec(
|
|
38
|
+
'UPDATE moz_cookies SET expiry = expiry * 1000 + 999 WHERE expiry > 0'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
database.close();
|
|
43
|
+
}
|
|
44
|
+
const before = await readFile(file);
|
|
45
|
+
const options = { platform: 'linux', homeDir, environment: {} };
|
|
46
|
+
const cookies = migration
|
|
47
|
+
? (
|
|
48
|
+
await migrateProfile({
|
|
49
|
+
from: { browser: 'firefox', userDataDir: source },
|
|
50
|
+
to: path.join(homeDir, 'target'),
|
|
51
|
+
include: ['cookies'],
|
|
52
|
+
domains: ['expiry.example'],
|
|
53
|
+
...options,
|
|
54
|
+
})
|
|
55
|
+
).cookies
|
|
56
|
+
: await readBrowserCookiesWithDependencies(
|
|
57
|
+
{
|
|
58
|
+
browser: 'firefox',
|
|
59
|
+
domainFilter: 'expiry.example',
|
|
60
|
+
cache: false,
|
|
61
|
+
},
|
|
62
|
+
options
|
|
63
|
+
);
|
|
64
|
+
assert.equal(cookies.length, 3);
|
|
65
|
+
const expiry = Object.fromEntries(
|
|
66
|
+
cookies.map((cookie) => [cookie.name, cookie.expires])
|
|
67
|
+
);
|
|
68
|
+
assert.deepEqual(expiry, {
|
|
69
|
+
persistent: 2_000_000_001,
|
|
70
|
+
'session-negative': -1,
|
|
71
|
+
'session-zero': -1,
|
|
72
|
+
});
|
|
73
|
+
assert.deepEqual(await readFile(file), before);
|
|
74
|
+
} finally {
|
|
75
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { it } from 'node:test';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ALL_DATA_CLASSES,
|
|
8
|
+
migrateProfile,
|
|
9
|
+
} from '../../../../src/browser/migration/index.js';
|
|
10
|
+
import { repoPath } from '../../../helpers/repo.js';
|
|
11
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
12
|
+
|
|
13
|
+
const makeTempDir = useTempDirectories('bc-data-classes-');
|
|
14
|
+
const classes = JSON.parse(
|
|
15
|
+
await readFile(repoPath('tests/fixtures/migration-data-classes.json'), 'utf8')
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
it('requires an actual boolean for payment-card consent before target writes', async () => {
|
|
19
|
+
const target = path.join(await makeTempDir(), 'new-profile');
|
|
20
|
+
await assert.rejects(
|
|
21
|
+
migrateProfile({
|
|
22
|
+
from: { browser: 'chrome', userDataDir: await makeTempDir() },
|
|
23
|
+
to: target,
|
|
24
|
+
include: ['paymentCards'],
|
|
25
|
+
includePaymentCards: 'true',
|
|
26
|
+
}),
|
|
27
|
+
/must be a boolean/u
|
|
28
|
+
);
|
|
29
|
+
await assert.rejects(readFile(path.join(target, 'Web Data')), {
|
|
30
|
+
code: 'ENOENT',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('reports consented payment cards as unsupported until a native writer exists', async () => {
|
|
35
|
+
const report = await migrateProfile({
|
|
36
|
+
from: { browser: 'chrome', userDataDir: await makeTempDir() },
|
|
37
|
+
to: path.join(await makeTempDir(), 'new-profile'),
|
|
38
|
+
include: ['paymentCards'],
|
|
39
|
+
includePaymentCards: true,
|
|
40
|
+
});
|
|
41
|
+
assert.equal(report.migrated.paymentCards, 0);
|
|
42
|
+
assert.equal(report.skipped[0].reason, 'data-class-not-supported');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for (const browser of ['chrome', 'firefox', 'safari']) {
|
|
46
|
+
it(`reports every selected additional data class for ${browser} without reading protected card data`, async () => {
|
|
47
|
+
const source = await makeTempDir();
|
|
48
|
+
const target = path.join(await makeTempDir(), 'new-profile');
|
|
49
|
+
const profile =
|
|
50
|
+
browser === 'chrome' ? path.join(source, 'Default') : source;
|
|
51
|
+
await mkdir(profile, { recursive: true });
|
|
52
|
+
const marker = Buffer.from(
|
|
53
|
+
'protected card store must remain unread and unchanged'
|
|
54
|
+
);
|
|
55
|
+
await writeFile(path.join(profile, 'Web Data'), marker);
|
|
56
|
+
const report = await migrateProfile({
|
|
57
|
+
from: { browser, userDataDir: source },
|
|
58
|
+
to: target,
|
|
59
|
+
include: classes.slice(6),
|
|
60
|
+
platform: 'darwin',
|
|
61
|
+
});
|
|
62
|
+
assert.deepEqual(ALL_DATA_CLASSES, classes);
|
|
63
|
+
assert.deepEqual(Object.keys(report.migrated), classes);
|
|
64
|
+
assert.ok(Object.values(report.migrated).every((count) => count === 0));
|
|
65
|
+
for (const type of classes.slice(6)) {
|
|
66
|
+
assert.ok(
|
|
67
|
+
report.skipped.some((entry) => entry.type === type),
|
|
68
|
+
type
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
assert.equal(
|
|
72
|
+
report.skipped.find(({ type }) => type === 'paymentCards').reason,
|
|
73
|
+
'payment-card-consent-required'
|
|
74
|
+
);
|
|
75
|
+
const passkeys = report.skipped.filter(({ type }) => type === 'passkeys');
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
passkeys.map(({ item }) => item),
|
|
78
|
+
['iCloud Keychain', 'Google Password Manager', 'Windows Hello']
|
|
79
|
+
);
|
|
80
|
+
for (const entry of passkeys) {
|
|
81
|
+
assert.equal(entry.reason, 'passkey-not-exportable');
|
|
82
|
+
assert.match(entry.detail, /persistent profile/u);
|
|
83
|
+
}
|
|
84
|
+
await assert.rejects(readFile(path.join(target, 'Web Data')), {
|
|
85
|
+
code: 'ENOENT',
|
|
86
|
+
});
|
|
87
|
+
assert.deepEqual(await readFile(path.join(profile, 'Web Data')), marker);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
8
|
+
import { migrateHistory } from '../../../../src/browser/migration/history.js';
|
|
9
|
+
import { migratePasswords } from '../../../../src/browser/migration/passwords.js';
|
|
10
|
+
import { encryptChromiumValue } from '../../../../src/browser/migration/chromium-crypto.js';
|
|
11
|
+
import {
|
|
12
|
+
decryptChromiumCookie,
|
|
13
|
+
deriveChromiumCookieKey,
|
|
14
|
+
} from '../../../../src/browser/browser-cookie-crypto.js';
|
|
15
|
+
|
|
16
|
+
const temp = useTempDirectories('bc-domain-isolation-');
|
|
17
|
+
describe('per-site domain isolation (#119)', () => {
|
|
18
|
+
for (const domains of [['github.com'], []]) {
|
|
19
|
+
it(`re-encrypts login notes and ${domains.length ? 'filters' : 'preserves'} domain metadata`, async () => {
|
|
20
|
+
const source = await temp();
|
|
21
|
+
const target = await temp();
|
|
22
|
+
const sourceKey = randomBytes(16);
|
|
23
|
+
const targetKey = randomBytes(16);
|
|
24
|
+
const sourcePath = path.join(source, 'Login Data');
|
|
25
|
+
const db = new Database(sourcePath);
|
|
26
|
+
db.exec(
|
|
27
|
+
await readFile(
|
|
28
|
+
new URL(
|
|
29
|
+
'../../../../../tests/fixtures/password-domain-isolation.sql',
|
|
30
|
+
import.meta.url
|
|
31
|
+
),
|
|
32
|
+
'utf8'
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
const encrypted = (plaintext) =>
|
|
36
|
+
encryptChromiumValue({
|
|
37
|
+
plaintext,
|
|
38
|
+
key: sourceKey,
|
|
39
|
+
platform: 'linux',
|
|
40
|
+
prefix: 'v11',
|
|
41
|
+
});
|
|
42
|
+
db.prepare('UPDATE logins SET password_value=? WHERE id IN (7,8)').run(
|
|
43
|
+
encrypted('password')
|
|
44
|
+
);
|
|
45
|
+
const sourceNote = encrypted('private note ☃');
|
|
46
|
+
db.prepare(
|
|
47
|
+
'UPDATE password_notes SET value=? WHERE id IN (70,71,72)'
|
|
48
|
+
).run(sourceNote);
|
|
49
|
+
db.close();
|
|
50
|
+
const original = await readFile(sourcePath);
|
|
51
|
+
const report = await migratePasswords({
|
|
52
|
+
sourceProfileDir: source,
|
|
53
|
+
targetProfileDir: target,
|
|
54
|
+
domains,
|
|
55
|
+
platform: 'linux',
|
|
56
|
+
resolveSourceKey: () => sourceKey,
|
|
57
|
+
targetKey,
|
|
58
|
+
});
|
|
59
|
+
const filtered = domains.length > 0;
|
|
60
|
+
assert.equal(report.migrated, filtered ? 1 : 2);
|
|
61
|
+
const copy = new Database(path.join(target, 'Login Data'));
|
|
62
|
+
try {
|
|
63
|
+
assert.deepEqual(
|
|
64
|
+
copy.prepare('SELECT parent_id FROM insecure_credentials').all(),
|
|
65
|
+
filtered ? [{ parent_id: 7 }] : [{ parent_id: 7 }, { parent_id: 8 }]
|
|
66
|
+
);
|
|
67
|
+
assert.deepEqual(
|
|
68
|
+
copy.prepare('SELECT origin_domain FROM stats').all(),
|
|
69
|
+
filtered
|
|
70
|
+
? [{ origin_domain: 'https://github.com' }]
|
|
71
|
+
: [
|
|
72
|
+
{ origin_domain: 'https://github.com' },
|
|
73
|
+
{ origin_domain: 'https://notgithub.com' },
|
|
74
|
+
]
|
|
75
|
+
);
|
|
76
|
+
const notes = copy
|
|
77
|
+
.prepare(
|
|
78
|
+
'SELECT id,value,date_created,confidential FROM password_notes'
|
|
79
|
+
)
|
|
80
|
+
.all();
|
|
81
|
+
assert.equal(notes.length, filtered ? 1 : 2);
|
|
82
|
+
assert.equal(notes[0].id, 70);
|
|
83
|
+
assert.equal(notes[0].date_created, 123);
|
|
84
|
+
assert.equal(notes[0].confidential, 1);
|
|
85
|
+
for (const note of notes) {
|
|
86
|
+
assert.equal(
|
|
87
|
+
decryptChromiumCookie({
|
|
88
|
+
encryptedValue: note.value,
|
|
89
|
+
databaseVersion: 0,
|
|
90
|
+
platform: 'linux',
|
|
91
|
+
key: targetKey,
|
|
92
|
+
}),
|
|
93
|
+
'private note ☃'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
for (const table of [
|
|
97
|
+
'sync_entities_metadata',
|
|
98
|
+
'sync_model_metadata',
|
|
99
|
+
'future_password_metadata',
|
|
100
|
+
]) {
|
|
101
|
+
const preserved = table === 'future_password_metadata' && !filtered;
|
|
102
|
+
assert.equal(
|
|
103
|
+
copy.prepare(`SELECT count(*) AS n FROM ${table}`).get().n,
|
|
104
|
+
preserved ? 1 : 0,
|
|
105
|
+
table
|
|
106
|
+
);
|
|
107
|
+
assert.equal(
|
|
108
|
+
report.warnings.some((warning) => warning.item === table),
|
|
109
|
+
!preserved
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
assert.ok(
|
|
113
|
+
report.skipped.some(
|
|
114
|
+
(entry) =>
|
|
115
|
+
entry.item === 'password_notes/73' &&
|
|
116
|
+
entry.reason === 'app-bound-v20'
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
} finally {
|
|
120
|
+
copy.close();
|
|
121
|
+
}
|
|
122
|
+
const bytes = await readFile(path.join(target, 'Login Data'));
|
|
123
|
+
assert.equal(bytes.includes(Buffer.from('unrelated-sync-marker')), false);
|
|
124
|
+
assert.equal(
|
|
125
|
+
bytes.includes(Buffer.from('unrelated-future-marker')),
|
|
126
|
+
!filtered
|
|
127
|
+
);
|
|
128
|
+
assert.equal(bytes.includes(sourceNote), false);
|
|
129
|
+
assert.deepEqual(await readFile(sourcePath), original);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
it('excludes unrelated history URLs, their visits, and Top Sites', async () => {
|
|
134
|
+
const source = await temp();
|
|
135
|
+
const target = await temp();
|
|
136
|
+
const db = new Database(path.join(source, 'History'));
|
|
137
|
+
db.exec(
|
|
138
|
+
await readFile(
|
|
139
|
+
new URL(
|
|
140
|
+
'../../../../../tests/fixtures/history-domain-isolation.sql',
|
|
141
|
+
import.meta.url
|
|
142
|
+
),
|
|
143
|
+
'utf8'
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
db.close();
|
|
147
|
+
await migrateHistory({
|
|
148
|
+
sourceProfileDir: source,
|
|
149
|
+
targetProfileDir: target,
|
|
150
|
+
domains: ['github.com'],
|
|
151
|
+
});
|
|
152
|
+
const copy = new Database(path.join(target, 'History'));
|
|
153
|
+
try {
|
|
154
|
+
assert.equal(copy.prepare('SELECT count(*) AS n FROM urls').get().n, 1);
|
|
155
|
+
assert.equal(copy.prepare('SELECT count(*) AS n FROM visits').get().n, 1);
|
|
156
|
+
for (const table of [
|
|
157
|
+
'content_annotations',
|
|
158
|
+
'context_annotations',
|
|
159
|
+
'segment_usage',
|
|
160
|
+
'downloads',
|
|
161
|
+
'downloads_url_chains',
|
|
162
|
+
'downloads_slices',
|
|
163
|
+
]) {
|
|
164
|
+
assert.equal(
|
|
165
|
+
copy.prepare(`SELECT count(*) AS n FROM ${table}`).get().n,
|
|
166
|
+
1,
|
|
167
|
+
table
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
copy.close();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('filters passwords before decryption and removes undecryptable source values', async () => {
|
|
176
|
+
const source = await temp();
|
|
177
|
+
const target = await temp();
|
|
178
|
+
const key = deriveChromiumCookieKey('source', 'linux');
|
|
179
|
+
const db = new Database(path.join(source, 'Login Data'));
|
|
180
|
+
db.exec('CREATE TABLE logins(origin_url TEXT, password_value BLOB)');
|
|
181
|
+
const insert = db.prepare('INSERT INTO logins VALUES (?,?)');
|
|
182
|
+
insert.run(
|
|
183
|
+
'https://github.com',
|
|
184
|
+
encryptChromiumValue({
|
|
185
|
+
plaintext: 'secret',
|
|
186
|
+
key,
|
|
187
|
+
platform: 'linux',
|
|
188
|
+
prefix: 'v11',
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
insert.run('https://notgithub.com', Buffer.from('v20unreadable'));
|
|
192
|
+
insert.run('https://locked.github.com', Buffer.from('v20unreadable'));
|
|
193
|
+
db.close();
|
|
194
|
+
const report = await migratePasswords({
|
|
195
|
+
sourceProfileDir: source,
|
|
196
|
+
targetProfileDir: target,
|
|
197
|
+
domains: ['github.com'],
|
|
198
|
+
platform: 'linux',
|
|
199
|
+
resolveSourceKey: () => key,
|
|
200
|
+
targetKey: key,
|
|
201
|
+
});
|
|
202
|
+
assert.equal(report.migrated, 1);
|
|
203
|
+
const copy = new Database(path.join(target, 'Login Data'));
|
|
204
|
+
try {
|
|
205
|
+
assert.deepEqual(copy.prepare('SELECT origin_url FROM logins').all(), [
|
|
206
|
+
{ origin_url: 'https://github.com' },
|
|
207
|
+
]);
|
|
208
|
+
} finally {
|
|
209
|
+
copy.close();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|