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, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* LoginManagerStorage implementation for the JSON back-end.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
const SYNCABLE_LOGIN_FIELDS = [
// `nsILoginInfo` fields.
"hostname",
"formSubmitURL",
"httpRealm",
"username",
"password",
"usernameField",
"passwordField",
// `nsILoginMetaInfo` fields.
"timeCreated",
"timePasswordChanged",
];
// Compares two logins to determine if their syncable fields changed. The login
// manager fires `modifyLogin` for changes to all fields, including ones we
// don't sync. In particular, `timeLastUsed` changes shouldn't mark the login
// for upload; otherwise, we might overwrite changed passwords before they're
// downloaded (bug 973166).
function isSyncableChange(oldLogin, newLogin) {
oldLogin.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
newLogin.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
return SYNCABLE_LOGIN_FIELDS.some(prop => oldLogin[prop] != newLogin[prop]);
}
// Returns true if the argument is for the FxA login.
function isFXAHost(login) {
return login.hostname == lazy.FXA_PWDMGR_HOST;
}
export class LoginManagerStorage_json {
constructor() {
this.__crypto = null; // nsILoginManagerCrypto service
this.__decryptedPotentiallyVulnerablePasswords = null;
}
get _crypto() {
if (!this.__crypto) {
this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService(
Ci.nsILoginManagerCrypto
);
}
return this.__crypto;
}
get _decryptedPotentiallyVulnerablePasswords() {
if (!this.__decryptedPotentiallyVulnerablePasswords) {
this._store.ensureDataReady();
this.__decryptedPotentiallyVulnerablePasswords = [];
for (const potentiallyVulnerablePassword of this._store.data
.potentiallyVulnerablePasswords) {
const decryptedPotentiallyVulnerablePassword = this._crypto.decrypt(
potentiallyVulnerablePassword.encryptedPassword
);
this.__decryptedPotentiallyVulnerablePasswords.push(
decryptedPotentiallyVulnerablePassword
);
}
}
return this.__decryptedPotentiallyVulnerablePasswords;
}
initialize() {
try {
// Force initialization of the crypto module.
// See bug 717490 comment 17.
this._crypto;
let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
// Set the reference to LoginStore synchronously.
let jsonPath = PathUtils.join(profileDir, "logins.json");
let backupPath = "";
let loginsBackupEnabled = Services.prefs.getBoolPref(
"signon.backup.enabled"
);
if (loginsBackupEnabled) {
backupPath = PathUtils.join(profileDir, "logins-backup.json");
}
this._store = new lazy.LoginStore(jsonPath, backupPath);
return (async () => {
// Load the data asynchronously.
this.log(`Opening database at ${this._store.path}.`);
await this._store.load();
})().catch(console.error);
} catch (e) {
this.log(`Initialization failed ${e.name}.`);
throw new Error("Initialization failed");
}
}
/**
* Internal method used by regression tests only. It is called before
* replacing this storage module with a new instance.
*/
terminate() {
this._store._saver.disarm();
return this._store._save();
}
/**
* Returns the "sync id" used by Sync to know whether the store is current with
* respect to the sync servers. It is stored encrypted, but only so we
* can detect failure to decrypt (for example, a "reset" of the primary
* password will leave all logins alone, but they will fail to decrypt. We
* also want this metadata to be unavailable in that scenario)
*
* Returns null if the data doesn't exist or if the data can't be
* decrypted (including if the primary-password prompt is cancelled). This is
* OK for Sync as it can't even begin syncing if the primary-password is
* locked as the sync encrytion keys are stored in this login manager.
*/
async getSyncID() {
await this._store.load();
if (!this._store.data.sync) {
return null;
}
let raw = this._store.data.sync.syncID;
try {
return raw ? this._crypto.decrypt(raw) : null;
} catch (e) {
if (e.result == Cr.NS_ERROR_FAILURE) {
this.log("Could not decrypt the syncID - returning null.");
return null;
}
// any other errors get re-thrown.
throw e;
}
}
async setSyncID(syncID) {
await this._store.load();
if (!this._store.data.sync) {
this._store.data.sync = {};
}
this._store.data.sync.syncID = syncID ? this._crypto.encrypt(syncID) : null;
this._store.saveSoon();
}
async getLastSync() {
await this._store.load();
if (!this._store.data.sync) {
return 0;
}
return this._store.data.sync.lastSync || 0.0;
}
async setLastSync(timestamp) {
await this._store.load();
if (!this._store.data.sync) {
this._store.data.sync = {};
}
this._store.data.sync.lastSync = timestamp;
this._store.saveSoon();
}
#incrementSyncCounter(login) {
login.syncCounter++;
}
async resetSyncCounter(guid, value) {
this._store.ensureDataReady();
// This will also find deleted items.
let login = this._store.data.logins.find(login => login.guid == guid);
if (login?.syncCounter > 0) {
login.syncCounter = Math.max(0, login.syncCounter - value);
login.everSynced = true;
}
this._store.saveSoon();
}
// Returns false if the login has marked as deleted or doesn't exist.
loginIsDeleted(guid) {
let login = this._store.data.logins.find(l => l.guid == guid);
return !!login?.deleted;
}
// Synrhronuously stores encrypted login, returns login clone with upserted
// uuid and updated timestamps
#addLogin(login) {
this._store.ensureDataReady();
// Throws if there are bogus values.
lazy.LoginHelper.checkLoginValues(login);
// Clone the login, so we don't modify the caller's object.
let loginClone = login.clone();
// Initialize the nsILoginMetaInfo fields, unless the caller gave us values
loginClone.QueryInterface(Ci.nsILoginMetaInfo);
if (loginClone.guid) {
let guid = loginClone.guid;
if (!this._isGuidUnique(guid)) {
// We have an existing GUID, but it's possible that entry is unable
// to be decrypted - if that's the case we remove the existing one
// and allow this one to be added.
let existing = this._searchLogins({ guid })[0];
if (this._decryptLogins(existing).length) {
// Existing item is good, so it's an error to try and re-add it.
throw new Error("specified GUID already exists");
}
// find and remove the existing bad entry.
let foundIndex = this._store.data.logins.findIndex(l => l.guid == guid);
if (foundIndex == -1) {
throw new Error("can't find a matching GUID to remove");
}
this._store.data.logins.splice(foundIndex, 1);
}
} else {
loginClone.guid = Services.uuid.generateUUID().toString();
}
// Set timestamps
let currentTime = Date.now();
if (!loginClone.timeCreated) {
loginClone.timeCreated = currentTime;
}
if (!loginClone.timeLastUsed) {
loginClone.timeLastUsed = currentTime;
}
if (!loginClone.timePasswordChanged) {
loginClone.timePasswordChanged = currentTime;
}
if (!loginClone.timesUsed) {
loginClone.timesUsed = 1;
}
// If the everSynced is already set, then this login is an incoming
// sync record, so there is no need to mark this as needed to be synced.
if (!loginClone.everSynced && !isFXAHost(loginClone)) {
this.#incrementSyncCounter(loginClone);
}
this._store.data.logins.push({
id: this._store.data.nextId++,
hostname: loginClone.origin,
httpRealm: loginClone.httpRealm,
formSubmitURL: loginClone.formActionOrigin,
usernameField: loginClone.usernameField,
passwordField: loginClone.passwordField,
encryptedUsername: loginClone.username,
encryptedPassword: loginClone.password,
guid: loginClone.guid,
encType: this._crypto.defaultEncType,
timeCreated: loginClone.timeCreated,
timeLastUsed: loginClone.timeLastUsed,
timePasswordChanged: loginClone.timePasswordChanged,
timesUsed: loginClone.timesUsed,
syncCounter: loginClone.syncCounter,
everSynced: loginClone.everSynced,
encryptedUnknownFields: loginClone.unknownFields,
});
this._store.saveSoon();
return loginClone;
}
async addLoginsAsync(logins, continueOnDuplicates = false) {
if (logins.length === 0) {
return logins;
}
const encryptedLogins = await this.#encryptLogins(logins);
const resultLogins = [];
for (const [login, encryptedLogin] of encryptedLogins) {
// check for duplicates
let loginData = {
origin: login.origin,
formActionOrigin: login.formActionOrigin,
httpRealm: login.httpRealm,
};
const existingLogins = await Services.logins.searchLoginsAsync(loginData);
const matchingLogin = existingLogins.find(l => login.matches(l, true));
if (matchingLogin) {
if (continueOnDuplicates) {
continue;
} else {
throw lazy.LoginHelper.createLoginAlreadyExistsError(
matchingLogin.guid
);
}
}
const resultLogin = this.#addLogin(encryptedLogin);
// restore unencrypted username and password for use in `addLogin` event
// and return value
resultLogin.username = login.username;
resultLogin.password = login.password;
// Send a notification that a login was added.
lazy.LoginHelper.notifyStorageChanged("addLogin", resultLogin);
resultLogins.push(resultLogin);
}
return resultLogins;
}
removeLogin(login, fromSync) {
this._store.ensureDataReady();
let [idToDelete, storedLogin] = this._getIdForLogin(login);
if (!idToDelete) {
throw new Error("No matching logins");
}
let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
if (foundIndex != -1) {
let login = this._store.data.logins[foundIndex];
if (!login.deleted) {
if (fromSync) {
this.#replaceLoginWithTombstone(login);
} else if (login.everSynced) {
// The login has been synced, so mark it as deleted.
this.#incrementSyncCounter(login);
this.#replaceLoginWithTombstone(login);
} else {
// The login was never synced, so just remove it from the data.
this._store.data.logins.splice(foundIndex, 1);
}
this._store.saveSoon();
}
}
lazy.LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
}
modifyLogin(oldLogin, newLoginData, fromSync) {
this._store.ensureDataReady();
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
if (!idToModify) {
throw new Error("No matching logins");
}
let newLogin = lazy.LoginHelper.buildModifiedLogin(
oldStoredLogin,
newLoginData
);
// Check if the new GUID is duplicate.
if (
newLogin.guid != oldStoredLogin.guid &&
!this._isGuidUnique(newLogin.guid)
) {
throw new Error("specified GUID already exists");
}
// Look for an existing entry in case key properties changed.
if (!newLogin.matches(oldLogin, true)) {
let loginData = {
origin: newLogin.origin,
formActionOrigin: newLogin.formActionOrigin,
httpRealm: newLogin.httpRealm,
};
let logins = this.searchLogins(
lazy.LoginHelper.newPropertyBag(loginData)
);
let matchingLogin = logins.find(login => newLogin.matches(login, true));
if (matchingLogin) {
throw lazy.LoginHelper.createLoginAlreadyExistsError(
matchingLogin.guid
);
}
}
// Don't sync changes to the accounts password or when changes were only
// made to fields that should not be synced.
if (
!fromSync &&
!isFXAHost(newLogin) &&
isSyncableChange(oldLogin, newLogin)
) {
this.#incrementSyncCounter(newLogin);
}
// Get the encrypted value of the username and password.
let [encUsername, encPassword, encType, encUnknownFields] =
this._encryptLogin(newLogin);
for (let loginItem of this._store.data.logins) {
if (loginItem.id == idToModify && !loginItem.deleted) {
loginItem.hostname = newLogin.origin;
loginItem.httpRealm = newLogin.httpRealm;
loginItem.formSubmitURL = newLogin.formActionOrigin;
loginItem.usernameField = newLogin.usernameField;
loginItem.passwordField = newLogin.passwordField;
loginItem.encryptedUsername = encUsername;
loginItem.encryptedPassword = encPassword;
loginItem.guid = newLogin.guid;
loginItem.encType = encType;
loginItem.timeCreated = newLogin.timeCreated;
loginItem.timeLastUsed = newLogin.timeLastUsed;
loginItem.timePasswordChanged = newLogin.timePasswordChanged;
loginItem.timesUsed = newLogin.timesUsed;
loginItem.encryptedUnknownFields = encUnknownFields;
loginItem.syncCounter = newLogin.syncCounter;
this._store.saveSoon();
break;
}
}
lazy.LoginHelper.notifyStorageChanged("modifyLogin", [
oldStoredLogin,
newLogin,
]);
}
// Replace the login with a tombstone. It has a guid and sync-related properties,
// but does not contain the login or password information.
#replaceLoginWithTombstone(login) {
login.deleted = true;
// Delete all fields except guid, timePasswordChanged, syncCounter
// and everSynced;
delete login.hostname;
delete login.httpRealm;
delete login.formSubmitURL;
delete login.usernameField;
delete login.passwordField;
delete login.encryptedUsername;
delete login.encryptedPassword;
delete login.encType;
delete login.timeCreated;
delete login.timeLastUsed;
delete login.timesUsed;
delete login.encryptedUnknownFields;
}
recordPasswordUse(login) {
// Update the lastUsed timestamp and increment the use count.
let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
Ci.nsIWritablePropertyBag
);
propBag.setProperty("timeLastUsed", Date.now());
propBag.setProperty("timesUsedIncrement", 1);
this.modifyLogin(login, propBag);
}
async recordBreachAlertDismissal(loginGUID) {
this._store.ensureDataReady();
const dismissedBreachAlertsByLoginGUID =
this._store._data.dismissedBreachAlertsByLoginGUID;
dismissedBreachAlertsByLoginGUID[loginGUID] = {
timeBreachAlertDismissed: new Date().getTime(),
};
return this._store.saveSoon();
}
getBreachAlertDismissalsByLoginGUID() {
this._store.ensureDataReady();
return this._store._data.dismissedBreachAlertsByLoginGUID;
}
/**
* Returns an array of nsILoginInfo. If decryption of a login
* fails due to a corrupt entry, the login is not included in
* the resulting array.
*
* @resolve {nsILoginInfo[]}
*/
async getAllLogins(includeDeleted) {
this._store.ensureDataReady();
let [logins] = this._searchLogins({}, includeDeleted);
if (!logins.length) {
return [];
}
return this.#decryptLogins(logins);
}
async searchLoginsAsync(matchData, includeDeleted) {
this.log(`Searching for matching logins for origin ${matchData.origin}.`);
let result = this.searchLogins(
lazy.LoginHelper.newPropertyBag(matchData),
includeDeleted
);
// Emulate being async:
return Promise.resolve(result);
}
/**
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
* JavaScript object and decrypt the results.
*
* @return {nsILoginInfo[]} which are decrypted.
*/
searchLogins(matchData, includeDeleted) {
this._store.ensureDataReady();
let realMatchData = {};
let options = {};
matchData.QueryInterface(Ci.nsIPropertyBag2);
if (matchData.hasKey("guid")) {
// Enforce GUID-based filtering when available, since the origin of the
// login may not match the origin of the form in the case of scheme
// upgrades.
realMatchData = { guid: matchData.getProperty("guid") };
} else {
// Convert nsIPropertyBag to normal JS object.
for (let prop of matchData.enumerator) {
switch (prop.name) {
// Some property names aren't field names but are special options to
// affect the search.
case "acceptDifferentSubdomains":
case "schemeUpgrades":
case "acceptRelatedRealms":
case "relatedRealms": {
options[prop.name] = prop.value;
break;
}
default: {
realMatchData[prop.name] = prop.value;
break;
}
}
}
}
let [logins] = this._searchLogins(realMatchData, includeDeleted, options);
// Decrypt entries found for the caller.
logins = this._decryptLogins(logins);
return logins;
}
/**
* Private method to perform arbitrary searches on any field. Decryption is
* left to the caller.
*
* formActionOrigin is handled specially for compatibility. If a null string
* is passed and other match fields are present, it is treated as if it was
* not present.
*
* Returns [logins, ids] for logins that match the arguments, where logins
* is an array of encrypted nsLoginInfo and ids is an array of associated
* ids in the database.
*/
_searchLogins(
matchData,
includeDeleted = false,
aOptions = {
schemeUpgrades: false,
acceptDifferentSubdomains: false,
acceptRelatedRealms: false,
relatedRealms: [],
},
candidateLogins = this._store.data.logins
) {
function match(aLoginItem) {
for (let field in matchData) {
let wantedValue = matchData[field];
// Override the storage field name for some fields due to backwards
// compatibility with Sync/storage.
let storageFieldName = field;
switch (field) {
case "formActionOrigin": {
storageFieldName = "formSubmitURL";
break;
}
case "origin": {
storageFieldName = "hostname";
break;
}
}
switch (field) {
case "formActionOrigin":
if (wantedValue != null) {
// Historical compatibility requires this special case
if (
aLoginItem.formSubmitURL == "" ||
(wantedValue == "" && Object.keys(matchData).length != 1)
) {
break;
}
if (
!lazy.LoginHelper.isOriginMatching(
aLoginItem[storageFieldName],
wantedValue,
aOptions
)
) {
return false;
}
break;
}
// fall through
case "origin":
if (wantedValue != null) {
// needed for formActionOrigin fall through
if (
!lazy.LoginHelper.isOriginMatching(
aLoginItem[storageFieldName],
wantedValue,
aOptions
)
) {
return false;
}
break;
}
// Normal cases.
// fall through
case "httpRealm":
case "id":
case "usernameField":
case "passwordField":
case "encryptedUsername":
case "encryptedPassword":
case "guid":
case "encType":
case "timeCreated":
case "timeLastUsed":
case "timePasswordChanged":
case "timesUsed":
case "syncCounter":
case "everSynced":
if (wantedValue == null && aLoginItem[storageFieldName]) {
return false;
} else if (aLoginItem[storageFieldName] != wantedValue) {
return false;
}
break;
// Fail if caller requests an unknown property.
default:
throw new Error("Unexpected field: " + field);
}
}
return true;
}
let foundLogins = [],
foundIds = [];
for (let loginItem of candidateLogins) {
if (loginItem.deleted && !includeDeleted) {
continue; // skip deleted items
}
if (match(loginItem)) {
// Create the new nsLoginInfo object, push to array
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
Ci.nsILoginInfo
);
login.init(
loginItem.hostname,
loginItem.formSubmitURL,
loginItem.httpRealm,
loginItem.encryptedUsername,
loginItem.encryptedPassword,
loginItem.usernameField,
loginItem.passwordField
);
// set nsILoginMetaInfo values
login.QueryInterface(Ci.nsILoginMetaInfo);
login.guid = loginItem.guid;
login.timeCreated = loginItem.timeCreated;
login.timeLastUsed = loginItem.timeLastUsed;
login.timePasswordChanged = loginItem.timePasswordChanged;
login.timesUsed = loginItem.timesUsed;
login.syncCounter = loginItem.syncCounter;
login.everSynced = loginItem.everSynced;
// Any unknown fields along for the ride
login.unknownFields = loginItem.encryptedUnknownFields;
foundLogins.push(login);
foundIds.push(loginItem.id);
}
}
this.log(
`Returning ${foundLogins.length} logins for specified origin with options ${aOptions}`
);
return [foundLogins, foundIds];
}
/**
* Removes all logins from local storage, including FxA Sync key.
*
* NOTE: You probably want removeAllUserFacingLogins instead of this function.
*
*/
removeAllLogins() {
this.#removeLogins(false, true);
}
/**
* Removes all user facing logins from storage. e.g. all logins except the FxA Sync key
*
* If you need to remove the FxA key, use `removeAllLogins` instead
*
* @param fullyRemove remove the logins rather than mark them deleted.
*/
removeAllUserFacingLogins(fullyRemove) {
this.#removeLogins(fullyRemove, false);
}
/**
* Removes all logins from storage. If removeFXALogin is true, then the FxA Sync
* key is also removed.
*
* @param fullyRemove remove the logins rather than mark them deleted.
* @param removeFXALogin also remove the FxA Sync key.
*/
#removeLogins(fullyRemove, removeFXALogin = false) {
this._store.ensureDataReady();
this.log("Removing all logins.");
let removedLogins = [];
let remainingLogins = [];
for (let login of this._store.data.logins) {
if (
!removeFXALogin &&
isFXAHost(login) &&
login.httpRealm == lazy.FXA_PWDMGR_REALM
) {
remainingLogins.push(login);
} else {
removedLogins.push(login);
if (!fullyRemove && login?.everSynced) {
// The login has been synced, so mark it as deleted.
this.#incrementSyncCounter(login);
this.#replaceLoginWithTombstone(login);
remainingLogins.push(login);
}
}
}
this._store.data.logins = remainingLogins;
this._store.data.potentiallyVulnerablePasswords = [];
this.__decryptedPotentiallyVulnerablePasswords = null;
this._store.data.dismissedBreachAlertsByLoginGUID = {};
this._store.saveSoon();
lazy.LoginHelper.notifyStorageChanged("removeAllLogins", removedLogins);
}
findLogins(origin, formActionOrigin, httpRealm) {
this._store.ensureDataReady();
let loginData = {
origin,
formActionOrigin,
httpRealm,
};
let matchData = {};
for (let field of ["origin", "formActionOrigin", "httpRealm"]) {
if (loginData[field] != "") {
matchData[field] = loginData[field];
}
}
let [logins] = this._searchLogins(matchData);
// Decrypt entries found for the caller.
logins = this._decryptLogins(logins);
this.log(`Returning ${logins.length} logins.`);
return logins;
}
countLogins(origin, formActionOrigin, httpRealm) {
this._store.ensureDataReady();
let loginData = {
origin,
formActionOrigin,
httpRealm,
};
let matchData = {};
for (let field of ["origin", "formActionOrigin", "httpRealm"]) {
if (loginData[field] != "") {
matchData[field] = loginData[field];
}
}
let [logins] = this._searchLogins(matchData);
this.log(`Counted ${logins.length} logins.`);
return logins.length;
}
addPotentiallyVulnerablePassword(login) {
this._store.ensureDataReady();
// this breached password is already stored
if (this.isPotentiallyVulnerablePassword(login)) {
return;
}
this.__decryptedPotentiallyVulnerablePasswords.push(login.password);
this._store.data.potentiallyVulnerablePasswords.push({
encryptedPassword: this._crypto.encrypt(login.password),
});
this._store.saveSoon();
}
isPotentiallyVulnerablePassword(login) {
return this._decryptedPotentiallyVulnerablePasswords.includes(
login.password
);
}
clearAllPotentiallyVulnerablePasswords() {
this._store.ensureDataReady();
if (!this._store.data.potentiallyVulnerablePasswords.length) {
// No need to write to disk
return;
}
this._store.data.potentiallyVulnerablePasswords = [];
this._store.saveSoon();
this.__decryptedPotentiallyVulnerablePasswords = null;
}
get uiBusy() {
return this._crypto.uiBusy;
}
get isLoggedIn() {
return this._crypto.isLoggedIn;
}
/**
* Returns an array with two items: [id, login]. If the login was not
* found, both items will be null. The returned login contains the actual
* stored login (useful for looking at the actual nsILoginMetaInfo values).
*/
_getIdForLogin(login) {
this._store.ensureDataReady();
let matchData = {};
for (let field of ["origin", "formActionOrigin", "httpRealm"]) {
if (login[field] != "") {
matchData[field] = login[field];
}
}
let [logins, ids] = this._searchLogins(matchData);
let id = null;
let foundLogin = null;
// The specified login isn't encrypted, so we need to ensure
// the logins we're comparing with are decrypted. We decrypt one entry
// at a time, lest _decryptLogins return fewer entries and screw up
// indices between the two.
for (let i = 0; i < logins.length; i++) {
let [decryptedLogin] = this._decryptLogins([logins[i]]);
if (!decryptedLogin || !decryptedLogin.equals(login)) {
continue;
}
// We've found a match, set id and break
foundLogin = decryptedLogin;
id = ids[i];
break;
}
return [id, foundLogin];
}
/**
* Checks to see if the specified GUID already exists.
*/
_isGuidUnique(guid) {
this._store.ensureDataReady();
return this._store.data.logins.every(l => l.guid != guid);
}
/*
* Asynchronously encrypt multiple logins.
* Returns a promise resolving to an array of arrays containing two entries:
* the original login and a clone with encrypted properties.
*/
async #encryptLogins(logins) {
if (logins.length === 0) {
return logins;
}
const plaintexts = logins.reduce(
(memo, { username, password, unknownFields }) =>
memo.concat([username, password, unknownFields]),
[]
);
const ciphertexts = await this._crypto.encryptMany(plaintexts);
return logins.map((login, i) => {
const [encryptedUsername, encryptedPassword, encryptedUnknownFields] =
ciphertexts.slice(3 * i, 3 * i + 3);
const encryptedLogin = login.clone();
encryptedLogin.username = encryptedUsername;
encryptedLogin.password = encryptedPassword;
encryptedLogin.unknownFields = encryptedUnknownFields;
return [login, encryptedLogin];
});
}
/*
* Asynchronously decrypt multiple logins.
* Returns a promise resolving to an array of clones with decrypted properties.
*/
async #decryptLogins(logins) {
if (logins.length === 0) {
return logins;
}
const ciphertexts = logins.reduce(
(memo, { username, password, unknownFields }) =>
memo.concat([username, password, unknownFields]),
[]
);
const plaintexts = await this._crypto.decryptMany(ciphertexts);
return logins
.map((login, i) => {
// Deleted logins don't have any info to decrypt.
const decryptedLogin = login.clone();
if (this.loginIsDeleted(login.guid)) {
return decryptedLogin;
}
const [username, password, unknownFields] = plaintexts.slice(
3 * i,
3 * i + 3
);
// If the username or password is blank it means that decryption may have
// failed during decryptMany but we can't differentiate an empty string
// value from a failure so we attempt to decrypt again and check the
// result.
if (!username || !password) {
try {
this._crypto.decrypt(login.username);
this._crypto.decrypt(login.password);
} catch (e) {
// If decryption failed (corrupt entry?), just return it as it is.
// Rethrow other errors (like canceling entry of a primary pw)
if (e.result == Cr.NS_ERROR_FAILURE) {
this.log(
`Could not decrypt login: ${
login.QueryInterface(Ci.nsILoginMetaInfo).guid
}.`
);
return null;
}
throw e;
}
}
decryptedLogin.username = username;
decryptedLogin.password = password;
decryptedLogin.unknownFields = unknownFields;
return decryptedLogin;
})
.filter(Boolean);
}
/**
* Returns the encrypted username, password, and encrypton type for the specified
* login. Can throw if the user cancels a primary password entry.
*/
_encryptLogin(login) {
let encUsername = this._crypto.encrypt(login.username);
let encPassword = this._crypto.encrypt(login.password);
// Unknown fields should be encrypted since we can't know whether new fields
// from other clients will contain sensitive data or not
let encUnknownFields = null;
if (login.unknownFields) {
encUnknownFields = this._crypto.encrypt(login.unknownFields);
}
let encType = this._crypto.defaultEncType;
return [encUsername, encPassword, encType, encUnknownFields];
}
/**
* Decrypts username and password fields in the provided array of
* logins.
*
* The entries specified by the array will be decrypted, if possible.
* An array of successfully decrypted logins will be returned. The return
* value should be given to external callers (since still-encrypted
* entries are useless), whereas internal callers generally don't want
* to lose unencrypted entries (eg, because the user clicked Cancel
* instead of entering their primary password)
*/
_decryptLogins(logins) {
let result = [];
for (let login of logins) {
if (this.loginIsDeleted(login.guid)) {
result.push(login);
continue;
}
try {
login.username = this._crypto.decrypt(login.username);
login.password = this._crypto.decrypt(login.password);
// Verify unknownFields actually has a value
if (login.unknownFields) {
login.unknownFields = this._crypto.decrypt(login.unknownFields);
}
} catch (e) {
// If decryption failed (corrupt entry?), just skip it.
// Rethrow other errors (like canceling entry of a primary pw)
if (e.result == Cr.NS_ERROR_FAILURE) {
continue;
}
throw e;
}
result.push(login);
}
return result;
}
}
ChromeUtils.defineLazyGetter(LoginManagerStorage_json.prototype, "log", () => {
let logger = lazy.LoginHelper.createLogger("Login storage");
return logger.log.bind(logger);
});