/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Password Manager. * * The Initial Developer of the Original Code is * Brian Ryner. * Portions created by the Initial Developer are Copyright (C) 2003 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Brian Ryner * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsPasswordManager.h" #include "nsIFile.h" #include "nsNetUtil.h" #include "nsILineInputStream.h" #include "plbase64.h" #include "nsISecretDecoderRing.h" #include "nsIPasswordInternal.h" #include "nsIPrompt.h" #include "nsIPrefService.h" #include "nsIPrefBranch.h" #include "nsIPrefBranch2.h" #include "prmem.h" #include "nsIStringBundle.h" #include "nsArray.h" #include "nsICategoryManager.h" #include "nsIObserverService.h" #include "nsIDocumentLoader.h" #include "nsIWebProgress.h" #include "nsIDOMDocument.h" #include "nsIDOMWindow.h" #include "nsIDOMHTMLDocument.h" #include "nsIDocument.h" #include "nsIDOMHTMLCollection.h" #include "nsIForm.h" #include "nsIDOMHTMLInputElement.h" #include "nsIContent.h" #include "nsIFormControl.h" #include "nsIDOMWindowInternal.h" #include "nsCURILoader.h" #include "nsAppDirectoryServiceDefs.h" #include "nsIDOMEventTarget.h" #include "nsIDOMHTMLFormElement.h" #include "nsIAutoCompleteResult.h" #include "nsIPK11TokenDB.h" #include "nsIPK11Token.h" static const char kPMPropertiesURL[] = "chrome://passwordmgr/locale/passwordmgr.properties"; static PRBool sRememberPasswords = PR_FALSE; static PRBool sPrefsInitialized = PR_FALSE; static PRBool sPasswordsLoaded = PR_FALSE; static nsIStringBundle* sPMBundle; static nsISecretDecoderRing* sDecoderRing; static nsPasswordManager* sPasswordManager; class nsPasswordManager::SignonDataEntry { public: nsString userField; nsString userValue; nsString passField; nsString passValue; nsCString actionOrigin; SignonDataEntry* next; SignonDataEntry() : next(nsnull) { } ~SignonDataEntry() { delete next; } }; class nsPasswordManager::SignonHashEntry { // Wraps a pointer to the linked list of SignonDataEntry objects. // This allows us to adjust the head of the linked list without a // hashtable operation. public: SignonDataEntry* head; SignonHashEntry(SignonDataEntry* aEntry) : head(aEntry) { } ~SignonHashEntry() { delete head; } }; class nsPasswordManager::PasswordEntry : public nsIPasswordInternal { public: PasswordEntry(const nsACString& aKey, SignonDataEntry* aData); virtual ~PasswordEntry() { } NS_DECL_ISUPPORTS NS_DECL_NSIPASSWORD NS_DECL_NSIPASSWORDINTERNAL protected: nsCString mHost; nsString mUser; nsString mUserField; nsString mPassword; nsString mPasswordField; nsCString mActionOrigin; PRBool mDecrypted[2]; }; NS_IMPL_ISUPPORTS2(nsPasswordManager::PasswordEntry, nsIPassword, nsIPasswordInternal) nsPasswordManager::PasswordEntry::PasswordEntry(const nsACString& aKey, SignonDataEntry* aData) : mHost(aKey) { mDecrypted[0] = mDecrypted[1] = PR_FALSE; if (aData) { mUser.Assign(aData->userValue); mUserField.Assign(aData->userField); mPassword.Assign(aData->passValue); mPasswordField.Assign(aData->passField); mActionOrigin.Assign(aData->actionOrigin); } } NS_IMETHODIMP nsPasswordManager::PasswordEntry::GetHost(nsACString& aHost) { aHost.Assign(mHost); return NS_OK; } NS_IMETHODIMP nsPasswordManager::PasswordEntry::GetUser(nsAString& aUser) { if (!mUser.IsEmpty() && !mDecrypted[0]) { if (NS_SUCCEEDED(DecryptData(mUser, mUser))) mDecrypted[0] = PR_TRUE; else return NS_ERROR_FAILURE; } aUser.Assign(mUser); return NS_OK; } NS_IMETHODIMP nsPasswordManager::PasswordEntry::GetPassword(nsAString& aPassword) { if (!mPassword.IsEmpty() && !mDecrypted[1]) { if (NS_SUCCEEDED(DecryptData(mPassword, mPassword))) mDecrypted[1] = PR_TRUE; else return NS_ERROR_FAILURE; } aPassword.Assign(mPassword); return NS_OK; } NS_IMETHODIMP nsPasswordManager::PasswordEntry::GetUserFieldName(nsAString& aField) { aField.Assign(mUserField); return NS_OK; } NS_IMETHODIMP nsPasswordManager::PasswordEntry::GetPasswordFieldName(nsAString& aField) { aField.Assign(mPasswordField); return NS_OK; } NS_IMPL_ADDREF(nsPasswordManager) NS_IMPL_RELEASE(nsPasswordManager) NS_INTERFACE_MAP_BEGIN(nsPasswordManager) NS_INTERFACE_MAP_ENTRY(nsIPasswordManager) NS_INTERFACE_MAP_ENTRY(nsIPasswordManagerInternal) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsIFormSubmitObserver) NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) NS_INTERFACE_MAP_ENTRY(nsIDOMFocusListener) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIDOMEventListener, nsIDOMFocusListener) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPasswordManager) NS_INTERFACE_MAP_END nsPasswordManager::nsPasswordManager() : mAutoCompletingField(nsnull) { } nsPasswordManager::~nsPasswordManager() { } /* static */ nsPasswordManager* nsPasswordManager::GetInstance() { if (!sPasswordManager) { sPasswordManager = new nsPasswordManager(); if (!sPasswordManager) return nsnull; NS_ADDREF(sPasswordManager); // addref the global if (NS_FAILED(sPasswordManager->Init())) { NS_RELEASE(sPasswordManager); return nsnull; } } // We fail to load passwords during early initialization // This wrapper function allows us to handle that error and defer // password loading until later sPasswordManager->LoadPasswords(); NS_ADDREF(sPasswordManager); // addref the return result return sPasswordManager; } nsresult nsPasswordManager::Init() { mSignonTable.Init(); mRejectTable.Init(); mAutoCompleteInputs.Init(); sPrefsInitialized = PR_TRUE; nsCOMPtr prefService = do_GetService(NS_PREFSERVICE_CONTRACTID); NS_ASSERTION(prefService, "No pref service"); prefService->GetBranch("signon.", getter_AddRefs(mPrefBranch)); NS_ASSERTION(mPrefBranch, "No pref branch"); mPrefBranch->GetBoolPref("rememberSignons", &sRememberPasswords); nsCOMPtr branchInternal = do_QueryInterface(mPrefBranch); // Have the pref service hold a weak reference; the service manager // will be holding a strong reference. branchInternal->AddObserver("rememberSignons", this, PR_TRUE); // Be a form submit and web progress observer so that we can save and // prefill passwords. nsCOMPtr obsService = do_GetService("@mozilla.org/observer-service;1"); NS_ASSERTION(obsService, "No observer service"); obsService->AddObserver(this, NS_EARLYFORMSUBMIT_SUBJECT, PR_TRUE); nsCOMPtr docLoaderService = do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); NS_ASSERTION(docLoaderService, "No document loader service"); nsCOMPtr progress = do_QueryInterface(docLoaderService); NS_ASSERTION(progress, "docloader service does not implement nsIWebProgress"); progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); return NS_OK; } /* static */ PRBool nsPasswordManager::SingleSignonEnabled() { if (!sPrefsInitialized) { // Create the PasswordManager service to initialize the prefs and callback nsCOMPtr manager = do_GetService(NS_PASSWORDMANAGER_CONTRACTID); } return sRememberPasswords; } /* static */ NS_METHOD nsPasswordManager::Register(nsIComponentManager* aCompMgr, nsIFile* aPath, const char* aRegistryLocation, const char* aComponentType, const nsModuleComponentInfo* aInfo) { // By registering in NS_PASSWORDMANAGER_CATEGORY, an instance of the password // manager will be created when a password input is added to a form. We // can then register that singleton instance as a form submission . nsresult rv; nsCOMPtr catman = do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); nsXPIDLCString prevEntry; catman->AddCategoryEntry(NS_PASSWORDMANAGER_CATEGORY, "Password Manager", NS_PASSWORDMANAGER_CONTRACTID, PR_TRUE, PR_TRUE, getter_Copies(prevEntry)); catman->AddCategoryEntry("app-startup", "Password Manager", NS_PASSWORDMANAGER_CONTRACTID, PR_TRUE, PR_TRUE, getter_Copies(prevEntry)); return NS_OK; } /* static */ NS_METHOD nsPasswordManager::Unregister(nsIComponentManager* aCompMgr, nsIFile* aPath, const char* aRegistryLocation, const nsModuleComponentInfo* aInfo) { nsresult rv; nsCOMPtr catman = do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); catman->DeleteCategoryEntry(NS_PASSWORDMANAGER_CATEGORY, NS_PASSWORDMANAGER_CONTRACTID, PR_TRUE); return NS_OK; } /* static */ void nsPasswordManager::Shutdown() { NS_IF_RELEASE(sDecoderRing); NS_IF_RELEASE(sPMBundle); NS_IF_RELEASE(sPasswordManager); } // nsIPasswordManager implementation NS_IMETHODIMP nsPasswordManager::AddUser(const nsACString& aHost, const nsAString& aUser, const nsAString& aPassword) { // Silently ignore an empty username/password entry. // There's no point in taking up space in the signon file with this. if (aUser.IsEmpty() && aPassword.IsEmpty()) return NS_OK; // Reject values that would cause problems when parsing the storage file nsresult rv = CheckLoginValues(aHost, EmptyString(), EmptyString(), EmptyCString()); NS_ENSURE_SUCCESS(rv, rv); // Check for an existing entry for this host + user if (!aHost.IsEmpty()) { SignonHashEntry *hashEnt; if (mSignonTable.Get(aHost, &hashEnt)) { nsString empty; SignonDataEntry *entry = nsnull; FindPasswordEntryInternal(hashEnt->head, aUser, empty, empty, &entry); if (entry) { // Just change the password return EncryptDataUCS2(aPassword, entry->passValue); } } } SignonDataEntry* entry = new SignonDataEntry(); if (NS_FAILED(EncryptDataUCS2(aUser, entry->userValue)) || NS_FAILED(EncryptDataUCS2(aPassword, entry->passValue))) { delete entry; return NS_ERROR_FAILURE; } AddSignonData(aHost, entry); WritePasswords(mSignonFile); return NS_OK; } NS_IMETHODIMP nsPasswordManager::RemoveUser(const nsACString& aHost, const nsAString& aUser) { SignonDataEntry* entry, *prevEntry = nsnull; SignonHashEntry* hashEnt; if (!mSignonTable.Get(aHost, &hashEnt)) return NS_ERROR_FAILURE; for (entry = hashEnt->head; entry; prevEntry = entry, entry = entry->next) { nsAutoString ptUser; if (!entry->userValue.IsEmpty() && NS_FAILED(DecryptData(entry->userValue, ptUser))) break; if (ptUser.Equals(aUser)) { if (prevEntry) prevEntry->next = entry->next; else hashEnt->head = entry->next; entry->next = nsnull; delete entry; if (!hashEnt->head) mSignonTable.Remove(aHost); // deletes hashEnt WritePasswords(mSignonFile); return NS_OK; } } return NS_ERROR_FAILURE; } NS_IMETHODIMP nsPasswordManager::AddReject(const nsACString& aHost) { // Reject values that would cause problems when parsing the storage file nsresult rv = CheckLoginValues(aHost, EmptyString(), EmptyString(), EmptyCString()); NS_ENSURE_SUCCESS(rv, rv); mRejectTable.Put(aHost, 1); WritePasswords(mSignonFile); return NS_OK; } NS_IMETHODIMP nsPasswordManager::RemoveReject(const nsACString& aHost) { mRejectTable.Remove(aHost); WritePasswords(mSignonFile); return NS_OK; } /* static */ PLDHashOperator PR_CALLBACK nsPasswordManager::BuildArrayEnumerator(const nsACString& aKey, SignonHashEntry* aEntry, void* aUserData) { nsIMutableArray* array = NS_STATIC_CAST(nsIMutableArray*, aUserData); for (SignonDataEntry* e = aEntry->head; e; e = e->next) array->AppendElement(new PasswordEntry(aKey, e), PR_FALSE); return PL_DHASH_NEXT; } NS_IMETHODIMP nsPasswordManager::GetEnumerator(nsISimpleEnumerator** aEnumerator) { // Build an array out of the hashtable nsCOMPtr signonArray; NS_NewArray(getter_AddRefs(signonArray)); mSignonTable.EnumerateRead(BuildArrayEnumerator, signonArray); return signonArray->Enumerate(aEnumerator); } /* static */ PLDHashOperator PR_CALLBACK nsPasswordManager::BuildRejectArrayEnumerator(const nsACString& aKey, PRInt32 aEntry, void* aUserData) { nsIMutableArray* array = NS_STATIC_CAST(nsIMutableArray*, aUserData); nsCOMPtr passwordEntry = new PasswordEntry(aKey, nsnull); array->AppendElement(passwordEntry, PR_FALSE); return PL_DHASH_NEXT; } NS_IMETHODIMP nsPasswordManager::GetRejectEnumerator(nsISimpleEnumerator** aEnumerator) { // Build an array out of the hashtable nsCOMPtr rejectArray; NS_NewArray(getter_AddRefs(rejectArray)); mRejectTable.EnumerateRead(BuildRejectArrayEnumerator, rejectArray); return rejectArray->Enumerate(aEnumerator); } // nsIPasswordManagerInternal implementation struct findEntryContext { nsPasswordManager* manager; const nsACString& hostURI; const nsAString& username; const nsAString& password; nsACString& hostURIFound; nsAString& usernameFound; nsAString& passwordFound; PRBool matched; findEntryContext(nsPasswordManager* aManager, const nsACString& aHostURI, const nsAString& aUsername, const nsAString& aPassword, nsACString& aHostURIFound, nsAString& aUsernameFound, nsAString& aPasswordFound) : manager(aManager), hostURI(aHostURI), username(aUsername), password(aPassword), hostURIFound(aHostURIFound), usernameFound(aUsernameFound), passwordFound(aPasswordFound), matched(PR_FALSE) { } }; /* static */ PLDHashOperator PR_CALLBACK nsPasswordManager::FindEntryEnumerator(const nsACString& aKey, SignonHashEntry* aEntry, void* aUserData) { findEntryContext* context = NS_STATIC_CAST(findEntryContext*, aUserData); nsPasswordManager* manager = context->manager; nsresult rv; SignonDataEntry* entry = nsnull; rv = manager->FindPasswordEntryInternal(aEntry->head, context->username, context->password, EmptyString(), &entry); if (NS_SUCCEEDED(rv) && entry) { if (NS_SUCCEEDED(DecryptData(entry->userValue, context->usernameFound)) && NS_SUCCEEDED(DecryptData(entry->passValue, context->passwordFound))) { context->matched = PR_TRUE; context->hostURIFound.Assign(context->hostURI); } return PL_DHASH_STOP; } return PL_DHASH_NEXT; } NS_IMETHODIMP nsPasswordManager::FindPasswordEntry(const nsACString& aHostURI, const nsAString& aUsername, const nsAString& aPassword, nsACString& aHostURIFound, nsAString& aUsernameFound, nsAString& aPasswordFound) { if (!aHostURI.IsEmpty()) { SignonHashEntry* hashEnt; if (mSignonTable.Get(aHostURI, &hashEnt)) { SignonDataEntry* entry; nsresult rv = FindPasswordEntryInternal(hashEnt->head, aUsername, aPassword, EmptyString(), &entry); if (NS_SUCCEEDED(rv) && entry) { if (NS_SUCCEEDED(DecryptData(entry->userValue, aUsernameFound)) && NS_SUCCEEDED(DecryptData(entry->passValue, aPasswordFound))) { aHostURIFound.Assign(aHostURI); } else { return NS_ERROR_FAILURE; } } return rv; } return NS_ERROR_FAILURE; } // No host given, so enumerate all entries in the hashtable findEntryContext context(this, aHostURI, aUsername, aPassword, aHostURIFound, aUsernameFound, aPasswordFound); mSignonTable.EnumerateRead(FindEntryEnumerator, &context); return NS_OK; } NS_IMETHODIMP nsPasswordManager::AddUserFull(const nsACString& aKey, const nsAString& aUser, const nsAString& aPassword, const nsAString& aUserFieldName, const nsAString& aPassFieldName) { // Silently ignore an empty username/password entry. // There's no point in taking up space in the signon file with this. if (aUser.IsEmpty() && aPassword.IsEmpty()) return NS_OK; // Reject values that would cause problems when parsing the storage file nsresult rv = CheckLoginValues(aKey, aUserFieldName, aPassFieldName, EmptyCString()); NS_ENSURE_SUCCESS(rv, rv); // Check for an existing entry for this host + user if (!aKey.IsEmpty()) { SignonHashEntry *hashEnt; if (mSignonTable.Get(aKey, &hashEnt)) { nsString empty; SignonDataEntry *entry = nsnull; FindPasswordEntryInternal(hashEnt->head, aUser, empty, empty, &entry); if (entry) { // Just change the password EncryptDataUCS2(aPassword, entry->passValue); // ... and update the field names...s entry->userField.Assign(aUserFieldName); entry->passField.Assign(aPassFieldName); return NS_OK; } } } SignonDataEntry* entry = new SignonDataEntry(); entry->userField.Assign(aUserFieldName); entry->passField.Assign(aPassFieldName); EncryptDataUCS2(aUser, entry->userValue); EncryptDataUCS2(aPassword, entry->passValue); AddSignonData(aKey, entry); WritePasswords(mSignonFile); return NS_OK; } NS_IMETHODIMP nsPasswordManager::ReadPasswords(nsIFile* aPasswordFile) { nsCOMPtr fileStream; NS_NewLocalFileInputStream(getter_AddRefs(fileStream), aPasswordFile); if (!fileStream) return NS_ERROR_OUT_OF_MEMORY; nsCOMPtr lineStream = do_QueryInterface(fileStream); NS_ASSERTION(lineStream, "File stream is not an nsILineInputStream"); // Read the header nsCAutoString utf8Buffer; PRBool moreData = PR_FALSE; nsresult rv = lineStream->ReadLine(utf8Buffer, &moreData); if (NS_FAILED(rv)) return NS_OK; PRBool updateEntries = PR_FALSE; PRBool writeOnFinish = PR_FALSE; if (utf8Buffer.Equals("#2c")) { // we've hit an older version of the file, but we can import it updateEntries = PR_TRUE; // make sure we write the new file out so we only do this once writeOnFinish = PR_TRUE; } else if (!utf8Buffer.Equals("#2d")) { NS_ERROR("Unexpected version header in signon file"); return NS_OK; } enum { STATE_REJECT, STATE_REALM, STATE_USERFIELD, STATE_USERVALUE, STATE_PASSFIELD, STATE_PASSVALUE, STATE_ACTION_ORIGIN } state = STATE_REJECT; nsCAutoString realm; SignonDataEntry* entry = nsnull; do { rv = lineStream->ReadLine(utf8Buffer, &moreData); if (NS_FAILED(rv)) return NS_OK; switch (state) { case STATE_REJECT: if (utf8Buffer.Equals(NS_LITERAL_CSTRING("."))) state = STATE_REALM; else mRejectTable.Put(utf8Buffer, 1); break; case STATE_REALM: realm.Assign(utf8Buffer); state = STATE_USERFIELD; break; case STATE_USERFIELD: // Commit any completed entry if (entry) { // Weed out empty username+password entries from corrupted signon files if (entry->userValue.IsEmpty() && entry->passValue.IsEmpty()) { NS_WARNING("Discarding empty password entry"); writeOnFinish = PR_TRUE; // so we won't get this on the next startup delete entry; } else { AddSignonData(realm, entry); } } // If the line is a ., we've reached the end of this realm's entries. if (utf8Buffer.Equals(NS_LITERAL_CSTRING("."))) { entry = nsnull; state = STATE_REALM; } else { entry = new SignonDataEntry(); CopyUTF8toUTF16(utf8Buffer, entry->userField); state = STATE_USERVALUE; } break; case STATE_USERVALUE: NS_ASSERTION(entry, "bad state"); CopyUTF8toUTF16(utf8Buffer, entry->userValue); state = STATE_PASSFIELD; break; case STATE_PASSFIELD: NS_ASSERTION(entry, "bad state"); // Strip off the leading "*" character CopyUTF8toUTF16(Substring(utf8Buffer, 1, utf8Buffer.Length() - 1), entry->passField); state = STATE_PASSVALUE; break; case STATE_PASSVALUE: NS_ASSERTION(entry, "bad state"); CopyUTF8toUTF16(utf8Buffer, entry->passValue); // if we're updating entries from an older file skip the action url if (updateEntries) state = STATE_USERFIELD; else state = STATE_ACTION_ORIGIN; break; case STATE_ACTION_ORIGIN: NS_ASSERTION(entry, "bad state"); entry->actionOrigin.Assign(utf8Buffer); state = STATE_USERFIELD; break; } } while (moreData); // Don't leak if the file ended unexpectedly delete entry; if (writeOnFinish) { fileStream->Close(); WritePasswords(mSignonFile); } return NS_OK; } // nsIObserver implementation NS_IMETHODIMP nsPasswordManager::Observe(nsISupports* aSubject, const char* aTopic, const PRUnichar* aData) { if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { nsCOMPtr branch = do_QueryInterface(aSubject); NS_ASSERTION(branch == mPrefBranch, "unexpected pref change notification"); branch->GetBoolPref("rememberSignons", &sRememberPasswords); } else if (!strcmp(aTopic, "app-startup")) { nsCOMPtr obsService = do_GetService("@mozilla.org/observer-service;1"); NS_ASSERTION(obsService, "No observer service"); obsService->AddObserver(this, "profile-after-change", PR_TRUE); } else if (!strcmp(aTopic, "profile-after-change")) LoadPasswords(); return NS_OK; } // nsIWebProgressListener implementation NS_IMETHODIMP nsPasswordManager::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, PRUint32 aStateFlags, nsresult aStatus) { if (!(aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) || !(aStateFlags & nsIWebProgressListener::STATE_TRANSFERRING) || NS_FAILED(aStatus)) return NS_OK; // Don't do anything if the global signon pref is disabled if (!SingleSignonEnabled()) return NS_OK; nsCOMPtr domWin; nsresult rv = aWebProgress->GetDOMWindow(getter_AddRefs(domWin)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr domDoc; domWin->GetDocument(getter_AddRefs(domDoc)); NS_ASSERTION(domDoc, "DOM window should always have a document!"); // For now, only prepare to prefill forms in HTML documents. nsCOMPtr htmlDoc = do_QueryInterface(domDoc); if (!htmlDoc) return NS_OK; if (aStateFlags & nsIWebProgressListener::STATE_RESTORING) return FillDocument(domDoc); nsCOMPtr targDoc = do_QueryInterface(domDoc); nsCOMPtr targWin = do_QueryInterface(domWin); nsIDOMEventListener* listener = NS_STATIC_CAST(nsIDOMFocusListener*, this); targDoc->AddEventListener(NS_LITERAL_STRING("DOMContentLoaded"), listener, PR_FALSE); targWin->AddEventListener(NS_LITERAL_STRING("pagehide"), listener, PR_FALSE); return NS_OK; } NS_IMETHODIMP nsPasswordManager::OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, PRInt32 aCurSelfProgress, PRInt32 aMaxSelfProgress, PRInt32 aCurTotalProgress, PRInt32 aMaxTotalProgress) { return NS_OK; } NS_IMETHODIMP nsPasswordManager::OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* aLocation) { return NS_OK; } NS_IMETHODIMP nsPasswordManager::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsresult aStatus, const PRUnichar* aMessage) { return NS_OK; } NS_IMETHODIMP nsPasswordManager::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, PRUint32 aState) { return NS_OK; } // nsIFormSubmitObserver implementation NS_IMETHODIMP nsPasswordManager::Notify(nsIContent* aFormNode, nsIDOMWindowInternal* aWindow, nsIURI* aActionURL, PRBool* aCancelSubmit) { // This function must never return a failure code or the form submit // will be cancelled. NS_ENSURE_TRUE(aWindow, NS_OK); // Don't do anything if the global signon pref is disabled if (!SingleSignonEnabled()) return NS_OK; // Check the reject list nsCAutoString realm; // XXX bug 281125: GetDocument() could sometimes be null here, hinting // XXX at a problem with document teardown while a modal dialog is posted. if (!GetPasswordRealm(aFormNode->GetOwnerDoc()->GetDocumentURI(), realm)) return NS_OK; PRInt32 rejectValue; if (mRejectTable.Get(realm, &rejectValue)) { // The user has opted to never save passwords for this site. return NS_OK; } nsCOMPtr formElement = do_QueryInterface(aFormNode); PRUint32 numControls; formElement->GetElementCount(&numControls); // Count the number of password fields in the form. nsCOMPtr userField; nsCOMArray passFields; PRUint32 i, firstPasswordIndex = numControls; for (i = 0; i < numControls; ++i) { nsCOMPtr control; formElement->GetElementAt(i, getter_AddRefs(control)); if (control->GetType() == NS_FORM_INPUT_PASSWORD) { nsCOMPtr elem = do_QueryInterface(control); passFields.AppendObject(elem); if (firstPasswordIndex == numControls) firstPasswordIndex = i; } } nsCOMPtr prompt; aWindow->GetPrompter(getter_AddRefs(prompt)); switch (passFields.Count()) { case 1: // normal login { // Search backwards from the password field to find a username field. for (PRInt32 j = (PRInt32) firstPasswordIndex - 1; j >= 0; --j) { nsCOMPtr control; formElement->GetElementAt(j, getter_AddRefs(control)); if (control->GetType() == NS_FORM_INPUT_TEXT) { userField = do_QueryInterface(control); break; } } // If the username field or the form has autocomplete=off, // we don't store the login nsAutoString autocomplete; if (userField) { nsCOMPtr userFieldElement = do_QueryInterface(userField); userFieldElement->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete); if (autocomplete.EqualsIgnoreCase("off")) return NS_OK; } nsCOMPtr formDOMEl = do_QueryInterface(aFormNode); formDOMEl->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete); if (autocomplete.EqualsIgnoreCase("off")) return NS_OK; nsCOMPtr passFieldElement = do_QueryInterface(passFields.ObjectAt(0)); passFieldElement->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete); if (autocomplete.EqualsIgnoreCase("off")) return NS_OK; // Check whether this signon is already stored. // Note that we don't prompt the user if only the password doesn't match; // we instead just silently change the stored password. nsAutoString userValue, passValue, userFieldName, passFieldName, actionOrigin; if (userField) { userField->GetValue(userValue); userField->GetName(userFieldName); } passFields.ObjectAt(0)->GetValue(passValue); passFields.ObjectAt(0)->GetName(passFieldName); // If the password is empty, there is no reason to store this login. if (passValue.IsEmpty()) return NS_OK; SignonHashEntry* hashEnt; nsCAutoString formActionOrigin; if (mSignonTable.Get(realm, &hashEnt)) { SignonDataEntry* entry; nsAutoString buffer; for (entry = hashEnt->head; entry; entry = entry->next) { if (entry->userField.Equals(userFieldName) && entry->passField.Equals(passFieldName)) { if (NS_FAILED(DecryptData(entry->userValue, buffer))) return NS_OK; if (buffer.Equals(userValue)) { if (NS_FAILED(DecryptData(entry->passValue, buffer))) return NS_OK; PRBool writePasswords = PR_FALSE; if (!buffer.Equals(passValue)) { if (NS_FAILED(EncryptDataUCS2(passValue, entry->passValue))) return NS_OK; writePasswords = PR_TRUE; } if (NS_SUCCEEDED(GetActionRealm(formElement, formActionOrigin)) && !entry->actionOrigin.Equals(formActionOrigin)) { // Reject values that would cause problems when parsing the storage file if (NS_SUCCEEDED(CheckLoginValues(EmptyCString(), EmptyString(), EmptyString(), formActionOrigin))) { // update the action URL entry->actionOrigin.Assign(formActionOrigin); writePasswords = PR_TRUE; } } if (writePasswords) WritePasswords(mSignonFile); return NS_OK; } } } } nsresult rv; nsCOMPtr bundleService = do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); nsCOMPtr brandBundle; rv = bundleService->CreateBundle("chrome://branding/locale/brand.properties", getter_AddRefs(brandBundle)); NS_ENSURE_SUCCESS(rv, rv); nsXPIDLString brandShortName; rv = brandBundle->GetStringFromName(NS_LITERAL_STRING("brandShortName").get(), getter_Copies(brandShortName)); NS_ENSURE_SUCCESS(rv, rv); const PRUnichar* formatArgs[1] = { brandShortName.get() }; nsAutoString dialogText; GetLocalizedString(NS_LITERAL_STRING("savePasswordText"), dialogText, PR_TRUE, formatArgs, 1); nsAutoString dialogTitle, neverButtonText, rememberButtonText, notNowButtonText; GetLocalizedString(NS_LITERAL_STRING("savePasswordTitle"), dialogTitle); GetLocalizedString(NS_LITERAL_STRING("neverForSiteButtonText"), neverButtonText); GetLocalizedString(NS_LITERAL_STRING("rememberButtonText"), rememberButtonText); GetLocalizedString(NS_LITERAL_STRING("notNowButtonText"), notNowButtonText); PRInt32 selection; prompt->ConfirmEx(dialogTitle.get(), dialogText.get(), nsIPrompt::BUTTON_POS_1_DEFAULT + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_1) + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2), rememberButtonText.get(), notNowButtonText.get(), neverButtonText.get(), nsnull, nsnull, &selection); if (selection == 0) { SignonDataEntry* entry = new SignonDataEntry(); entry->userField.Assign(userFieldName); entry->passField.Assign(passFieldName); // save the hostname of the action URL if (NS_FAILED(GetActionRealm(formElement, formActionOrigin))) { delete entry; return NS_OK; } entry->actionOrigin.Assign(formActionOrigin); if (NS_FAILED(EncryptDataUCS2(userValue, entry->userValue)) || NS_FAILED(EncryptDataUCS2(passValue, entry->passValue))) { delete entry; return NS_OK; } // Reject values that would cause problems when parsing the storage file // We do this after prompting, lest any code somehow change the values // during the prompting. nsresult rv = CheckLoginValues(realm, entry->userField, entry->passField, entry->actionOrigin); NS_ENSURE_SUCCESS(rv, NS_OK); AddSignonData(realm, entry); WritePasswords(mSignonFile); } else if (selection == 2) { // Reject values that would cause problems when parsing the storage file // We do this after prompting, lest any code run from prompt context. nsresult rv = CheckLoginValues(realm, EmptyString(), EmptyString(), EmptyCString()); NS_ENSURE_SUCCESS(rv, NS_OK); AddReject(realm); } } break; case 2: case 3: { // If the following conditions are true, we guess that this is a // password change page: // - there are 2 or 3 password fields on the page // - the fields do not all have the same value // - there is already a stored login for this realm // // In this situation, prompt the user to confirm that this is a password // change. SignonDataEntry* changeEntry = nsnull; nsAutoString value0, valueN; passFields.ObjectAt(0)->GetValue(value0); for (PRInt32 k = 1; k < passFields.Count(); ++k) { passFields.ObjectAt(k)->GetValue(valueN); if (!value0.Equals(valueN)) { SignonHashEntry* hashEnt; if (mSignonTable.Get(realm, &hashEnt)) { SignonDataEntry* entry = hashEnt->head; if (entry->next) { // Multiple stored logons, prompt for which username is // being changed. PRUint32 entryCount = 2; SignonDataEntry* temp = entry->next; while (temp->next) { ++entryCount; temp = temp->next; } nsAutoString* ptUsernames = new nsAutoString[entryCount]; const PRUnichar** formatArgs = new const PRUnichar*[entryCount]; temp = entry; for (PRUint32 arg = 0; arg < entryCount; ++arg) { if (NS_FAILED(DecryptData(temp->userValue, ptUsernames[arg]))) { delete [] formatArgs; delete [] ptUsernames; return NS_OK; } formatArgs[arg] = ptUsernames[arg].get(); temp = temp->next; } nsAutoString dialogTitle, dialogText; GetLocalizedString(NS_LITERAL_STRING("passwordChangeTitle"), dialogTitle); GetLocalizedString(NS_LITERAL_STRING("userSelectText"), dialogText); PRInt32 selection; PRBool confirm; prompt->Select(dialogTitle.get(), dialogText.get(), entryCount, formatArgs, &selection, &confirm); delete[] formatArgs; delete[] ptUsernames; if (confirm && selection >= 0) { changeEntry = entry; for (PRInt32 m = 0; m < selection; ++m) changeEntry = changeEntry->next; } } else { nsAutoString dialogTitle, dialogText, ptUser; if (NS_FAILED(DecryptData(entry->userValue, ptUser))) return NS_OK; const PRUnichar* formatArgs[1] = { ptUser.get() }; GetLocalizedString(NS_LITERAL_STRING("passwordChangeTitle"), dialogTitle); GetLocalizedString(NS_LITERAL_STRING("passwordChangeText"), dialogText, PR_TRUE, formatArgs, 1); PRInt32 selection; prompt->ConfirmEx(dialogTitle.get(), dialogText.get(), (nsIPrompt::BUTTON_TITLE_YES * nsIPrompt::BUTTON_POS_0) + (nsIPrompt::BUTTON_TITLE_NO * nsIPrompt::BUTTON_POS_1), nsnull, nsnull, nsnull, nsnull, nsnull, &selection); if (selection == 0) changeEntry = entry; } } break; } } if (changeEntry) { nsAutoString newValue; passFields.ObjectAt(1)->GetValue(newValue); if (NS_FAILED(EncryptDataUCS2(newValue, changeEntry->passValue))) return NS_OK; WritePasswords(mSignonFile); } } break; default: // no passwords or something odd; be safe and just don't store anything break; } return NS_OK; } // nsIDOMFocusListener implementation NS_IMETHODIMP nsPasswordManager::Focus(nsIDOMEvent* aEvent) { return NS_OK; } NS_IMETHODIMP nsPasswordManager::Blur(nsIDOMEvent* aEvent) { return FillPassword(aEvent); } NS_IMETHODIMP nsPasswordManager::HandleEvent(nsIDOMEvent* aEvent) { nsAutoString type; aEvent->GetType(type); if (type.EqualsLiteral("DOMAutoComplete")) return FillPassword(aEvent); nsCOMPtr target; aEvent->GetTarget(getter_AddRefs(target)); nsCOMPtr domDoc = do_QueryInterface(target); if (!domDoc) return NS_OK; if (type.EqualsLiteral("pagehide")) mAutoCompleteInputs.Enumerate(RemoveForDOMDocumentEnumerator, domDoc); else if (type.EqualsLiteral("DOMContentLoaded")) return FillDocument(domDoc); return NS_OK; } // Autocomplete implementation class UserAutoComplete : public nsIAutoCompleteResult { public: UserAutoComplete(const nsACString& aHost, const nsAString& aSearchString); virtual ~UserAutoComplete(); NS_DECL_ISUPPORTS NS_DECL_NSIAUTOCOMPLETERESULT nsVoidArray mArray; nsCString mHost; nsString mSearchString; PRInt32 mDefaultIndex; PRUint16 mResult; }; UserAutoComplete::UserAutoComplete(const nsACString& aHost, const nsAString& aSearchString) : mHost(aHost), mSearchString(aSearchString), mDefaultIndex(-1), mResult(RESULT_FAILURE) { } UserAutoComplete::~UserAutoComplete() { for (PRInt32 i = 0; i < mArray.Count(); ++i) nsMemory::Free(mArray.ElementAt(i)); } NS_IMPL_ISUPPORTS1(UserAutoComplete, nsIAutoCompleteResult) NS_IMETHODIMP UserAutoComplete::GetSearchString(nsAString& aString) { aString.Assign(mSearchString); return NS_OK; } NS_IMETHODIMP UserAutoComplete::GetSearchResult(PRUint16* aResult) { *aResult = mResult; return NS_OK; } NS_IMETHODIMP UserAutoComplete::GetDefaultIndex(PRInt32* aDefaultIndex) { *aDefaultIndex = mDefaultIndex; return NS_OK; } NS_IMETHODIMP UserAutoComplete::GetErrorDescription(nsAString& aDescription) { aDescription.Truncate(); return NS_OK; } NS_IMETHODIMP UserAutoComplete::GetMatchCount(PRUint32* aCount) { *aCount = mArray.Count(); return NS_OK; } NS_IMETHODIMP UserAutoComplete::GetValueAt(PRInt32 aIndex, nsAString& aValue) { aValue.Assign(NS_STATIC_CAST(PRUnichar*, mArray.ElementAt(aIndex))); return NS_OK; } NS_IMETHODIMP UserAutoComplete::GetCommentAt(PRInt32 aIndex, nsAString& aComment) { aComment.Truncate(); return NS_OK; } NS_IMETHODIMP UserAutoComplete::GetStyleAt(PRInt32 aIndex, nsAString& aHint) { aHint.Truncate(); return NS_OK; } NS_IMETHODIMP UserAutoComplete::RemoveValueAt(PRInt32 aIndex, PRBool aRemoveFromDB) { NS_ENSURE_TRUE(aIndex >= 0 && aIndex < mArray.Count(), NS_ERROR_INVALID_ARG); PRUnichar *user = NS_STATIC_CAST(PRUnichar*, mArray.ElementAt(aIndex)); if (aRemoveFromDB) sPasswordManager->RemoveUser(mHost, nsDependentString(user)); nsMemory::Free(user); mArray.RemoveElementAt(aIndex); return NS_OK; } PR_STATIC_CALLBACK(int) SortPRUnicharComparator(const void* aElement1, const void* aElement2, void* aData) { return nsCRT::strcmp(NS_STATIC_CAST(const PRUnichar*, aElement1), NS_STATIC_CAST(const PRUnichar*, aElement2)); } PRBool nsPasswordManager::AutoCompleteSearch(const nsAString& aSearchString, nsIAutoCompleteResult* aPreviousResult, nsIDOMHTMLInputElement* aElement, nsIAutoCompleteResult** aResult) { PRInt32 dummy; if (!SingleSignonEnabled() || !mAutoCompleteInputs.Get(aElement, &dummy)) return PR_FALSE; UserAutoComplete* result = nsnull; if (aPreviousResult) { // We have a list of results for a shorter search string, so just // filter them further based on the new search string. result = NS_STATIC_CAST(UserAutoComplete*, aPreviousResult); if (result->mArray.Count()) { for (PRInt32 i = result->mArray.Count() - 1; i >= 0; --i) { nsDependentString match(NS_STATIC_CAST(PRUnichar*, result->mArray.ElementAt(i))); if (aSearchString.Length() > match.Length() || !StringBeginsWith(match, aSearchString)) { nsMemory::Free(result->mArray.ElementAt(i)); result->mArray.RemoveElementAt(i); } } } } else { nsCOMPtr domDoc; aElement->GetOwnerDocument(getter_AddRefs(domDoc)); nsCOMPtr doc = do_QueryInterface(domDoc); nsCAutoString realm; if (!GetPasswordRealm(doc->GetDocumentURI(), realm)) { *aResult = nsnull; return NS_OK; } // Get all of the matches into an array that we can sort. result = new UserAutoComplete(realm, aSearchString); SignonHashEntry* hashEnt; if (mSignonTable.Get(realm, &hashEnt)) { // Protect against a reentrant call to DecryptData. For example, if // DecryptData causes the Master Password dialog to appear, we don't // want to respond to a blur on the input element by trying to prefill // the password. mAutoCompletingField = aElement; nsCOMPtr formEl; aElement->GetForm(getter_AddRefs(formEl)); if (!formEl) return NS_OK; nsCOMPtr form = do_QueryInterface(formEl); nsCAutoString formActionOrigin; if (NS_FAILED(GetActionRealm(form, formActionOrigin))) return NS_OK; for (SignonDataEntry* e = hashEnt->head; e; e = e->next) { nsAutoString userValue; if (NS_FAILED(DecryptData(e->userValue, userValue))) return NS_ERROR_FAILURE; // if we don't match actionOrigin, don't count this as a match if (!e->actionOrigin.IsEmpty() && !e->actionOrigin.Equals(formActionOrigin)) continue; if (aSearchString.Length() <= userValue.Length() && StringBeginsWith(userValue, aSearchString)) { PRUnichar* data = ToNewUnicode(userValue); if (data) result->mArray.AppendElement(data); } } mAutoCompletingField = nsnull; } if (result->mArray.Count()) { result->mArray.Sort(SortPRUnicharComparator, nsnull); result->mResult = nsIAutoCompleteResult::RESULT_SUCCESS; result->mDefaultIndex = 0; } else { result->mResult = nsIAutoCompleteResult::RESULT_NOMATCH; result->mDefaultIndex = -1; } } *aResult = result; NS_ADDREF(*aResult); return PR_TRUE; } /* static */ PLDHashOperator PR_CALLBACK nsPasswordManager::RemoveForDOMDocumentEnumerator(nsISupports* aKey, PRInt32& aEntry, void* aUserData) { nsIDOMDocument* domDoc = NS_STATIC_CAST(nsIDOMDocument*, aUserData); nsCOMPtr element = do_QueryInterface(aKey); nsCOMPtr elementDoc; element->GetOwnerDocument(getter_AddRefs(elementDoc)); if (elementDoc == domDoc) return PL_DHASH_REMOVE; return PL_DHASH_NEXT; } // internal methods /* Format of the single signon file: <1-line version header> . * <.....> . ..... . */ /* static */ PLDHashOperator PR_CALLBACK nsPasswordManager::WriteRejectEntryEnumerator(const nsACString& aKey, PRInt32 aEntry, void* aUserData) { nsIOutputStream* stream = NS_STATIC_CAST(nsIOutputStream*, aUserData); PRUint32 bytesWritten; nsCAutoString buffer(aKey); buffer.Append(NS_LINEBREAK); stream->Write(buffer.get(), buffer.Length(), &bytesWritten); return PL_DHASH_NEXT; } /* static */ PLDHashOperator PR_CALLBACK nsPasswordManager::WriteSignonEntryEnumerator(const nsACString& aKey, SignonHashEntry* aEntry, void* aUserData) { nsIOutputStream* stream = NS_STATIC_CAST(nsIOutputStream*, aUserData); PRUint32 bytesWritten; nsCAutoString buffer(aKey); buffer.Append(NS_LINEBREAK); stream->Write(buffer.get(), buffer.Length(), &bytesWritten); for (SignonDataEntry* e = aEntry->head; e; e = e->next) { NS_ConvertUCS2toUTF8 userField(e->userField); userField.Append(NS_LINEBREAK); stream->Write(userField.get(), userField.Length(), &bytesWritten); buffer.Assign(NS_ConvertUCS2toUTF8(e->userValue)); buffer.Append(NS_LINEBREAK); stream->Write(buffer.get(), buffer.Length(), &bytesWritten); buffer.Assign("*"); buffer.Append(NS_ConvertUCS2toUTF8(e->passField)); buffer.Append(NS_LINEBREAK); stream->Write(buffer.get(), buffer.Length(), &bytesWritten); buffer.Assign(NS_ConvertUCS2toUTF8(e->passValue)); buffer.Append(NS_LINEBREAK); stream->Write(buffer.get(), buffer.Length(), &bytesWritten); buffer.Assign(e->actionOrigin); buffer.Append(NS_LINEBREAK); stream->Write(buffer.get(), buffer.Length(), &bytesWritten); } buffer.Assign("." NS_LINEBREAK); stream->Write(buffer.get(), buffer.Length(), &bytesWritten); return PL_DHASH_NEXT; } // Wrapper function for ReadPasswords void nsPasswordManager::LoadPasswords() { if (sPasswordsLoaded) return; nsXPIDLCString signonFile; nsresult rv; rv = mPrefBranch->GetCharPref("SignonFileName2", getter_Copies(signonFile)); if (NS_FAILED(rv)) signonFile.Assign(NS_LITERAL_CSTRING("signons2.txt")); NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mSignonFile)); if (!mSignonFile) return; mSignonFile->AppendNative(signonFile); nsCAutoString path; mSignonFile->GetNativePath(path); PRBool signonExists = PR_FALSE; mSignonFile->Exists(&signonExists); if (signonExists) { if (NS_SUCCEEDED(ReadPasswords(mSignonFile))) sPasswordsLoaded = PR_TRUE; } else { // no current signons file, look for an older version rv = mPrefBranch->GetCharPref("SignonFileName", getter_Copies(signonFile)); if (NS_FAILED(rv)) signonFile.Assign(NS_LITERAL_CSTRING("signons.txt")); nsCOMPtr oldSignonFile; mSignonFile->GetParent(getter_AddRefs(oldSignonFile)); oldSignonFile->AppendNative(signonFile); if (NS_SUCCEEDED(ReadPasswords(oldSignonFile))) { sPasswordsLoaded = PR_TRUE; oldSignonFile->Remove(PR_FALSE); } } } void nsPasswordManager::WritePasswords(nsIFile* aPasswordFile) { nsCOMPtr fileStream; NS_NewLocalFileOutputStream(getter_AddRefs(fileStream), aPasswordFile, -1, 0600, 0); if (!fileStream) return; PRUint32 bytesWritten; // File header nsCAutoString buffer("#2d" NS_LINEBREAK); fileStream->Write(buffer.get(), buffer.Length(), &bytesWritten); // Write out the reject list. mRejectTable.EnumerateRead(WriteRejectEntryEnumerator, fileStream); buffer.Assign("." NS_LINEBREAK); fileStream->Write(buffer.get(), buffer.Length(), &bytesWritten); // Write out the signon data. mSignonTable.EnumerateRead(WriteSignonEntryEnumerator, fileStream); } void nsPasswordManager::AddSignonData(const nsACString& aRealm, SignonDataEntry* aEntry) { // See if there is already an entry for this URL SignonHashEntry* hashEnt; if (mSignonTable.Get(aRealm, &hashEnt)) { // Add this one at the front of the linked list aEntry->next = hashEnt->head; hashEnt->head = aEntry; } else { mSignonTable.Put(aRealm, new SignonHashEntry(aEntry)); } } /* static */ nsresult nsPasswordManager::DecryptData(const nsAString& aData, nsAString& aPlaintext) { NS_ConvertUCS2toUTF8 flatData(aData); char* buffer = nsnull; if (flatData.CharAt(0) == '~') { // This is a base64-encoded string. Strip off the ~ prefix. PRUint32 srcLength = flatData.Length() - 1; if (!(buffer = PL_Base64Decode(&(flatData.get())[1], srcLength, NULL))) return NS_ERROR_FAILURE; } else { // This is encrypted using nsISecretDecoderRing. EnsureDecoderRing(); if (!sDecoderRing) { NS_WARNING("Unable to get decoder ring service"); return NS_ERROR_FAILURE; } if (NS_FAILED(sDecoderRing->DecryptString(flatData.get(), &buffer))) return NS_ERROR_FAILURE; } aPlaintext.Assign(NS_ConvertUTF8toUCS2(buffer)); PR_Free(buffer); return NS_OK; } // Note that nsISecretDecoderRing encryption uses a pseudo-random salt value, // so it's not possible to test equality of two strings by comparing their // ciphertexts. We need to decrypt both strings and compare the plaintext. /* static */ nsresult nsPasswordManager::EncryptData(const nsAString& aPlaintext, nsACString& aEncrypted) { EnsureDecoderRing(); NS_ENSURE_TRUE(sDecoderRing, NS_ERROR_FAILURE); char* buffer; if (NS_FAILED(sDecoderRing->EncryptString(NS_ConvertUCS2toUTF8(aPlaintext).get(), &buffer))) return NS_ERROR_FAILURE; aEncrypted.Assign(buffer); PR_Free(buffer); return NS_OK; } /* static */ nsresult nsPasswordManager::EncryptDataUCS2(const nsAString& aPlaintext, nsAString& aEncrypted) { nsCAutoString buffer; nsresult rv = EncryptData(aPlaintext, buffer); NS_ENSURE_SUCCESS(rv, rv); aEncrypted.Assign(NS_ConvertUTF8toUCS2(buffer)); return NS_OK; } /* static */ void nsPasswordManager::EnsureDecoderRing() { if (!sDecoderRing) { CallGetService("@mozilla.org/security/sdr;1", &sDecoderRing); // Ensure that the master password (internal key) has been initialized. // If not, set a default empty master password. nsCOMPtr tokenDB = do_GetService(NS_PK11TOKENDB_CONTRACTID); if (!tokenDB) return; nsCOMPtr token; tokenDB->GetInternalKeyToken(getter_AddRefs(token)); PRBool needUserInit = PR_FALSE; token->GetNeedsUserInit(&needUserInit); if (needUserInit) token->InitPassword(EmptyString().get()); } } nsresult nsPasswordManager::FindPasswordEntryInternal(const SignonDataEntry* aEntry, const nsAString& aUser, const nsAString& aPassword, const nsAString& aUserField, SignonDataEntry** aResult) { // host has already been checked, so just look for user/password match. const SignonDataEntry* entry = aEntry; nsAutoString buffer; for (; entry; entry = entry->next) { PRBool matched; if (aUser.IsEmpty()) { matched = PR_TRUE; } else { if (NS_FAILED(DecryptData(entry->userValue, buffer))) { *aResult = nsnull; return NS_ERROR_FAILURE; } matched = aUser.Equals(buffer); } if (!matched) continue; if (aPassword.IsEmpty()) { matched = PR_TRUE; } else { if (NS_FAILED(DecryptData(entry->passValue, buffer))) { *aResult = nsnull; return NS_ERROR_FAILURE; } matched = aPassword.Equals(buffer); } if (!matched) continue; if (aUserField.IsEmpty()) matched = PR_TRUE; else matched = entry->userField.Equals(aUserField); if (matched) break; } if (entry) { *aResult = NS_CONST_CAST(SignonDataEntry*, entry); return NS_OK; } *aResult = nsnull; return NS_ERROR_FAILURE; } nsresult nsPasswordManager::FillDocument(nsIDOMDocument* aDomDoc) { nsCOMPtr htmlDoc = do_QueryInterface(aDomDoc); if (!htmlDoc) return NS_OK; nsCOMPtr forms; htmlDoc->GetForms(getter_AddRefs(forms)); nsCOMPtr doc = do_QueryInterface(aDomDoc); nsCAutoString realm; if (!GetPasswordRealm(doc->GetDocumentURI(), realm)) return NS_OK; SignonHashEntry* hashEnt; if (!mSignonTable.Get(realm, &hashEnt)) return NS_OK; PRUint32 formCount; forms->GetLength(&formCount); // check to see if we should formfill. failure is non-fatal PRBool prefillForm = PR_TRUE; mPrefBranch->GetBoolPref("prefillForms", &prefillForm); nsCAutoString formActionOrigin; // We can auto-prefill the username and password if there is only // one stored login that matches the username and password field names // on the form in question. Note that we only need to worry about a // single login per form. for (PRUint32 i = 0; i < formCount; ++i) { nsCOMPtr formNode; forms->Item(i, getter_AddRefs(formNode)); nsCOMPtr form = do_QueryInterface(formNode); SignonDataEntry* firstMatch = nsnull; PRBool attachedToInput = PR_FALSE; PRBool prefilledUser = PR_FALSE; nsCOMPtr userField, passField; nsCOMPtr temp; nsAutoString fieldType; // before we start iterating, make sure we have the action host if (NS_FAILED(GetActionRealm(form, formActionOrigin))) return NS_OK; for (SignonDataEntry* e = hashEnt->head; e; e = e->next) { nsCOMPtr foundNode; if (!(e->userField).IsEmpty()) { form->ResolveName(e->userField, getter_AddRefs(foundNode)); temp = do_QueryInterface(foundNode); } nsAutoString oldUserValue; if (temp) { temp->GetType(fieldType); if (!fieldType.Equals(NS_LITERAL_STRING("text"))) continue; temp->GetValue(oldUserValue); userField = temp; } else if ((e->passField).IsEmpty()) { // Happens sometimes when we import passwords from IE since // their form name match is case insensitive. In this case, // we'll just have to do a case insensitive search for the // userField and hope we get something. PRUint32 count; form->GetElementCount(&count); PRUint32 i; nsCOMPtr formControl; for (i = 0; i < count; i++) { form->GetElementAt(i, getter_AddRefs(formControl)); if (formControl && formControl->GetType() == NS_FORM_INPUT_TEXT) { nsCOMPtr inputField = do_QueryInterface(formControl); nsAutoString name; inputField->GetName(name); if (name.EqualsIgnoreCase(NS_ConvertUTF16toUTF8(e->userField).get())) { inputField->GetValue(oldUserValue); userField = inputField; foundNode = inputField; // Only the case differs, so CheckLoginValues() unneeded. e->userField.Assign(name); break; } } } } if (!(e->passField).IsEmpty()) { form->ResolveName(e->passField, getter_AddRefs(foundNode)); temp = do_QueryInterface(foundNode); } else if (userField) { // No password field name was supplied, try to locate one in the form, // but only if we have a username field. nsCOMPtr fc(do_QueryInterface(foundNode)); PRInt32 index = -1; form->IndexOfControl(fc, &index); if (index >= 0) { PRUint32 count; form->GetElementCount(&count); PRUint32 i; temp = nsnull; // Search forwards nsCOMPtr passField; for (i = index + 1; i < count; ++i) { form->GetElementAt(i, getter_AddRefs(passField)); if (passField && passField->GetType() == NS_FORM_INPUT_PASSWORD) { foundNode = passField; temp = do_QueryInterface(foundNode); } } if (!temp && index != 0) { // Search backwards i = index; do { form->GetElementAt(i, getter_AddRefs(passField)); if (passField && passField->GetType() == NS_FORM_INPUT_PASSWORD) { foundNode = passField; temp = do_QueryInterface(foundNode); } } while (i-- != 0); } } } nsAutoString oldPassValue; if (temp) { temp->GetType(fieldType); if (!fieldType.Equals(NS_LITERAL_STRING("password"))) continue; temp->GetValue(oldPassValue); passField = temp; if ((e->passField).IsEmpty()) { nsAutoString passName; passField->GetName(passName); // Reject values that would cause problems when parsing the storage file if (NS_SUCCEEDED(CheckLoginValues(EmptyCString(), EmptyString(), passName, EmptyCString()))) e->passField.Assign(passName); } } else { continue; } // if we don't match actionOrigin, don't count this as a match if (!e->actionOrigin.IsEmpty() && !e->actionOrigin.Equals(formActionOrigin)) continue; if (!oldUserValue.IsEmpty() && prefillForm) { // The page has prefilled a username. // If it matches any of our saved usernames, prefill the password // for that username. If there are multiple saved usernames, // we will also attach the autocomplete listener. prefilledUser = PR_TRUE; nsAutoString userValue; if (NS_FAILED(DecryptData(e->userValue, userValue))) return NS_OK; if (userValue.Equals(oldUserValue)) { nsAutoString passValue; if (NS_FAILED(DecryptData(e->passValue, passValue))) return NS_OK; passField->SetValue(passValue); } } if (firstMatch && userField && !attachedToInput) { // We've found more than one possible signon for this form. // Listen for blur and autocomplete events on the username field so // that we can attempt to prefill the password after the user has // entered the username. AttachToInput(userField); attachedToInput = PR_TRUE; } else { firstMatch = e; } } // If we found more than one match, attachedToInput will be true, // but if we found just one, we need to attach the autocomplete listener, // and fill in the username and password only if the HTML didn't prefill // the username. if (firstMatch && !attachedToInput) { if (!prefilledUser && prefillForm) { nsAutoString buffer; if (userField) { if (NS_FAILED(DecryptData(firstMatch->userValue, buffer))) return NS_OK; userField->SetValue(buffer); } if (NS_FAILED(DecryptData(firstMatch->passValue, buffer))) return NS_OK; passField->SetValue(buffer); } if (userField) AttachToInput(userField); } } return NS_OK; } nsresult nsPasswordManager::FillPassword(nsIDOMEvent* aEvent) { // Try to prefill the password for the just-changed username. nsCOMPtr target; aEvent->GetTarget(getter_AddRefs(target)); nsCOMPtr userField = do_QueryInterface(target); if (!userField || userField == mAutoCompletingField) return NS_OK; nsCOMPtr fieldContent = do_QueryInterface(userField); // The document may be null during teardown, for example as Windows // sends a blur event as a native widget is destroyed. nsIDocument *doc = fieldContent->GetDocument(); if (!doc) return NS_OK; nsCAutoString realm; if (!GetPasswordRealm(doc->GetDocumentURI(), realm)) return NS_OK; nsAutoString userValue; userField->GetValue(userValue); if (userValue.IsEmpty()) return NS_OK; nsAutoString fieldName; userField->GetName(fieldName); SignonHashEntry* hashEnt; if (!mSignonTable.Get(realm, &hashEnt)) return NS_OK; SignonDataEntry* foundEntry; FindPasswordEntryInternal(hashEnt->head, userValue, EmptyString(), fieldName, &foundEntry); if (!foundEntry) return NS_OK; nsCOMPtr formEl; userField->GetForm(getter_AddRefs(formEl)); if (!formEl) return NS_OK; nsCOMPtr form = do_QueryInterface(formEl); nsCAutoString formActionOrigin; GetActionRealm(form, formActionOrigin); if (NS_FAILED(GetActionRealm(form, formActionOrigin))) return NS_OK; if (!foundEntry->actionOrigin.IsEmpty() && !foundEntry->actionOrigin.Equals(formActionOrigin)) return NS_OK; nsCOMPtr foundNode; form->ResolveName(foundEntry->passField, getter_AddRefs(foundNode)); nsCOMPtr passField = do_QueryInterface(foundNode); if (!passField) return NS_OK; nsAutoString passValue; if (NS_SUCCEEDED(DecryptData(foundEntry->passValue, passValue))) passField->SetValue(passValue); return NS_OK; } void nsPasswordManager::AttachToInput(nsIDOMHTMLInputElement* aElement) { nsCOMPtr targ = do_QueryInterface(aElement); nsIDOMEventListener* listener = NS_STATIC_CAST(nsIDOMFocusListener*, this); targ->AddEventListener(NS_LITERAL_STRING("blur"), listener, PR_FALSE); targ->AddEventListener(NS_LITERAL_STRING("DOMAutoComplete"), listener, PR_FALSE); mAutoCompleteInputs.Put(aElement, 1); } PRBool nsPasswordManager::GetPasswordRealm(nsIURI* aURI, nsACString& aRealm) { // Note: this _is_ different from getting the uri's prePath! // We don't want to include a username or password that's part of the // URL in the host key... it will cause lookups to work incorrectly, and will // also cause usernames and passwords to be stored in cleartext. nsCAutoString buffer; aURI->GetScheme(buffer); aRealm.Append(buffer); aRealm.Append(NS_LITERAL_CSTRING("://")); aURI->GetHostPort(buffer); if (buffer.IsEmpty()) { // The scheme does not support hostnames, so don't attempt to save/restore // any signon data. (see bug 159484) return PR_FALSE; } aRealm.Append(buffer); return PR_TRUE; } /* static */ void nsPasswordManager::GetLocalizedString(const nsAString& key, nsAString& aResult, PRBool aIsFormatted, const PRUnichar** aFormatArgs, PRUint32 aFormatArgsLength) { if (!sPMBundle) { nsCOMPtr bundleService = do_GetService(NS_STRINGBUNDLE_CONTRACTID); bundleService->CreateBundle(kPMPropertiesURL, &sPMBundle); if (!sPMBundle) { NS_ERROR("string bundle not present"); return; } } nsXPIDLString str; if (aIsFormatted) sPMBundle->FormatStringFromName(PromiseFlatString(key).get(), aFormatArgs, aFormatArgsLength, getter_Copies(str)); else sPMBundle->GetStringFromName(PromiseFlatString(key).get(), getter_Copies(str)); aResult.Assign(str); } /* static */ nsresult nsPasswordManager::GetActionRealm(nsIForm* aForm, nsCString& aURL) { nsCOMPtr actionURI; nsCAutoString formActionOrigin; if (NS_FAILED(aForm->GetActionURL(getter_AddRefs(actionURI))) || !actionURI) return NS_ERROR_FAILURE; if (!GetPasswordRealm(actionURI, formActionOrigin)) return NS_ERROR_FAILURE; aURL.Assign(formActionOrigin); return NS_OK; } /* static */ PRBool nsPasswordManager::BadCharacterPresent(const nsAString &aString) { if (aString.FindChar('\r') >= 0) return PR_TRUE; if (aString.FindChar('\n') >= 0) return PR_TRUE; if (aString.FindChar('\0') >= 0) return PR_TRUE; return PR_FALSE; } /* static */ nsresult nsPasswordManager::CheckLoginValues(const nsACString &aHost, const nsAString &aUserField, const nsAString &aPassField, const nsACString &aActionOrigin) { // aHost if (BadCharacterPresent(NS_ConvertUTF8toUTF16(aHost))) { NS_WARNING("Login rejected, bad character in aHost"); return NS_ERROR_FAILURE; } // The aHost arg is used for both login entry hostnames and reject entry // hostnames ("never for this site"). A value of "." is not allowed for // reject entries. It's technically ok for login entries, but to keep the // code simple we'll disallow it anyway. if (aHost.EqualsLiteral(".")) { NS_WARNING("Login rejected, aHost can not be just a period"); return NS_ERROR_FAILURE; } // aUserField if (BadCharacterPresent(aUserField)) { NS_WARNING("Login rejected, bad character in aUserField"); return NS_ERROR_FAILURE; } if (aUserField.EqualsLiteral(".")) { NS_WARNING("Login rejected, aUserField can not be just a period"); return NS_ERROR_FAILURE; } // aPassField if (BadCharacterPresent(aPassField)) { NS_WARNING("Login rejected, bad character in aPassField"); return NS_ERROR_FAILURE; } // aActionOrigin if (BadCharacterPresent(NS_ConvertUTF8toUTF16(aActionOrigin))) { NS_WARNING("Login rejected, bad character in aActionOrigin"); return NS_ERROR_FAILURE; } if (aActionOrigin.EqualsLiteral(".")) { NS_WARNING("Login rejected, aActionOrigin can not be just a period"); return NS_ERROR_FAILURE; } return NS_OK; }