/* 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/. */ import { ctypes } from "resource://gre/modules/ctypes.sys.mjs"; import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", }); const EDGE_FAVORITES = "AC\\MicrosoftEdge\\User\\Default\\Favorites"; const FREE_CLOSE_FAILED = 0; const INTERNET_EXPLORER_EDGE_GUID = [ 0x3ccd5499, 0x4b1087a8, 0x886015a2, 0x553bdd88, ]; const RESULT_SUCCESS = 0; const VAULT_ENUMERATE_ALL_ITEMS = 512; const WEB_CREDENTIALS_VAULT_ID = [ 0x4bf4c442, 0x41a09b8a, 0x4add80b3, 0x28db4d70, ]; const wintypes = { BOOL: ctypes.int, DWORD: ctypes.uint32_t, DWORDLONG: ctypes.uint64_t, CHAR: ctypes.char, PCHAR: ctypes.char.ptr, LPCWSTR: ctypes.char16_t.ptr, PDWORD: ctypes.uint32_t.ptr, VOIDP: ctypes.voidptr_t, WORD: ctypes.uint16_t, }; // TODO: Bug 1202978 - Refactor MSMigrationUtils ctypes helpers function CtypesKernelHelpers() { this._structs = {}; this._functions = {}; this._libs = {}; this._structs.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [ { wYear: wintypes.WORD }, { wMonth: wintypes.WORD }, { wDayOfWeek: wintypes.WORD }, { wDay: wintypes.WORD }, { wHour: wintypes.WORD }, { wMinute: wintypes.WORD }, { wSecond: wintypes.WORD }, { wMilliseconds: wintypes.WORD }, ]); this._structs.FILETIME = new ctypes.StructType("FILETIME", [ { dwLowDateTime: wintypes.DWORD }, { dwHighDateTime: wintypes.DWORD }, ]); try { this._libs.kernel32 = ctypes.open("Kernel32"); this._functions.FileTimeToSystemTime = this._libs.kernel32.declare( "FileTimeToSystemTime", ctypes.winapi_abi, wintypes.BOOL, this._structs.FILETIME.ptr, this._structs.SYSTEMTIME.ptr ); } catch (ex) { this.finalize(); } } CtypesKernelHelpers.prototype = { /** * Must be invoked once after last use of any of the provided helpers. */ finalize() { this._structs = {}; this._functions = {}; for (let key in this._libs) { let lib = this._libs[key]; try { lib.close(); } catch (ex) {} } this._libs = {}; }, /** * Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct, * and then deduces the number of seconds since the epoch (which * is the data we want for the cookie expiry date). * * @param {number} aTimeHi * Least significant DWORD. * @param {number} aTimeLo * Most significant DWORD. * @returns {number} the number of seconds since the epoch */ fileTimeToSecondsSinceEpoch(aTimeHi, aTimeLo) { let fileTime = this._structs.FILETIME(); fileTime.dwLowDateTime = aTimeLo; fileTime.dwHighDateTime = aTimeHi; let systemTime = this._structs.SYSTEMTIME(); let result = this._functions.FileTimeToSystemTime( fileTime.address(), systemTime.address() ); if (result == 0) { throw new Error(ctypes.winLastError); } // System time is in UTC, so we use Date.UTC to get milliseconds from epoch, // then divide by 1000 to get seconds, and round down: return Math.floor( Date.UTC( systemTime.wYear, systemTime.wMonth - 1, systemTime.wDay, systemTime.wHour, systemTime.wMinute, systemTime.wSecond, systemTime.wMilliseconds ) / 1000 ); }, }; function CtypesVaultHelpers() { this._structs = {}; this._functions = {}; this._structs.GUID = new ctypes.StructType("GUID", [ { id: wintypes.DWORD.array(4) }, ]); this._structs.VAULT_ITEM_ELEMENT = new ctypes.StructType( "VAULT_ITEM_ELEMENT", [ // not documented { schemaElementId: wintypes.DWORD }, // not documented { unknown1: wintypes.DWORD }, // vault type { type: wintypes.DWORD }, // not documented { unknown2: wintypes.DWORD }, // value of the item { itemValue: wintypes.LPCWSTR }, // not documented { unknown3: wintypes.CHAR.array(12) }, ] ); this._structs.VAULT_ELEMENT = new ctypes.StructType("VAULT_ELEMENT", [ // vault item schemaId { schemaId: this._structs.GUID }, // a pointer to the name of the browser VAULT_ITEM_ELEMENT { pszCredentialFriendlyName: wintypes.LPCWSTR }, // a pointer to the url VAULT_ITEM_ELEMENT { pResourceElement: this._structs.VAULT_ITEM_ELEMENT.ptr }, // a pointer to the username VAULT_ITEM_ELEMENT { pIdentityElement: this._structs.VAULT_ITEM_ELEMENT.ptr }, // not documented { pAuthenticatorElement: this._structs.VAULT_ITEM_ELEMENT.ptr }, // not documented { pPackageSid: this._structs.VAULT_ITEM_ELEMENT.ptr }, // time stamp in local format { lowLastModified: wintypes.DWORD }, { highLastModified: wintypes.DWORD }, // not documented { flags: wintypes.DWORD }, // not documented { dwPropertiesCount: wintypes.DWORD }, // not documented { pPropertyElements: this._structs.VAULT_ITEM_ELEMENT.ptr }, ]); try { this._vaultcliLib = ctypes.open("vaultcli.dll"); this._functions.VaultOpenVault = this._vaultcliLib.declare( "VaultOpenVault", ctypes.winapi_abi, wintypes.DWORD, // GUID this._structs.GUID.ptr, // Flags wintypes.DWORD, // Vault Handle wintypes.VOIDP.ptr ); this._functions.VaultEnumerateItems = this._vaultcliLib.declare( "VaultEnumerateItems", ctypes.winapi_abi, wintypes.DWORD, // Vault Handle wintypes.VOIDP, // Flags wintypes.DWORD, // Items Count wintypes.PDWORD, // Items ctypes.voidptr_t ); this._functions.VaultCloseVault = this._vaultcliLib.declare( "VaultCloseVault", ctypes.winapi_abi, wintypes.DWORD, // Vault Handle wintypes.VOIDP ); this._functions.VaultGetItem = this._vaultcliLib.declare( "VaultGetItem", ctypes.winapi_abi, wintypes.DWORD, // Vault Handle wintypes.VOIDP, // Schema Id this._structs.GUID.ptr, // Resource this._structs.VAULT_ITEM_ELEMENT.ptr, // Identity this._structs.VAULT_ITEM_ELEMENT.ptr, // Package Sid this._structs.VAULT_ITEM_ELEMENT.ptr, // HWND Owner wintypes.DWORD, // Flags wintypes.DWORD, // Items this._structs.VAULT_ELEMENT.ptr.ptr ); this._functions.VaultFree = this._vaultcliLib.declare( "VaultFree", ctypes.winapi_abi, wintypes.DWORD, // Memory this._structs.VAULT_ELEMENT.ptr ); } catch (ex) { this.finalize(); } } CtypesVaultHelpers.prototype = { /** * Must be invoked once after last use of any of the provided helpers. */ finalize() { this._structs = {}; this._functions = {}; try { this._vaultcliLib.close(); } catch (ex) {} this._vaultcliLib = null; }, }; var gEdgeDir; function getEdgeLocalDataFolder() { if (gEdgeDir) { return gEdgeDir.clone(); } let packages = Services.dirsvc.get("LocalAppData", Ci.nsIFile); packages.append("Packages"); let edgeDir = packages.clone(); edgeDir.append("Microsoft.MicrosoftEdge_8wekyb3d8bbwe"); try { if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) { gEdgeDir = edgeDir; return edgeDir.clone(); } // Let's try the long way: let dirEntries = packages.directoryEntries; while (dirEntries.hasMoreElements()) { let subDir = dirEntries.nextFile; if ( subDir.leafName.startsWith("Microsoft.MicrosoftEdge") && subDir.isReadable() && subDir.isDirectory() ) { gEdgeDir = subDir; return subDir.clone(); } } } catch (ex) { console.error( "Exception trying to find the Edge favorites directory: ", ex ); } return null; } function Bookmarks(migrationType) { this._migrationType = migrationType; } Bookmarks.prototype = { type: MigrationUtils.resourceTypes.BOOKMARKS, get exists() { return !!this._favoritesFolder; }, get importedAppLabel() { return this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE ? "IE" : "Edge"; }, __favoritesFolder: null, get _favoritesFolder() { if (!this.__favoritesFolder) { if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) { let favoritesFolder = Services.dirsvc.get("Favs", Ci.nsIFile); if (favoritesFolder.exists() && favoritesFolder.isReadable()) { this.__favoritesFolder = favoritesFolder; } } else if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) { let edgeDir = getEdgeLocalDataFolder(); if (edgeDir) { edgeDir.appendRelativePath(EDGE_FAVORITES); if ( edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory() ) { this.__favoritesFolder = edgeDir; } } } } return this.__favoritesFolder; }, __toolbarFolderName: null, get _toolbarFolderName() { if (!this.__toolbarFolderName) { if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) { // Retrieve the name of IE's favorites subfolder that holds the bookmarks // in the toolbar. This was previously stored in the registry and changed // in IE7 to always be called "Links". let folderName = lazy.WindowsRegistry.readRegKey( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, "Software\\Microsoft\\Internet Explorer\\Toolbar", "LinksFolderName" ); this.__toolbarFolderName = folderName || "Links"; } else { this.__toolbarFolderName = "Links"; } } return this.__toolbarFolderName; }, migrate: function B_migrate(aCallback) { return (async () => { // Import to the bookmarks menu. let folderGuid = lazy.PlacesUtils.bookmarks.menuGuid; await this._migrateFolder(this._favoritesFolder, folderGuid); })().then( () => aCallback(true), e => { console.error(e); aCallback(false); } ); }, async _migrateFolder(aSourceFolder, aDestFolderGuid) { let { bookmarks, favicons } = await this._getBookmarksInFolder( aSourceFolder ); if (!bookmarks.length) { return; } await MigrationUtils.insertManyBookmarksWrapper(bookmarks, aDestFolderGuid); MigrationUtils.insertManyFavicons(favicons); }, /** * Iterates through a bookmark folder to obtain whatever information from each bookmark is needed elsewhere. This function also recurses into child folders. * * @param {nsIFile} aSourceFolder the folder to search for bookmarks and subfolders. * @returns {Promise} An object with the following properties: * {Object[]} bookmarks: * An array of Objects with these properties: * {number} type: A type mapping to one of the types in nsINavBookmarksService * {string} title: The title of the bookmark * {Object[]} children: An array of objects with the same structure as this one. * * {Object[]} favicons * An array of Objects with these properties: * {Uint8Array} faviconData: The binary data of a favicon * {nsIURI} uri: The URI of the associated bookmark */ async _getBookmarksInFolder(aSourceFolder) { // TODO (bug 741993): the favorites order is stored in the Registry, at // HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites // for IE, and in a similar location for Edge. // Until we support it, bookmarks are imported in alphabetical order. let entries = aSourceFolder.directoryEntries; let rv = []; let favicons = []; while (entries.hasMoreElements()) { let entry = entries.nextFile; try { // Make sure that entry.path == entry.target to not follow .lnk folder // shortcuts which could lead to infinite cycles. // Don't use isSymlink(), since it would throw for invalid // lnk files pointing to URLs or to unresolvable paths. if (entry.path == entry.target && entry.isDirectory()) { let isBookmarksFolder = entry.leafName == this._toolbarFolderName && entry.parent.equals(this._favoritesFolder); if (isBookmarksFolder && entry.isReadable()) { // Import to the bookmarks toolbar. let folderGuid = lazy.PlacesUtils.bookmarks.toolbarGuid; await this._migrateFolder(entry, folderGuid); } else if (entry.isReadable()) { let { bookmarks: childBookmarks, favicons: childFavicons } = await this._getBookmarksInFolder(entry); favicons = favicons.concat(childFavicons); rv.push({ type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, title: entry.leafName, children: childBookmarks, }); } } else { // Strip the .url extension, to both check this is a valid link file, // and get the associated title. let matches = entry.leafName.match(/(.+)\.url$/i); if (matches) { let fileHandler = Cc[ "@mozilla.org/network/protocol;1?name=file" ].getService(Ci.nsIFileProtocolHandler); let uri = fileHandler.readURLFile(entry); // Silently failing in the event that the alternative data stream for the favicon doesn't exist try { let faviconData = await IOUtils.read(entry.path + ":favicon"); favicons.push({ faviconData, uri }); } catch {} rv.push({ url: uri, title: matches[1] }); } } } catch (ex) { console.error( "Unable to import ", this.importedAppLabel, " favorite (", entry.leafName, "): ", ex ); } } return { bookmarks: rv, favicons }; }, }; function getTypedURLs(registryKeyPath) { // The list of typed URLs is a sort of annotation stored in the registry. // The number of entries stored is not UI-configurable, but has changed // between different Windows versions. We just keep reading up to the first // non-existing entry to support different limits / states of the registry. let typedURLs = new Map(); let typedURLKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( Ci.nsIWindowsRegKey ); let typedURLTimeKey = Cc[ "@mozilla.org/windows-registry-key;1" ].createInstance(Ci.nsIWindowsRegKey); let cTypes = new CtypesKernelHelpers(); try { try { typedURLKey.open( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, registryKeyPath + "\\TypedURLs", Ci.nsIWindowsRegKey.ACCESS_READ ); } catch (ex) { // Ignore errors opening this registry key - if it doesn't work, there's // no way we can get useful info here. return typedURLs; } try { typedURLTimeKey.open( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, registryKeyPath + "\\TypedURLsTime", Ci.nsIWindowsRegKey.ACCESS_READ ); } catch (ex) { typedURLTimeKey = null; } let entryName; for ( let entry = 1; typedURLKey.hasValue((entryName = "url" + entry)); entry++ ) { let url = typedURLKey.readStringValue(entryName); // If we can't get a date for whatever reason, default to 6 months ago let timeTyped = Date.now() - 31536000 / 2; if (typedURLTimeKey && typedURLTimeKey.hasValue(entryName)) { let urlTime = ""; try { urlTime = typedURLTimeKey.readBinaryValue(entryName); } catch (ex) { console.error("Couldn't read url time for ", entryName); } if (urlTime.length == 8) { let urlTimeHex = []; for (let i = 0; i < 8; i++) { let c = urlTime.charCodeAt(i).toString(16); if (c.length == 1) { c = "0" + c; } urlTimeHex.unshift(c); } try { let hi = parseInt(urlTimeHex.slice(0, 4).join(""), 16); let lo = parseInt(urlTimeHex.slice(4, 8).join(""), 16); // Convert to seconds since epoch: let secondsSinceEpoch = cTypes.fileTimeToSecondsSinceEpoch(hi, lo); // If the date is very far in the past, just use the default if (secondsSinceEpoch > Date.now() / 1000000) { // Callers expect PRTime, which is microseconds since epoch: timeTyped = secondsSinceEpoch * 1000; } } catch (ex) { // Ignore conversion exceptions. Callers will have to deal // with the fallback value. } } } typedURLs.set(url, timeTyped * 1000); } } catch (ex) { console.error("Error reading typed URL history: ", ex); } finally { if (typedURLKey) { typedURLKey.close(); } if (typedURLTimeKey) { typedURLTimeKey.close(); } cTypes.finalize(); } return typedURLs; } // Migrator for form passwords function WindowsVaultFormPasswords() {} WindowsVaultFormPasswords.prototype = { type: MigrationUtils.resourceTypes.PASSWORDS, get exists() { // check if there are passwords available for migration. return this.migrate(() => {}, true); }, /** * If aOnlyCheckExists is false, import the form passwords from the vault * and then call the aCallback. * Otherwise, check if there are passwords in the vault. * * @param {Function} aCallback - a callback called when the migration is done. * @param {boolean} [aOnlyCheckExists=false] - if aOnlyCheckExists is true, just check if there are some * passwords to migrate. Import the passwords from the vault and call aCallback otherwise. * @returns {boolean} true if there are passwords in the vault and aOnlyCheckExists is set to true, * false if there is no password in the vault and aOnlyCheckExists is set to true, undefined if * aOnlyCheckExists is set to false. */ async migrate(aCallback, aOnlyCheckExists = false) { // check if the vault item is an IE/Edge one function _isIEOrEdgePassword(id) { return ( id[0] == INTERNET_EXPLORER_EDGE_GUID[0] && id[1] == INTERNET_EXPLORER_EDGE_GUID[1] && id[2] == INTERNET_EXPLORER_EDGE_GUID[2] && id[3] == INTERNET_EXPLORER_EDGE_GUID[3] ); } let ctypesVaultHelpers = new CtypesVaultHelpers(); let ctypesKernelHelpers = new CtypesKernelHelpers(); let migrationSucceeded = true; let successfulVaultOpen = false; let error, vault; try { // web credentials vault id let vaultGuid = new ctypesVaultHelpers._structs.GUID( WEB_CREDENTIALS_VAULT_ID ); error = new wintypes.DWORD(); // web credentials vault vault = new wintypes.VOIDP(); // open the current vault using the vaultGuid error = ctypesVaultHelpers._functions.VaultOpenVault( vaultGuid.address(), 0, vault.address() ); if (error != RESULT_SUCCESS) { throw new Error("Unable to open Vault: " + error); } successfulVaultOpen = true; let item = new ctypesVaultHelpers._structs.VAULT_ELEMENT.ptr(); let itemCount = new wintypes.DWORD(); // enumerate all the available items. This api is going to return a table of all the // available items and item is going to point to the first element of this table. error = ctypesVaultHelpers._functions.VaultEnumerateItems( vault, VAULT_ENUMERATE_ALL_ITEMS, itemCount.address(), item.address() ); if (error != RESULT_SUCCESS) { throw new Error("Unable to enumerate Vault items: " + error); } let logins = []; for (let j = 0; j < itemCount.value; j++) { try { // if it's not an ie/edge password, skip it if (!_isIEOrEdgePassword(item.contents.schemaId.id)) { continue; } let url = item.contents.pResourceElement.contents.itemValue.readString(); let realURL; try { realURL = Services.io.newURI(url); } catch (ex) { /* leave realURL as null */ } if (!realURL || !["http", "https", "ftp"].includes(realURL.scheme)) { // Ignore items for non-URLs or URLs that aren't HTTP(S)/FTP continue; } // if aOnlyCheckExists is set to true, the purpose of the call is to return true if there is at // least a password which is true in this case because a password was by now already found if (aOnlyCheckExists) { return true; } let username = item.contents.pIdentityElement.contents.itemValue.readString(); // the current login credential object let credential = new ctypesVaultHelpers._structs.VAULT_ELEMENT.ptr(); error = ctypesVaultHelpers._functions.VaultGetItem( vault, item.contents.schemaId.address(), item.contents.pResourceElement, item.contents.pIdentityElement, null, 0, 0, credential.address() ); if (error != RESULT_SUCCESS) { throw new Error("Unable to get item: " + error); } let password = credential.contents.pAuthenticatorElement.contents.itemValue.readString(); let creation = Date.now(); try { // login manager wants time in milliseconds since epoch, so convert // to seconds since epoch and multiply to get milliseconds: creation = ctypesKernelHelpers.fileTimeToSecondsSinceEpoch( item.contents.highLastModified, item.contents.lowLastModified ) * 1000; } catch (ex) { // Ignore exceptions in the dates and just create the login for right now. } // create a new login logins.push({ username, password, origin: realURL.prePath, timeCreated: creation, }); // close current item error = ctypesVaultHelpers._functions.VaultFree(credential); if (error == FREE_CLOSE_FAILED) { throw new Error("Unable to free item: " + error); } } catch (e) { migrationSucceeded = false; console.error(e); } finally { // move to next item in the table returned by VaultEnumerateItems item = item.increment(); } } if (logins.length) { await MigrationUtils.insertLoginsWrapper(logins); } } catch (e) { console.error(e); migrationSucceeded = false; } finally { if (successfulVaultOpen) { // close current vault error = ctypesVaultHelpers._functions.VaultCloseVault(vault); if (error == FREE_CLOSE_FAILED) { console.error("Unable to close vault: ", error); } } ctypesKernelHelpers.finalize(); ctypesVaultHelpers.finalize(); aCallback(migrationSucceeded); } if (aOnlyCheckExists) { return false; } return undefined; }, }; export var MSMigrationUtils = { MIGRATION_TYPE_IE: 1, MIGRATION_TYPE_EDGE: 2, CtypesKernelHelpers, getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) { return new Bookmarks(migrationType); }, getWindowsVaultFormPasswordsMigrator() { return new WindowsVaultFormPasswords(); }, getTypedURLs, getEdgeLocalDataFolder, };