Bug 381164 - when Master Password is used, it is asked at every startup. r=gavin

This commit is contained in:
dolske@mozilla.com 2007-07-11 12:40:24 -07:00
parent 646e15fe76
commit 5b43f48b37
2 changed files with 179 additions and 30 deletions

View File

@ -49,6 +49,12 @@ nsLoginInfo.prototype = {
classID : Components.ID("{0f2f347c-1e4f-40cc-8efd-792dea70a85e}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginInfo]),
// Allow storage-Legacy.js to get at the JS object so it can
// slap on a few extra properties for internal use.
get wrappedJSObject() {
return this;
},
hostname : null,
formSubmitURL : null,
httpRealm : null,

View File

@ -169,6 +169,19 @@ LoginManagerStorage_legacy.prototype = {
*
*/
addLogin : function (login) {
// We rely on using login.wrappedJSObject. addLogin is the
// only entry point where we might get a nsLoginInfo object
// that wasn't created by us (and so might not be a JS
// implementation being wrapped)
if (!login.wrappedJSObject) {
var clone = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
clone.init(login.hostname, login.formSubmitURL, login.httpRealm,
login.username, login.password,
login.usernameField, login.passwordField);
login = clone;
}
var key = login.hostname;
// If first entry for key, create an Array to hold it's logins.
@ -192,6 +205,10 @@ LoginManagerStorage_legacy.prototype = {
if (!logins)
throw "No logins found for hostname (" + key + ")";
// The specified login isn't encrypted, so we need to ensure
// the logins we're comparing with are decrypted.
this._decryptLogins(logins);
for (var i = 0; i < logins.length; i++) {
if (logins[i].equals(login)) {
logins.splice(i, 1); // delete that login from array.
@ -225,13 +242,16 @@ LoginManagerStorage_legacy.prototype = {
* Returns an array of nsAccountInfo.
*/
getAllLogins : function (count) {
var result = [];
var result = [], userCanceled;
// Each entry is an array -- append the array entries to |result|.
for each (var hostLogins in this._logins) {
result = result.concat(hostLogins);
}
// decrypt entries for caller.
[result, userCanceled] = this._decryptLogins(result);
count.value = result.length; // needed for XPCOM
return result;
},
@ -300,7 +320,7 @@ LoginManagerStorage_legacy.prototype = {
return [];
}
var result = [];
var result = [], userCanceled;
for each (var login in hostLogins) {
@ -318,6 +338,15 @@ LoginManagerStorage_legacy.prototype = {
result.push(login);
}
// Decrypt entries found for the caller.
[result, userCanceled] = this._decryptLogins(result);
// We want to throw in this case, so that the Login Manager
// knows to stop processing forms on the page so the user isn't
// prompted multiple times.
if (userCanceled)
throw "User canceled Master Password entry";
count.value = result.length; // needed for XPCOM
return result;
},
@ -383,7 +412,6 @@ LoginManagerStorage_legacy.prototype = {
*/
_readFile : function () {
var oldFormat = false;
var writeOnFinish = false;
this.log("Reading passwords from " + this._signonsFile.path);
@ -484,7 +512,7 @@ LoginManagerStorage_legacy.prototype = {
// Line is a username
case STATE.USERVALUE:
entry.username = this._decrypt(line.value);
entry.wrappedJSObject.encryptedUsername = line.value;
parseState++;
break;
@ -497,7 +525,7 @@ LoginManagerStorage_legacy.prototype = {
// Line is a password
case STATE.PASSVALUE:
entry.password = this._decrypt(line.value);
entry.wrappedJSObject.encryptedPassword = line.value;
if (oldFormat) {
entry.formSubmitURL = "";
processEntry = true;
@ -517,14 +545,11 @@ LoginManagerStorage_legacy.prototype = {
}
if (processEntry) {
if (entry.username == "" && entry.password == "") {
// Discard bogus entry, and update the file when done.
writeOnFinish = true;
} else {
if (!this._logins[hostname])
this._logins[hostname] = [];
this._logins[hostname].push(entry);
}
if (!this._logins[hostname])
this._logins[hostname] = [];
this._logins[hostname].push(entry);
entry = null;
processEntry = false;
}
@ -532,9 +557,6 @@ LoginManagerStorage_legacy.prototype = {
lineStream.close();
if (writeOnFinish)
this._writeFile();
return;
},
@ -594,6 +616,7 @@ LoginManagerStorage_legacy.prototype = {
// write each login known for the host
var lastRealm = null;
var firstEntry = true;
var userCanceled = false;
for each (var login in this._logins[hostname]) {
// If this login is for a new realm, start a new entry.
@ -611,8 +634,28 @@ LoginManagerStorage_legacy.prototype = {
firstEntry = false;
var encUsername = this._encrypt(login.username);
var encPassword = this._encrypt(login.password);
// Get the encrypted value of the username. Newly added
// logins will need the plaintext value encrypted.
var encUsername = login.wrappedJSObject.encryptedUsername;
if (!encUsername) {
[encUsername, userCanceled] = this._encrypt(login.username);
login.wrappedJSObject.encryptedUsername = encUsername;
}
if (userCanceled)
break;
// Get the encrypted value of the password. Newly added
// logins will need the plaintext value encrypted.
var encPassword = login.wrappedJSObject.encryptedPassword;
if (!encPassword) {
[encPassword, userCanceled] = this._encrypt(login.password);
login.wrappedJSObject.encryptedPassword = encPassword;
}
if (userCanceled)
break;
writeLine((login.usernameField ? login.usernameField : ""));
writeLine(encUsername);
@ -624,6 +667,13 @@ LoginManagerStorage_legacy.prototype = {
lastRealm = login.httpRealm;
}
if (userCanceled) {
this.log("User canceled Master Password, aborting write.");
// .close will cause an abort w/o modifying original file
outputStream.close();
return;
}
// write end-of-host marker
writeLine(".");
}
@ -635,25 +685,110 @@ LoginManagerStorage_legacy.prototype = {
/*
* _encrypt
* _decryptLogins
*
* Decrypts username and password fields in the provided array of
* logins. This is deferred from the _readFile() code, so that
* the user is not prompted for a master password (if set) until
* the entries are actually used.
*
* 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 master password)
*/
_encrypt : function (plainText) {
var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var plainOctet = converter.ConvertFromUnicode(plainText);
plainOctet += converter.Finish();
return this._decoderRing.encryptString(plainOctet);
_decryptLogins : function (logins) {
var result = [], userCanceled = false;
for each (var login in logins) {
if (!login.username)
[login.username, userCanceled] =
this._decrypt(login.wrappedJSObject.encryptedUsername);
if (userCanceled)
break;
if (!login.password)
[login.password, userCanceled] =
this._decrypt(login.wrappedJSObject.encryptedPassword);
// Probably can't hit this case, but for completeness...
if (userCanceled)
break;
// If decryption failed (corrupt entry?) skip it.
// XXX remove it from the original list entirely?
if (!login.username || !login.password)
continue;
// Force any old mime64-obscured entries to be reencrypted.
if (login.wrappedJSObject.encryptedUsername &&
login.wrappedJSObject.encryptedUsername.charAt(0) == '~')
login.wrappedJSObject.encryptedUsername = null;
if (login.wrappedJSObject.encryptedPassword &&
login.wrappedJSObject.encryptedPassword.charAt(0) == '~')
login.wrappedJSObject.encryptedPassword = null;
result.push(login);
}
return [result, userCanceled];
},
/*
* decrypt
* _encrypt
*
* Encrypts the specified string, using the SecretDecoderRing.
*
* Returns [cipherText, userCanceled] where:
* cipherText -- the encrypted string, or null if it failed.
* userCanceled -- if the encryption failed, this is true if the
* user selected Cancel when prompted to enter their
* Master Password. The caller should bail out, and not
* not request that more things be encrypted (which
* results in prompting the user for a Master Password
* over and over.)
*/
_encrypt : function (plainText) {
var cipherText = null, userCanceled = false;
try {
var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var plainOctet = converter.ConvertFromUnicode(plainText);
plainOctet += converter.Finish();
cipherText = this._decoderRing.encryptString(plainOctet);
} catch (e) {
this.log("Failed to encrypt string. (" + e.name + ")");
if (e.result == Components.results.NS_ERROR_NOT_AVAILABLE)
userCanceled = true;
}
return [cipherText, userCanceled];
},
/*
* _decrypt
*
* Decrypts the specified string, using the SecretDecoderRing.
*
* Returns [plainText, userCanceled] where:
* plainText -- the decrypted string, or null if it failed.
* userCanceled -- if the decryption failed, this is true if the
* user selected Cancel when prompted to enter their
* Master Password. The caller should bail out, and not
* not request that more things be decrypted (which
* results in prompting the user for a Master Password
* over and over.)
*/
_decrypt : function (cipherText) {
var plainText = null;
var plainText = null, userCanceled = false;
try {
var plainOctet;
@ -670,10 +805,18 @@ LoginManagerStorage_legacy.prototype = {
converter.charset = "UTF-8";
plainText = converter.ConvertToUnicode(plainOctet);
} catch (e) {
this.log("Failed to decrypt string: " + cipherText);
this.log("Failed to decrypt string: " + cipherText +
" (" + e.name + ")");
// If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
// If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
// Wrong passwords are handled by the decoderRing reprompting;
// we get no notification.
if (e.result == Components.results.NS_ERROR_NOT_AVAILABLE)
userCanceled = true;
}
return plainText;
return [plainText, userCanceled];
},
}; // end of nsLoginManagerStorage_legacy implementation