Source code
Revision control
Copy as Markdown
Other Tools
/* 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,
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,
};
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).catch(console.error);
},
/**
* 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<object>} 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) {
// 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,
};