RetroZilla/toolkit/components/passwordmgr/base/nsPasswordManager.cpp
2015-10-20 23:03:22 -04:00

2200 lines
64 KiB
C++

/* -*- 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 <bryner@brianryner.com>
*
* 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<nsIPrefService> 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<nsIPrefBranch2> 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<nsIObserverService> obsService = do_GetService("@mozilla.org/observer-service;1");
NS_ASSERTION(obsService, "No observer service");
obsService->AddObserver(this, NS_EARLYFORMSUBMIT_SUBJECT, PR_TRUE);
nsCOMPtr<nsIDocumentLoader> docLoaderService = do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID);
NS_ASSERTION(docLoaderService, "No document loader service");
nsCOMPtr<nsIWebProgress> 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<nsIPasswordManager> 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<nsICategoryManager> 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<nsICategoryManager> 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;
// 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)
{
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<nsIMutableArray> 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<nsIPassword> 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<nsIMutableArray> 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;
// 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<nsIInputStream> fileStream;
NS_NewLocalFileInputStream(getter_AddRefs(fileStream), aPasswordFile);
if (!fileStream)
return NS_ERROR_OUT_OF_MEMORY;
nsCOMPtr<nsILineInputStream> 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<nsIPrefBranch> branch = do_QueryInterface(aSubject);
NS_ASSERTION(branch == mPrefBranch, "unexpected pref change notification");
branch->GetBoolPref("rememberSignons", &sRememberPasswords);
} else if (!strcmp(aTopic, "app-startup")) {
nsCOMPtr<nsIObserverService> 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"))
nsCOMPtr<nsIPasswordManager> pm = do_GetService(NS_PASSWORDMANAGER_CONTRACTID);
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<nsIDOMWindow> domWin;
nsresult rv = aWebProgress->GetDOMWindow(getter_AddRefs(domWin));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIDOMDocument> 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<nsIDOMHTMLDocument> htmlDoc = do_QueryInterface(domDoc);
if (!htmlDoc)
return NS_OK;
if (aStateFlags & nsIWebProgressListener::STATE_RESTORING)
return FillDocument(domDoc);
nsCOMPtr<nsIDOMEventTarget> targDoc = do_QueryInterface(domDoc);
nsCOMPtr<nsIDOMEventTarget> 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<nsIForm> formElement = do_QueryInterface(aFormNode);
PRUint32 numControls;
formElement->GetElementCount(&numControls);
// Count the number of password fields in the form.
nsCOMPtr<nsIDOMHTMLInputElement> userField;
nsCOMArray<nsIDOMHTMLInputElement> passFields;
PRUint32 i, firstPasswordIndex = numControls;
for (i = 0; i < numControls; ++i) {
nsCOMPtr<nsIFormControl> control;
formElement->GetElementAt(i, getter_AddRefs(control));
if (control->GetType() == NS_FORM_INPUT_PASSWORD) {
nsCOMPtr<nsIDOMHTMLInputElement> elem = do_QueryInterface(control);
passFields.AppendObject(elem);
if (firstPasswordIndex == numControls)
firstPasswordIndex = i;
}
}
nsCOMPtr<nsIPrompt> 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<nsIFormControl> 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<nsIDOMElement> userFieldElement = do_QueryInterface(userField);
userFieldElement->GetAttribute(NS_LITERAL_STRING("autocomplete"),
autocomplete);
if (autocomplete.EqualsIgnoreCase("off"))
return NS_OK;
}
nsCOMPtr<nsIDOMElement> formDOMEl = do_QueryInterface(aFormNode);
formDOMEl->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete);
if (autocomplete.EqualsIgnoreCase("off"))
return NS_OK;
nsCOMPtr<nsIDOMElement> 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)) {
// update the action URL
entry->actionOrigin.Assign(formActionOrigin);
writePasswords = PR_TRUE;
}
if (writePasswords)
WritePasswords(mSignonFile);
return NS_OK;
}
}
}
}
nsresult rv;
nsCOMPtr<nsIStringBundleService> bundleService =
do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
nsCOMPtr<nsIStringBundle> 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;
}
AddSignonData(realm, entry);
WritePasswords(mSignonFile);
} else if (selection == 2) {
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<nsIDOMEventTarget> target;
aEvent->GetTarget(getter_AddRefs(target));
nsCOMPtr<nsIDOMDocument> 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<nsIDOMDocument> domDoc;
aElement->GetOwnerDocument(getter_AddRefs(domDoc));
nsCOMPtr<nsIDocument> 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<nsIDOMHTMLFormElement> formEl;
aElement->GetForm(getter_AddRefs(formEl));
if (!formEl)
return NS_OK;
nsCOMPtr<nsIForm> 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<nsIDOMHTMLInputElement> element = do_QueryInterface(aKey);
nsCOMPtr<nsIDOMDocument> 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>
<Reject list URL #1>
<Reject list URL #2>
.
<Saved URL #1 realm>
<Saved URL #1 username field name>
<Encrypted Saved URL #1 username field value>
*<Saved URL #1 password field name>
<Encrypted Saved URL #1 password field value>
<Saved URL #1 username #2 field name>
<.....>
.
<Saved URL #2 realm>
.....
<Encrypted Saved URL #N password field value>
.
<EOF>
*/
/* 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<nsIFile> 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<nsIOutputStream> 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<nsIPK11TokenDB> tokenDB = do_GetService(NS_PK11TOKENDB_CONTRACTID);
if (!tokenDB)
return;
nsCOMPtr<nsIPK11Token> 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<nsIDOMHTMLDocument> htmlDoc = do_QueryInterface(aDomDoc);
if (!htmlDoc)
return NS_OK;
nsCOMPtr<nsIDOMHTMLCollection> forms;
htmlDoc->GetForms(getter_AddRefs(forms));
nsCOMPtr<nsIDocument> 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<nsIDOMNode> formNode;
forms->Item(i, getter_AddRefs(formNode));
nsCOMPtr<nsIForm> form = do_QueryInterface(formNode);
SignonDataEntry* firstMatch = nsnull;
PRBool attachedToInput = PR_FALSE;
PRBool prefilledUser = PR_FALSE;
nsCOMPtr<nsIDOMHTMLInputElement> userField, passField;
nsCOMPtr<nsIDOMHTMLInputElement> 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<nsISupports> 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<nsIFormControl> formControl;
for (i = 0; i < count; i++) {
form->GetElementAt(i, getter_AddRefs(formControl));
if (formControl &&
formControl->GetType() == NS_FORM_INPUT_TEXT) {
nsCOMPtr<nsIDOMHTMLInputElement> inputField = do_QueryInterface(formControl);
nsAutoString name;
inputField->GetName(name);
if (name.EqualsIgnoreCase(NS_ConvertUTF16toUTF8(e->userField).get())) {
inputField->GetValue(oldUserValue);
userField = inputField;
foundNode = inputField;
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<nsIFormControl> 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<nsIFormControl> 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())
passField->GetName(e->passField);
} 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<nsIDOMEventTarget> target;
aEvent->GetTarget(getter_AddRefs(target));
nsCOMPtr<nsIDOMHTMLInputElement> userField = do_QueryInterface(target);
if (!userField || userField == mAutoCompletingField)
return NS_OK;
nsCOMPtr<nsIContent> 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<nsIDOMHTMLFormElement> formEl;
userField->GetForm(getter_AddRefs(formEl));
if (!formEl)
return NS_OK;
nsCOMPtr<nsIForm> form = do_QueryInterface(formEl);
nsCAutoString formActionOrigin;
GetActionRealm(form, formActionOrigin);
if (NS_FAILED(GetActionRealm(form, formActionOrigin)) ||
!foundEntry->actionOrigin.Equals(formActionOrigin))
return NS_OK;
nsCOMPtr<nsISupports> foundNode;
form->ResolveName(foundEntry->passField, getter_AddRefs(foundNode));
nsCOMPtr<nsIDOMHTMLInputElement> 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<nsIDOMEventTarget> 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<nsIStringBundleService> 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<nsIURI> 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;
}