summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/IEProfileMigrator.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/IEProfileMigrator.sys.mjs')
-rw-r--r--browser/components/migration/IEProfileMigrator.sys.mjs402
1 files changed, 402 insertions, 0 deletions
diff --git a/browser/components/migration/IEProfileMigrator.sys.mjs b/browser/components/migration/IEProfileMigrator.sys.mjs
new file mode 100644
index 0000000000..1a8c231b55
--- /dev/null
+++ b/browser/components/migration/IEProfileMigrator.sys.mjs
@@ -0,0 +1,402 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const kLoginsKey =
+ "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
+import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
+import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ OSCrypto: "resource://gre/modules/OSCrypto_win.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ ctypes: "resource://gre/modules/ctypes.sys.mjs",
+});
+
+// Resources
+
+function History() {}
+
+History.prototype = {
+ type: MigrationUtils.resourceTypes.HISTORY,
+
+ get exists() {
+ return true;
+ },
+
+ migrate: function H_migrate(aCallback) {
+ let pageInfos = [];
+ let typedURLs = MSMigrationUtils.getTypedURLs(
+ "Software\\Microsoft\\Internet Explorer"
+ );
+ let now = new Date();
+ let maxDate = new Date(
+ Date.now() - MigrationUtils.HISTORY_MAX_AGE_IN_MILLISECONDS
+ );
+
+ for (let entry of Cc[
+ "@mozilla.org/profile/migrator/iehistoryenumerator;1"
+ ].createInstance(Ci.nsISimpleEnumerator)) {
+ let url = entry.get("uri").QueryInterface(Ci.nsIURI);
+ // MSIE stores some types of URLs in its history that we don't handle,
+ // like HTMLHelp and others. Since we don't properly map handling for
+ // all of them we just avoid importing them.
+ if (!["http", "https", "ftp", "file"].includes(url.scheme)) {
+ continue;
+ }
+
+ let title = entry.get("title");
+ // Embed visits have no title and don't need to be imported.
+ if (!title.length) {
+ continue;
+ }
+
+ // The typed urls are already fixed-up, so we can use them for comparison.
+ let transition = typedURLs.has(url.spec)
+ ? lazy.PlacesUtils.history.TRANSITIONS.LINK
+ : lazy.PlacesUtils.history.TRANSITIONS.TYPED;
+
+ let time = entry.get("time");
+
+ let visitDate = time ? lazy.PlacesUtils.toDate(time) : null;
+ if (visitDate && visitDate < maxDate) {
+ continue;
+ }
+
+ pageInfos.push({
+ url,
+ title,
+ visits: [
+ {
+ transition,
+ // use the current date if we have no visits for this entry.
+ date: visitDate ?? now,
+ },
+ ],
+ });
+ }
+
+ // Check whether there is any history to import.
+ if (!pageInfos.length) {
+ aCallback(true);
+ return;
+ }
+
+ MigrationUtils.insertVisitsWrapper(pageInfos).then(
+ () => aCallback(true),
+ () => aCallback(false)
+ );
+ },
+};
+
+// IE form password migrator supporting windows from XP until 7 and IE from 7 until 11
+function IE7FormPasswords() {
+ // used to distinguish between this migrator and other passwords migrators in tests.
+ this.name = "IE7FormPasswords";
+}
+
+IE7FormPasswords.prototype = {
+ type: MigrationUtils.resourceTypes.PASSWORDS,
+
+ get exists() {
+ // work only on windows until 7
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ return false;
+ }
+
+ try {
+ let nsIWindowsRegKey = Ci.nsIWindowsRegKey;
+ let key =
+ Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ nsIWindowsRegKey
+ );
+ key.open(
+ nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ kLoginsKey,
+ nsIWindowsRegKey.ACCESS_READ
+ );
+ let count = key.valueCount;
+ key.close();
+ return count > 0;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ async migrate(aCallback) {
+ let uris = []; // the uris of the websites that are going to be migrated
+ for (let entry of Cc[
+ "@mozilla.org/profile/migrator/iehistoryenumerator;1"
+ ].createInstance(Ci.nsISimpleEnumerator)) {
+ let uri = entry.get("uri").QueryInterface(Ci.nsIURI);
+ // MSIE stores some types of URLs in its history that we don't handle, like HTMLHelp
+ // and others. Since we are not going to import the logins that are performed in these URLs
+ // we can just skip them.
+ if (!["http", "https", "ftp"].includes(uri.scheme)) {
+ continue;
+ }
+
+ uris.push(uri);
+ }
+ await this._migrateURIs(uris);
+ aCallback(true);
+ },
+
+ /**
+ * Migrate the logins that were saved for the uris arguments.
+ *
+ * @param {nsIURI[]} uris - the uris that are going to be migrated.
+ */
+ async _migrateURIs(uris) {
+ this.ctypesKernelHelpers = new MSMigrationUtils.CtypesKernelHelpers();
+ this._crypto = new lazy.OSCrypto();
+ let nsIWindowsRegKey = Ci.nsIWindowsRegKey;
+ let key =
+ Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ nsIWindowsRegKey
+ );
+ key.open(
+ nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ kLoginsKey,
+ nsIWindowsRegKey.ACCESS_READ
+ );
+
+ let urlsSet = new Set(); // set of the already processed urls.
+ // number of the successfully decrypted registry values
+ let successfullyDecryptedValues = 0;
+ /* The logins are stored in the registry, where the key is a hashed URL and its
+ * value contains the encrypted details for all logins for that URL.
+ *
+ * First iterate through IE history, hashing each URL and looking for a match. If
+ * found, decrypt the value, using the URL as a salt. Finally add any found logins
+ * to the Firefox password manager.
+ */
+
+ let logins = [];
+ for (let uri of uris) {
+ try {
+ // remove the query and the ref parts of the URL
+ let urlObject = new URL(uri.spec);
+ let url = urlObject.origin + urlObject.pathname;
+ // if the current url is already processed, it should be skipped
+ if (urlsSet.has(url)) {
+ continue;
+ }
+ urlsSet.add(url);
+ // hash value of the current uri
+ let hashStr = this._crypto.getIELoginHash(url);
+ if (!key.hasValue(hashStr)) {
+ continue;
+ }
+ let value = key.readBinaryValue(hashStr);
+ // if no value was found, the uri is skipped
+ if (value == null) {
+ continue;
+ }
+ let data;
+ try {
+ // the url is used as salt to decrypt the registry value
+ data = this._crypto.decryptData(value, url);
+ } catch (e) {
+ continue;
+ }
+ // extract the login details from the decrypted data
+ let ieLogins = this._extractDetails(data, uri);
+ // if at least a credential was found in the current data, successfullyDecryptedValues should
+ // be incremented by one
+ if (ieLogins.length) {
+ successfullyDecryptedValues++;
+ }
+ for (let ieLogin of ieLogins) {
+ logins.push({
+ username: ieLogin.username,
+ password: ieLogin.password,
+ origin: ieLogin.url,
+ timeCreated: ieLogin.creation,
+ });
+ }
+ } catch (e) {
+ console.error("Error while importing logins for ", uri.spec, ": ", e);
+ }
+ }
+
+ if (logins.length) {
+ await MigrationUtils.insertLoginsWrapper(logins);
+ }
+
+ // if the number of the imported values is less than the number of values in the key, it means
+ // that not all the values were imported and an error should be reported
+ if (successfullyDecryptedValues < key.valueCount) {
+ console.error(
+ "We failed to decrypt and import some logins. " +
+ "This is likely because we didn't find the URLs where these " +
+ "passwords were submitted in the IE history and which are needed to be used " +
+ "as keys in the decryption."
+ );
+ }
+
+ key.close();
+ this._crypto.finalize();
+ this.ctypesKernelHelpers.finalize();
+ },
+
+ _crypto: null,
+
+ /**
+ * Extract the details of one or more logins from the raw decrypted data.
+ *
+ * @param {string} data - the decrypted data containing raw information.
+ * @param {nsURI} uri - the nsURI of page where the login has occur.
+ * @returns {object[]} array of objects where each of them contains the username, password, URL,
+ * and creation time representing all the logins found in the data arguments.
+ */
+ _extractDetails(data, uri) {
+ // the structure of the header of the IE7 decrypted data for all the logins sharing the same URL
+ let loginData = new lazy.ctypes.StructType("loginData", [
+ // Bytes 0-3 are not needed and not documented
+ { unknown1: lazy.ctypes.uint32_t },
+ // Bytes 4-7 are the header size
+ { headerSize: lazy.ctypes.uint32_t },
+ // Bytes 8-11 are the data size
+ { dataSize: lazy.ctypes.uint32_t },
+ // Bytes 12-19 are not needed and not documented
+ { unknown2: lazy.ctypes.uint32_t },
+ { unknown3: lazy.ctypes.uint32_t },
+ // Bytes 20-23 are the data count: each username and password is considered as a data
+ { dataMax: lazy.ctypes.uint32_t },
+ // Bytes 24-35 are not needed and not documented
+ { unknown4: lazy.ctypes.uint32_t },
+ { unknown5: lazy.ctypes.uint32_t },
+ { unknown6: lazy.ctypes.uint32_t },
+ ]);
+
+ // the structure of a IE7 decrypted login item
+ let loginItem = new lazy.ctypes.StructType("loginItem", [
+ // Bytes 0-3 are the offset of the username
+ { usernameOffset: lazy.ctypes.uint32_t },
+ // Bytes 4-11 are the date
+ { loDateTime: lazy.ctypes.uint32_t },
+ { hiDateTime: lazy.ctypes.uint32_t },
+ // Bytes 12-15 are not needed and not documented
+ { foo: lazy.ctypes.uint32_t },
+ // Bytes 16-19 are the offset of the password
+ { passwordOffset: lazy.ctypes.uint32_t },
+ // Bytes 20-31 are not needed and not documented
+ { unknown1: lazy.ctypes.uint32_t },
+ { unknown2: lazy.ctypes.uint32_t },
+ { unknown3: lazy.ctypes.uint32_t },
+ ]);
+
+ let url = uri.prePath;
+ let results = [];
+ let arr = this._crypto.stringToArray(data);
+ // convert data to ctypes.unsigned_char.array(arr.length)
+ let cdata = lazy.ctypes.unsigned_char.array(arr.length)(arr);
+ // Bytes 0-35 contain the loginData data structure for all the logins sharing the same URL
+ let currentLoginData = lazy.ctypes.cast(cdata, loginData);
+ let headerSize = currentLoginData.headerSize;
+ let currentInfoIndex = loginData.size;
+ // pointer to the current login item
+ let currentLoginItemPointer = lazy.ctypes.cast(
+ cdata.addressOfElement(currentInfoIndex),
+ loginItem.ptr
+ );
+ // currentLoginData.dataMax is the data count: each username and password is considered as
+ // a data. So, the number of logins is the number of data dived by 2
+ let numLogins = currentLoginData.dataMax / 2;
+ for (let n = 0; n < numLogins; n++) {
+ // Bytes 0-31 starting from currentInfoIndex contain the loginItem data structure for the
+ // current login
+ let currentLoginItem = currentLoginItemPointer.contents;
+ let creation =
+ this.ctypesKernelHelpers.fileTimeToSecondsSinceEpoch(
+ currentLoginItem.hiDateTime,
+ currentLoginItem.loDateTime
+ ) * 1000;
+ let currentResult = {
+ creation,
+ url,
+ };
+ // The username is UTF-16 and null-terminated.
+ currentResult.username = lazy.ctypes
+ .cast(
+ cdata.addressOfElement(
+ headerSize + 12 + currentLoginItem.usernameOffset
+ ),
+ lazy.ctypes.char16_t.ptr
+ )
+ .readString();
+ // The password is UTF-16 and null-terminated.
+ currentResult.password = lazy.ctypes
+ .cast(
+ cdata.addressOfElement(
+ headerSize + 12 + currentLoginItem.passwordOffset
+ ),
+ lazy.ctypes.char16_t.ptr
+ )
+ .readString();
+ results.push(currentResult);
+ // move to the next login item
+ currentLoginItemPointer = currentLoginItemPointer.increment();
+ }
+ return results;
+ },
+};
+
+/**
+ * Internet Explorer profile migrator
+ */
+export class IEProfileMigrator extends MigratorBase {
+ static get key() {
+ return "ie";
+ }
+
+ static get displayNameL10nID() {
+ return "migration-wizard-migrator-display-name-ie";
+ }
+
+ static get brandImage() {
+ return "chrome://browser/content/migration/brands/ie.png";
+ }
+
+ getResources() {
+ let resources = [MSMigrationUtils.getBookmarksMigrator(), new History()];
+ // Only support the form password migrator for Windows XP to 7.
+ if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) {
+ resources.push(new IE7FormPasswords());
+ }
+ let windowsVaultFormPasswordsMigrator =
+ MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
+ windowsVaultFormPasswordsMigrator.name = "IEVaultFormPasswords";
+ resources.push(windowsVaultFormPasswordsMigrator);
+ return resources.filter(r => r.exists);
+ }
+
+ async getLastUsedDate() {
+ const datePromises = ["Favs", "CookD"].map(dirId => {
+ const { path } = Services.dirsvc.get(dirId, Ci.nsIFile);
+ return IOUtils.stat(path)
+ .then(info => info.lastModified)
+ .catch(() => 0);
+ });
+
+ const dates = await Promise.all(datePromises);
+
+ try {
+ const typedURLs = MSMigrationUtils.getTypedURLs(
+ "Software\\Microsoft\\Internet Explorer"
+ );
+ // typedURLs.values() returns an array of PRTimes, which are in
+ // microseconds - convert to milliseconds
+ dates.push(Math.max(0, ...typedURLs.values()) / 1000);
+ } catch (ex) {}
+
+ return new Date(Math.max(...dates));
+ }
+}