Bug 682069 - Password Import from IE not available. r=dolske r=mattn

This commit is contained in:
Riadh Chtara 2015-09-01 17:27:04 -07:00
parent ecb2f187fc
commit 1436f2c0ee
9 changed files with 927 additions and 85 deletions

View File

@ -390,7 +390,9 @@ function GetWindowsPasswordsResource(aProfileFolder) {
_rowToLoginInfo(row) {
let loginInfo = {
username: row.getResultByName("username_value"),
password: crypto.decryptData(row.getResultByName("password_value")),
password: crypto.
decryptData(crypto.arrayToString(row.getResultByName("password_value")),
null),
hostName: NetUtil.newURI(row.getResultByName("origin_url")).prePath,
submitURL: null,
httpRealm: null,

View File

@ -9,6 +9,7 @@ const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const kLoginsKey = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
const kMainKey = "Software\\Microsoft\\Internet Explorer\\Main";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -17,11 +18,17 @@ Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/MigrationUtils.jsm");
Cu.import("resource:///modules/MSMigrationUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
"resource://gre/modules/ctypes.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
"resource://gre/modules/OSCrypto.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
"resource://gre/modules/WindowsRegistry.jsm");
Cu.importGlobalProperties(["URL"]);
////////////////////////////////////////////////////////////////////////////////
//// Resources
@ -116,6 +123,270 @@ History.prototype = {
}
};
// IE password migrator supporting windows from XP until 7 and IE from 7 until 11
function IE7FormPasswords () {
}
IE7FormPasswords.prototype = {
type: MigrationUtils.resourceTypes.PASSWORDS,
get exists() {
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;
}
},
migrate(aCallback) {
let historyEnumerator = Cc["@mozilla.org/profile/migrator/iehistoryenumerator;1"].
createInstance(Ci.nsISimpleEnumerator);
let uris = []; // the uris of the websites that are going to be migrated
while (historyEnumerator.hasMoreElements()) {
let entry = historyEnumerator.getNext().QueryInterface(Ci.nsIPropertyBag2);
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"].indexOf(uri.scheme) == -1) {
continue;
}
uris.push(uri);
}
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.
*/
_migrateURIs(uris) {
this.ctypesHelpers = new MSMigrationUtils.CtypesHelpers();
this._crypto = new 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.
*/
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, true);
} 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++;
}
this._addLogins(ieLogins);
} catch (e) {
Cu.reportError("Error while importing logins for " + uri.spec + ": " + e);
}
}
// 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) {
Cu.reportError("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.ctypesHelpers.finalize();
},
_crypto: null,
/**
* Add the logins to the password manager.
* @param {Object[]} logins - array of the login details.
*/
_addLogins(ieLogins) {
function addLogin(login, existingLogins) {
// Add the login only if it doesn't already exist
// if the login is not already available, it s going to be added or merged with another
// login
if (existingLogins.some(l => login.matches(l, true))) {
return;
}
let isUpdate = false; // the login is just an update for an old one
for (let existingLogin of existingLogins) {
if (login.username == existingLogin.username && login.password != existingLogin.password) {
// if a login with the same username and different password already exists and it's older
// than the current one, that login needs to be updated using the current one details
if (login.timePasswordChanged > existingLogin.timePasswordChanged) {
// Bug 1187190: Password changes should be propagated depending on timestamps.
// the existing login password and timestamps should be updated
let propBag = Cc["@mozilla.org/hash-property-bag;1"].
createInstance(Ci.nsIWritablePropertyBag);
propBag.setProperty("password", login.password);
propBag.setProperty("timePasswordChanged", login.timePasswordChanged);
Services.logins.modifyLogin(existingLogin, propBag);
// make sure not to add the new login
isUpdate = true;
}
}
}
// if the new login is not an update, add it.
if (!isUpdate) {
Services.logins.addLogin(login);
}
}
for (let ieLogin of ieLogins) {
try {
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
login.init(ieLogin.url, "", null,
ieLogin.username, ieLogin.password, "", "");
login.QueryInterface(Ci.nsILoginMetaInfo);
login.timeCreated = ieLogin.creation;
login.timeLastUsed = ieLogin.creation;
login.timePasswordChanged = ieLogin.creation;
// login.timesUsed is going to set to the default value 1
// Add the login only if there's not an existing entry
let existingLogins = Services.logins.findLogins({}, login.hostname, "", null);
addLogin(login, existingLogins);
} catch (e) {
Cu.reportError(e);
}
}
},
/**
* 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 ctypes.StructType("loginData", [
// Bytes 0-3 are not needed and not documented
{"unknown1": ctypes.uint32_t},
// Bytes 4-7 are the header size
{"headerSize": ctypes.uint32_t},
// Bytes 8-11 are the data size
{"dataSize": ctypes.uint32_t},
// Bytes 12-19 are not needed and not documented
{"unknown2": ctypes.uint32_t},
{"unknown3": ctypes.uint32_t},
// Bytes 20-23 are the data count: each username and password is considered as a data
{"dataMax": ctypes.uint32_t},
// Bytes 24-35 are not needed and not documented
{"unknown4": ctypes.uint32_t},
{"unknown5": ctypes.uint32_t},
{"unknown6": ctypes.uint32_t}
]);
// the structure of a IE7 decrypted login item
let loginItem = new ctypes.StructType("loginItem", [
// Bytes 0-3 are the offset of the username
{"usernameOffset": ctypes.uint32_t},
// Bytes 4-11 are the date
{"loDateTime": ctypes.uint32_t},
{"hiDateTime": ctypes.uint32_t},
// Bytes 12-15 are not needed and not documented
{"foo": ctypes.uint32_t},
// Bytes 16-19 are the offset of the password
{"passwordOffset": ctypes.uint32_t},
// Bytes 20-31 are not needed and not documented
{"unknown1": ctypes.uint32_t},
{"unknown2": ctypes.uint32_t},
{"unknown3": 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 = 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 = ctypes.cast(cdata, loginData);
let headerSize = currentLoginData.headerSize;
let currentInfoIndex = loginData.size;
// pointer to the current login item
let currentLoginItemPointer = 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.ctypesHelpers.
fileTimeToSecondsSinceEpoch(currentLoginItem.hiDateTime,
currentLoginItem.loDateTime) * 1000;
let currentResult = {
creation: creation,
url: url,
};
// The username is UTF-16 and null-terminated.
currentResult.username =
ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.usernameOffset),
ctypes.char16_t.ptr).readString();
// The password is UTF-16 and null-terminated.
currentResult.password =
ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.passwordOffset),
ctypes.char16_t.ptr).readString();
results.push(currentResult);
// move to the next login item
currentLoginItemPointer = currentLoginItemPointer.increment();
}
return results;
},
};
function Settings() {
}
@ -243,6 +514,7 @@ Settings.prototype = {
function IEProfileMigrator()
{
this.wrappedJSObject = this; // export this to be able to use it in the unittest.
}
IEProfileMigrator.prototype = Object.create(MigratorPrototype);
@ -252,6 +524,7 @@ IEProfileMigrator.prototype.getResources = function IE_getResources() {
MSMigrationUtils.getBookmarksMigrator()
, new History()
, MSMigrationUtils.getCookiesMigrator()
, new IE7FormPasswords()
, new Settings()
];
return [r for each (r in resources) if (r.exists)];

View File

@ -30,48 +30,45 @@ Cu.importGlobalProperties(["File"]);
////////////////////////////////////////////////////////////////////////////////
//// Helpers.
let CtypesHelpers = {
_structs: {},
_functions: {},
_libs: {},
function CtypesHelpers() {
this._structs = {};
this._functions = {};
this._libs = {};
/**
* Must be invoked once before first use of any of the provided helpers.
*/
initialize() {
const WORD = ctypes.uint16_t;
const DWORD = ctypes.uint32_t;
const BOOL = ctypes.int;
const WORD = ctypes.uint16_t;
const DWORD = ctypes.uint32_t;
const BOOL = ctypes.int;
this._structs.SYSTEMTIME = new ctypes.StructType('SYSTEMTIME', [
{wYear: WORD},
{wMonth: WORD},
{wDayOfWeek: WORD},
{wDay: WORD},
{wHour: WORD},
{wMinute: WORD},
{wSecond: WORD},
{wMilliseconds: WORD}
]);
this._structs.SYSTEMTIME = new ctypes.StructType('SYSTEMTIME', [
{wYear: WORD},
{wMonth: WORD},
{wDayOfWeek: WORD},
{wDay: WORD},
{wHour: WORD},
{wMinute: WORD},
{wSecond: WORD},
{wMilliseconds: WORD}
]);
this._structs.FILETIME = new ctypes.StructType('FILETIME', [
{dwLowDateTime: DWORD},
{dwHighDateTime: DWORD}
]);
this._structs.FILETIME = new ctypes.StructType('FILETIME', [
{dwLowDateTime: DWORD},
{dwHighDateTime: DWORD}
]);
try {
this._libs.kernel32 = ctypes.open("Kernel32");
this._functions.FileTimeToSystemTime =
this._libs.kernel32.declare("FileTimeToSystemTime",
ctypes.default_abi,
BOOL,
this._structs.FILETIME.ptr,
this._structs.SYSTEMTIME.ptr);
} catch (ex) {
this.finalize();
}
},
try {
this._libs.kernel32 = ctypes.open("Kernel32");
this._functions.FileTimeToSystemTime =
this._libs.kernel32.declare("FileTimeToSystemTime",
ctypes.default_abi,
BOOL,
this._structs.FILETIME.ptr,
this._structs.SYSTEMTIME.ptr);
} catch (ex) {
this.finalize();
}
}
CtypesHelpers.prototype = {
/**
* Must be invoked once after last use of any of the provided helpers.
*/
@ -433,7 +430,7 @@ Cookies.prototype = {
},
migrate(aCallback) {
CtypesHelpers.initialize();
this.ctypesHelpers = new CtypesHelpers();
let cookiesGenerator = (function genCookie() {
let success = false;
@ -460,7 +457,7 @@ Cookies.prototype = {
}
}
CtypesHelpers.finalize();
this.ctypesHelpers.finalize();
aCallback(success);
}).apply(this);
@ -536,8 +533,8 @@ Cookies.prototype = {
host = "." + host;
}
let expireTime = CtypesHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
Number(expireTimeLo));
let expireTime = this.ctypesHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
Number(expireTimeLo));
Services.cookies.add(host,
path,
name,
@ -554,6 +551,7 @@ Cookies.prototype = {
let MSMigrationUtils = {
MIGRATION_TYPE_IE: 1,
MIGRATION_TYPE_EDGE: 2,
CtypesHelpers: CtypesHelpers,
getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) {
return new Bookmarks(migrationType);
},

View File

@ -84,7 +84,7 @@ function promiseSetPassword(login) {
SET password_value = :password_value
WHERE rowid = :rowid
`);
let passwordValue = crypto.encryptData(login.password);
let passwordValue = crypto.stringToArray(crypto.encryptData(login.password));
stmt.bindBlobByName("password_value", passwordValue, passwordValue.length);
stmt.params.rowid = login.id;

View File

@ -0,0 +1,386 @@
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
"resource://gre/modules/WindowsRegistry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
"resource://gre/modules/OSCrypto.jsm");
const CRYPT_PROTECT_UI_FORBIDDEN = 1;
const LOGINS_KEY = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
const EXTENSION = "-backup";
const TESTED_WEBSITES = {
twitter: {
uri: makeURI("https://twitter.com"),
hash: "A89D42BC6406E27265B1AD0782B6F376375764A301",
data: [12, 0, 0, 0, 56, 0, 0, 0, 38, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 8, 0, 0, 0, 18, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 9, 0, 0, 0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0, 103, 0, 104, 0, 0, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 0, 0, 0],
logins: [
{
username: "abcdefgh",
password: "123456789",
hostname: "https://twitter.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439325854000,
timeLastUsed: 1439325854000,
timePasswordChanged: 1439325854000,
timesUsed: 1,
},
],
},
facebook: {
uri: makeURI("https://www.facebook.com/"),
hash: "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796",
data: [12, 0, 0, 0, 152, 0, 0, 0, 160, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 40, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 60, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 80, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 100, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 120, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 140, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 48, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 48, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 49, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 49, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 50, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 50, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 51, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 51, 0, 0, 0],
logins: [
{
username: "username0",
password: "password0",
hostname: "https://www.facebook.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439326966000,
timeLastUsed: 1439326966000,
timePasswordChanged: 1439326966000,
timesUsed: 1,
},
{
username: "username1",
password: "password1",
hostname: "https://www.facebook.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439326997000,
timeLastUsed: 1439326997000,
timePasswordChanged: 1439326997000,
timesUsed: 1,
},
{
username: "username2",
password: "password2",
hostname: "https://www.facebook.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439327040000,
timeLastUsed: 1439327040000,
timePasswordChanged: 1439327040000,
timesUsed: 1,
},
{
username: "username3",
password: "password3",
hostname: "https://www.facebook.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439327070000,
timeLastUsed: 1439327070000,
timePasswordChanged: 1439327070000,
timesUsed: 1,
},
],
},
live: {
uri: makeURI("https://login.live.com/"),
hash: "7B506F2D6B81D939A8E0456F036EE8970856FF705E",
data: [12, 0, 0, 0, 56, 0, 0, 0, 44, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 11, 0, 0, 0, 114, 0, 105, 0, 97, 0, 100, 0, 104, 0, 49, 6, 74, 6, 39, 6, 54, 6, 0, 0, 39, 6, 66, 6, 49, 6, 35, 6, 80, 0, 192, 0, 223, 0, 119, 0, 246, 0, 114, 0, 100, 0, 0, 0],
logins: [
{
username: "riadhرياض",
password: "اقرأPÀßwörd",
hostname: "https://login.live.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439338767000,
timeLastUsed: 1439338767000,
timePasswordChanged: 1439338767000,
timesUsed: 1,
},
],
},
reddit: {
uri: makeURI("http://www.reddit.com/"),
hash: "B644028D1C109A91EC2C4B9D1F145E55A1FAE42065",
data: [12, 0, 0, 0, 152, 0, 0, 0, 212, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 8, 234, 114, 153, 212, 208, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 3, 0, 0, 0, 14, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 16, 0, 0, 0, 48, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 4, 0, 0, 0, 58, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 29, 0, 0, 0, 118, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 15, 0, 0, 0, 150, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 30, 0, 0, 0, 97, 0, 0, 0, 0, 0, 252, 140, 173, 138, 146, 48, 0, 0, 66, 0, 105, 0, 116, 0, 116, 0, 101, 0, 32, 0, 98, 0, 101, 0, 115, 0, 116, 0, 228, 0, 116, 0, 105, 0, 103, 0, 101, 0, 110, 0, 0, 0, 205, 145, 110, 127, 198, 91, 1, 120, 0, 0, 31, 4, 48, 4, 64, 4, 62, 4, 59, 4, 76, 4, 32, 0, 67, 4, 65, 4, 63, 4, 53, 4, 72, 4, 61, 4, 62, 4, 32, 0, 65, 4, 49, 4, 64, 4, 62, 4, 72, 4, 53, 4, 61, 4, 46, 0, 32, 0, 18, 4, 62, 4, 57, 4, 66, 4, 56, 4, 0, 0, 40, 6, 51, 6, 69, 6, 32, 0, 39, 6, 68, 6, 68, 6, 71, 6, 32, 0, 39, 6, 68, 6, 49, 6, 45, 6, 69, 6, 70, 6, 0, 0, 118, 0, 101, 0, 117, 0, 105, 0, 108, 0, 108, 0, 101, 0, 122, 0, 32, 0, 108, 0, 101, 0, 32, 0, 118, 0, 233, 0, 114, 0, 105, 0, 102, 0, 105, 0, 101, 0, 114, 0, 32, 0, 224, 0, 32, 0, 110, 0, 111, 0, 117, 0, 118, 0, 101, 0, 97, 0, 117, 0, 0, 0],
logins: [
{
username: "購読を",
password: "Bitte bestätigen",
hostname: "http://www.reddit.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439340874000,
timeLastUsed: 1439340874000,
timePasswordChanged: 1439340874000,
timesUsed: 1,
},
{
username: "重置密码",
password: "Пароль успешно сброшен. Войти",
hostname: "http://www.reddit.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439340971000,
timeLastUsed: 1439340971000,
timePasswordChanged: 1439340971000,
timesUsed: 1,
},
{
username: "بسم الله الرحمن",
password: "veuillez le vérifier à nouveau",
hostname: "http://www.reddit.com",
formSubmitURL: "",
httpRealm: null,
usernameField: "",
passwordField: "",
timeCreated: 1439341166000,
timeLastUsed: 1439341166000,
timePasswordChanged: 1439341166000,
timesUsed: 1,
},
],
},
};
const TESTED_URLS = [
"http://a.foo.com",
"http://b.foo.com",
"http://c.foo.com",
"http://www.test.net",
"http://www.test.net/home",
"http://www.test.net/index",
"https://a.bar.com",
"https://b.bar.com",
"https://c.bar.com",
];
let nsIWindowsRegKey = Ci.nsIWindowsRegKey;
let Storage2Key;
/*
* If the key value exists, it's going to be backed up and replaced, so the value could be restored.
* Otherwise a new value is going to be created.
*/
function backupAndStore(key, name, value) {
if (key.hasValue(name)) {
// backup the the current value
let type = key.getValueType(name);
// create a new value using use the current value name followed by EXTENSION as its new name
switch (type) {
case nsIWindowsRegKey.TYPE_STRING:
key.writeStringValue(name + EXTENSION, key.readStringValue(name));
break;
case nsIWindowsRegKey.TYPE_BINARY:
key.writeBinaryValue(name + EXTENSION, key.readBinaryValue(name));
break;
case nsIWindowsRegKey.TYPE_INT:
key.writeIntValue(name + EXTENSION, key.readIntValue(name));
break;
case nsIWindowsRegKey.TYPE_INT64:
key.writeInt64Value(name + EXTENSION, key.readInt64Value(name));
break;
}
}
key.writeBinaryValue(name, value);
}
// Remove all values where their names are members of the names array from the key of registry
function removeAllValues(key, names) {
for (let name of names) {
key.removeValue(name);
}
}
// Restore all the backed up values
function restore(key) {
let count = key.valueCount;
let names = []; // the names of the key values
for (let i = 0; i < count; ++i) {
names.push(key.getValueName(i));
}
for (let name of names) {
// backed up values have EXTENSION at the end of their names
if (name.lastIndexOf(EXTENSION) == name.length - EXTENSION.length) {
let valueName = name.substr(0, name.length - EXTENSION.length);
let type = key.getValueType(name);
// create a new value using the name before the backup and removed the backed up one
switch (type) {
case nsIWindowsRegKey.TYPE_STRING:
key.writeStringValue(valueName, key.readStringValue(name));
key.removeValue(name);
break;
case nsIWindowsRegKey.TYPE_BINARY:
key.writeBinaryValue(valueName, key.readBinaryValue(name));
key.removeValue(name);
break;
case nsIWindowsRegKey.TYPE_INT:
key.writeIntValue(valueName, key.readIntValue(name));
key.removeValue(name);
break;
case nsIWindowsRegKey.TYPE_INT64:
key.writeInt64Value(valueName, key.readInt64Value(name));
key.removeValue(name);
break;
}
}
}
}
function checkLoginsAreEqual(passwordManagerLogin, IELogin, id) {
passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
for (let attribute in IELogin) {
Assert.equal(passwordManagerLogin[attribute], IELogin[attribute],
"The two logins ID " + id + " have the same " + attribute);
}
}
function createRegistryPath(path) {
let loginPath = path.split("\\");
let parentKey = Cc["@mozilla.org/windows-registry-key;1"].
createInstance(nsIWindowsRegKey);
let currentPath =[];
for (let currentKey of loginPath) {
parentKey.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, currentPath.join("\\"),
nsIWindowsRegKey.ACCESS_ALL);
if (!parentKey.hasChild(currentKey)) {
parentKey.createChild(currentKey, 0);
}
currentPath.push(currentKey);
parentKey.close();
}
}
function getFirstResourceOfType(type) {
let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=ie"]
.createInstance(Ci.nsISupports)
.wrappedJSObject;
let migrators = migrator.getResources();
for (let m of migrators) {
if (m.type == type) {
return m;
}
}
throw new Error("failed to find the " + type + " migrator");
}
function makeURI(aURL) {
return Services.io.newURI(aURL, null, null);
}
add_task(function* setup() {
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
Assert.throws(() => getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS),
"The migrator doesn't exist for win8+");
return;
}
// create the path to Storage2 in the registry if it doest exist.
createRegistryPath(LOGINS_KEY);
Storage2Key = Cc["@mozilla.org/windows-registry-key;1"].
createInstance(nsIWindowsRegKey);
Storage2Key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, LOGINS_KEY,
nsIWindowsRegKey.ACCESS_ALL);
// create a dummy value otherwise the migrator doesn't exist
if (!Storage2Key.hasValue("dummy")) {
Storage2Key.writeBinaryValue("dummy", "dummy");
}
});
add_task(function* test_passwordsNotAvailable() {
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
return;
}
let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
Assert.ok(migrator.exists, "The migrator has to exist");
let logins = Services.logins.getAllLogins({});
Assert.equal(logins.length, 0, "There are no logins at the beginning of the test");
let uris = []; // the uris of the migrated logins
for (let url of TESTED_URLS) {
uris.push(makeURI(url));
// in this test, there is no IE login data in the registry, so after the migration, the number
// of logins in the store should be 0
migrator._migrateURIs(uris);
logins = Services.logins.getAllLogins({});
Assert.equal(logins.length, 0,
"There are no logins after doing the migration without adding values to the registry");
}
});
add_task(function* test_passwordsAvailable() {
if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
return;
}
let crypto = new OSCrypto();
let hashes = []; // the hashes of all migrator websites, this is going to be used for the clean up
do_register_cleanup(() => {
Services.logins.removeAllLogins();
logins = Services.logins.getAllLogins({});
Assert.equal(logins.length, 0, "There are no logins after the cleanup");
//remove all the values created in this test from the registry
removeAllValues(Storage2Key, hashes);
// restore all backed up values
restore(Storage2Key);
// clean the dummy value
if (Storage2Key.hasValue("dummy")) {
Storage2Key.removeValue("dummy");
}
Storage2Key.close();
crypto.finalize();
});
let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
Assert.ok(migrator.exists, "The migrator has to exist");
let logins = Services.logins.getAllLogins({});
Assert.equal(logins.length, 0, "There are no logins at the beginning of the test");
let uris = []; // the uris of the migrated logins
let loginCount = 0;
for (let current in TESTED_WEBSITES) {
let website = TESTED_WEBSITES[current];
// backup the current the registry value if it exists and replace the existing value/create a
// new value with the encrypted data
backupAndStore(Storage2Key, website.hash,
crypto.encryptData(crypto.arrayToString(website.data),
website.uri.spec, true));
Assert.ok(migrator.exists, "The migrator has to exist");
uris.push(website.uri);
hashes.push(website.hash);
migrator._migrateURIs(uris);
logins = Services.logins.getAllLogins({});
// check that the number of logins in the password manager has increased as expected which means
// that all the values for the current website were imported
loginCount += website.logins.length;
Assert.equal(logins.length, loginCount,
"The number of logins has increased after the migration");
let startIndex = loginCount - website.logins.length;
// compares the imported password manager logins with their expected logins
for (let i = 0; i < website.logins.length; i++) {
checkLoginsAreEqual(logins[startIndex + i], website.logins[i],
" " + current + " - " + i + " ");
}
}
});

View File

@ -17,5 +17,7 @@ skip-if = os != "win"
skip-if = os != "win"
[test_IE_cookies.js]
skip-if = os != "win"
[test_IE7_passwords.js]
skip-if = os != "win"
[test_Safari_bookmarks.js]
skip-if = os != "mac"

View File

@ -11,14 +11,27 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ctypes", "resource://gre/modules/ctypes.jsm");
const FLAGS_NOT_SET = 0;
const wintypes = {
BOOL: ctypes.bool,
BYTE: ctypes.uint8_t,
DWORD: ctypes.uint32_t,
PBYTE: ctypes.unsigned_char.ptr,
PCHAR: ctypes.char.ptr,
PDWORD: ctypes.uint32_t.ptr,
PVOID: ctypes.voidptr_t,
WORD: ctypes.uint16_t,
}
function OSCrypto() {
this._structs = {};
this._functions = new Map();
this._libs = new Map();
this._structs.DATA_BLOB = new ctypes.StructType("DATA_BLOB",
[
{cbData: ctypes.uint32_t},
{pbData: ctypes.uint8_t.ptr}
{cbData: wintypes.DWORD},
{pbData: wintypes.PVOID}
]);
try {
@ -29,31 +42,30 @@ function OSCrypto() {
this._functions.set("CryptProtectData",
this._libs.get("crypt32").declare("CryptProtectData",
ctypes.winapi_abi,
ctypes.uint32_t,
wintypes.DWORD,
this._structs.DATA_BLOB.ptr,
ctypes.voidptr_t,
ctypes.voidptr_t,
ctypes.voidptr_t,
ctypes.voidptr_t,
ctypes.uint32_t,
wintypes.PVOID,
wintypes.PVOID,
wintypes.PVOID,
wintypes.PVOID,
wintypes.DWORD,
this._structs.DATA_BLOB.ptr));
this._functions.set("CryptUnprotectData",
this._libs.get("crypt32").declare("CryptUnprotectData",
ctypes.winapi_abi,
ctypes.uint32_t,
wintypes.DWORD,
this._structs.DATA_BLOB.ptr,
ctypes.voidptr_t,
ctypes.voidptr_t,
ctypes.voidptr_t,
ctypes.voidptr_t,
ctypes.uint32_t,
wintypes.PVOID,
wintypes.PVOID,
wintypes.PVOID,
wintypes.PVOID,
wintypes.DWORD,
this._structs.DATA_BLOB.ptr));
this._functions.set("LocalFree",
this._libs.get("kernel32").declare("LocalFree",
ctypes.winapi_abi,
ctypes.uint32_t,
ctypes.voidptr_t));
wintypes.DWORD,
wintypes.PVOID));
} catch (ex) {
Cu.reportError(ex);
this.finalize();
@ -62,18 +74,101 @@ function OSCrypto() {
}
OSCrypto.prototype = {
/**
* Decrypt an array of numbers using the windows CryptUnprotectData API.
* @param {number[]} array - the encrypted array that needs to be decrypted.
* @returns {string} the decryption of the array.
* Convert an array containing only two bytes unsigned numbers to a string.
* @param {number[]} arr - the array that needs to be converted.
* @returns {string} the string representation of the array.
*/
decryptData(array) {
arrayToString(arr) {
let str = "";
for (let i = 0; i < arr.length; i++) {
str += String.fromCharCode(arr[i]);
}
return str;
},
/**
* Convert a string to an array.
* @param {string} str - the string that needs to be converted.
* @returns {number[]} the array representation of the string.
*/
stringToArray(str) {
let arr = [];
for (let i = 0; i < str.length; i++) {
arr.push(str.charCodeAt(i));
}
return arr;
},
/**
* Calculate the hash value used by IE as the name of the registry value where login details are
* stored.
* @param {string} data - the string value that needs to be hashed.
* @returns {string} the hash value of the string.
*/
getIELoginHash(data) {
// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
return ("00" + charCode.toString(16)).slice(-2);
}
// the data needs to be encoded in null terminated UTF-16
data += "\0";
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-16";
// result is an out parameter,
// result.value will contain the array length
let result = {};
// dataArray is an array of bytes
let dataArray = converter.convertToByteArray(data, result);
// calculation of SHA1 hash value
let cryptoHash = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);
cryptoHash.init(cryptoHash.SHA1);
cryptoHash.update(dataArray, dataArray.length);
let hash = cryptoHash.finish(false);
let tail = 0; // variable to calculate value for the last 2 bytes
// convert to a character string in hexadecimal notation
for (let c of hash) {
tail += c.charCodeAt(0);
}
hash += String.fromCharCode(tail % 256);
// convert the binary hash data to a hex string.
let hashStr = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
return hashStr.toUpperCase();
},
/**
* Decrypt a string using the windows CryptUnprotectData API.
* @param {string} data - the encrypted string that needs to be decrypted.
* @param {?string} entropy - the entropy value of the decryption (could be null). Its value must
* be the same as the one used when the data was encrypted.
* @returns {string} the decryption of the string.
*/
decryptData(data, entropy = null) {
let array = this.stringToArray(data);
let decryptedData = "";
let encryptedData = ctypes.uint8_t.array(array.length)(array);
let encryptedData = wintypes.BYTE.array(array.length)(array);
let inData = new this._structs.DATA_BLOB(encryptedData.length, encryptedData);
let outData = new this._structs.DATA_BLOB();
let entropyParam;
if (entropy) {
let entropyArray = this.stringToArray(entropy);
entropyArray.push(0);
let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
entropyData);
entropyParam = optionalEntropy.address();
} else {
entropyParam = null;
}
let status = this._functions.get("CryptUnprotectData")(inData.address(), null,
null, null, null, 0,
outData.address());
entropyParam,
null, null, FLAGS_NOT_SET,
outData.address());
if (status === 0) {
throw new Error("decryptData failed: " + status);
}
@ -81,32 +176,43 @@ OSCrypto.prototype = {
// convert byte array to JS string.
let len = outData.cbData;
let decrypted = ctypes.cast(outData.pbData,
ctypes.uint8_t.array(len).ptr).contents;
wintypes.BYTE.array(len).ptr).contents;
for (let i = 0; i < decrypted.length; i++) {
decryptedData += String.fromCharCode(decrypted[i]);
}
this._functions.get("LocalFree")(outData.pbData);
return decryptedData;
},
},
/**
* Encrypt a string using the windows CryptProtectData API.
* @param {string} string - the string that is going to be encrypted.
* @returns {number[]} the encrypted string encoded as an array of numbers.
* @param {string} data - the string that is going to be encrypted.
* @param {?string} entropy - the entropy value of the encryption (could be null). Its value must
* be the same as the one that is going to be used for the decryption.
* @returns {string} the encrypted string.
*/
encryptData(string) {
let encryptedData = [];
let decryptedData = ctypes.uint8_t.array(string.length)();
encryptData(data, entropy = null) {
let encryptedData = "";
let decryptedData = wintypes.BYTE.array(data.length)(this.stringToArray(data));
for (let i = 0; i < string.length; i++) {
decryptedData[i] = string.charCodeAt(i);
let inData = new this._structs.DATA_BLOB(data.length, decryptedData);
let outData = new this._structs.DATA_BLOB();
let entropyParam;
if (!entropy) {
entropyParam = null;
} else {
let entropyArray = this.stringToArray(entropy);
entropyArray.push(0);
let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
entropyData);
entropyParam = optionalEntropy.address();
}
let inData = new this._structs.DATA_BLOB(string.length, decryptedData);
let outData = new this._structs.DATA_BLOB();
let status = this._functions.get("CryptProtectData")(inData.address(), null,
null, null, null, 0,
entropyParam,
null, null, FLAGS_NOT_SET,
outData.address());
if (status === 0) {
throw new Error("encryptData failed: " + status);
@ -115,12 +221,8 @@ OSCrypto.prototype = {
// convert byte array to JS string.
let len = outData.cbData;
let encrypted = ctypes.cast(outData.pbData,
ctypes.uint8_t.array(len).ptr).contents;
for (let i = 0; i < len; i++) {
encryptedData.push(encrypted[i]);
}
wintypes.BYTE.array(len).ptr).contents;
encryptedData = this.arrayToString(encrypted);
this._functions.get("LocalFree")(outData.pbData);
return encryptedData;
},

View File

@ -0,0 +1,77 @@
/**
* Tests the OSCrypto object.
*/
"use strict";
////////////////////////////////////////////////////////////////////////////////
//// Globals
XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
"resource://gre/modules/OSCrypto.jsm");
let crypto = new OSCrypto();
////////////////////////////////////////////////////////////////////////////////
//// Tests
add_task(function test_getIELoginHash()
{
do_check_eq(crypto.getIELoginHash("https://bugzilla.mozilla.org/page.cgi"),
"4A66FE96607885790F8E67B56EEE52AB539BAFB47D");
do_check_eq(crypto.getIELoginHash("https://github.com/login"),
"0112F7DCE67B8579EA01367678AA44AB9868B5A143");
do_check_eq(crypto.getIELoginHash("https://login.live.com/login.srf"),
"FBF92E5D804C82717A57856533B779676D92903688");
do_check_eq(crypto.getIELoginHash("https://preview.c9.io/riadh/w1/pass.1.html"),
"6935CF27628830605927F86AB53831016FC8973D1A");
do_check_eq(crypto.getIELoginHash("https://reviewboard.mozilla.org/account/login/"),
"09141FD287E2E59A8B1D3BB5671537FD3D6B61337A");
do_check_eq(crypto.getIELoginHash("https://www.facebook.com/"),
"EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796");
});
add_task(function test_decryptData_encryptData()
{
function decryptEncryptTest(key) {
do_check_eq(crypto.decryptData(crypto.encryptData("", key), key),
"");
do_check_eq(crypto.decryptData(crypto.encryptData("secret", key), key),
"secret");
do_check_eq(crypto.decryptData(crypto.encryptData("https://www.mozilla.org", key),
key),
"https://www.mozilla.org");
do_check_eq(crypto.decryptData(crypto.encryptData("https://reviewboard.mozilla.org", key),
key),
"https://reviewboard.mozilla.org");
do_check_eq(crypto.decryptData(crypto.encryptData("https://bugzilla.mozilla.org/page.cgi",
key),
key),
"https://bugzilla.mozilla.org/page.cgi");
}
let keys = [null, "a", "keys", "abcdedf", "pass", "https://bugzilla.mozilla.org/page.cgi",
"https://login.live.com/login.srf"];
for (let key of keys) {
decryptEncryptTest(key);
}
let url = "https://twitter.com/";
let value = [1, 0, 0, 0, 208, 140, 157, 223, 1, 21, 209, 17, 140, 122, 0, 192, 79, 194, 151, 235, 1, 0, 0, 0, 254, 58, 230, 75, 132, 228, 181, 79, 184, 160, 37, 106, 201, 29, 42, 152, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 16, 102, 0, 0, 0, 1, 0, 0, 32, 0, 0, 0, 90, 136, 17, 124, 122, 57, 178, 24, 34, 86, 209, 198, 184, 107, 58, 58, 32, 98, 61, 239, 129, 101, 56, 239, 114, 159, 139, 165, 183, 40, 183, 85, 0, 0, 0, 0, 14, 128, 0, 0, 0, 2, 0, 0, 32, 0, 0, 0, 147, 170, 34, 21, 53, 227, 191, 6, 201, 84, 106, 31, 57, 227, 46, 127, 219, 199, 80, 142, 37, 104, 112, 223, 26, 165, 223, 55, 176, 89, 55, 37, 112, 0, 0, 0, 98, 70, 221, 109, 5, 152, 46, 11, 190, 213, 226, 58, 244, 20, 180, 217, 63, 155, 227, 132, 7, 151, 235, 6, 37, 232, 176, 182, 141, 191, 251, 50, 20, 123, 53, 11, 247, 233, 112, 121, 130, 27, 168, 68, 92, 144, 192, 7, 12, 239, 53, 217, 253, 155, 54, 109, 236, 216, 225, 245, 79, 234, 165, 225, 104, 36, 77, 13, 195, 237, 143, 165, 100, 107, 230, 70, 54, 19, 179, 35, 8, 101, 93, 202, 121, 210, 222, 28, 93, 122, 36, 84, 185, 249, 238, 3, 102, 149, 248, 94, 137, 16, 192, 22, 251, 220, 22, 223, 16, 58, 104, 187, 64, 0, 0, 0, 70, 72, 15, 119, 144, 66, 117, 203, 190, 82, 131, 46, 111, 130, 238, 191, 170, 63, 186, 117, 46, 88, 171, 3, 94, 146, 75, 86, 243, 159, 63, 195, 149, 25, 105, 141, 42, 217, 108, 18, 63, 62, 98, 182, 241, 195, 12, 216, 152, 230, 176, 253, 202, 129, 41, 185, 135, 111, 226, 92, 27, 78, 27, 198];
let arr1 = crypto.arrayToString(value)
let arr2 = crypto.stringToArray(crypto.decryptData(crypto.encryptData(arr1, url), url));
for(let i = 0; i < arr1.length; i++) {
do_check_eq(arr2[i], value[i]);
}
});

View File

@ -30,6 +30,8 @@ skip-if = os == "android" # Bug 1171687: Needs fixing on Android
[test_logins_metainfo.js]
[test_logins_search.js]
[test_notifications.js]
[test_OSCrypto_win.js]
skip-if = os != "win"
[test_recipes_add.js]
[test_recipes_content.js]
[test_storage.js]