// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/login/parallel_authenticator.h"
#include <string>
#include <vector>
#include "base/file_path.h"
#include "base/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/string_util.h"
#include "base/synchronization/lock.h"
#include "crypto/third_party/nss/blapi.h"
#include "crypto/third_party/nss/sha256.h"
#include "chrome/browser/chromeos/cros/cryptohome_library.h"
#include "chrome/browser/chromeos/login/auth_response_handler.h"
#include "chrome/browser/chromeos/login/authentication_notification_details.h"
#include "chrome/browser/chromeos/login/login_status_consumer.h"
#include "chrome/browser/chromeos/login/ownership_service.h"
#include "chrome/browser/chromeos/login/user_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/net/gaia/gaia_auth_fetcher.h"
#include "chrome/common/net/gaia/gaia_constants.h"
#include "content/browser/browser_thread.h"
#include "content/common/notification_service.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/url_request/url_request_status.h"
#include "third_party/libjingle/source/talk/base/urlencode.h"
using base::Time;
using base::TimeDelta;
using file_util::GetFileSize;
using file_util::PathExists;
using file_util::ReadFile;
using file_util::ReadFileToString;
namespace chromeos {
// static
const char ParallelAuthenticator::kLocalaccountFile[] = "localaccount";
// static
const int ParallelAuthenticator::kClientLoginTimeoutMs = 10000;
// static
const int ParallelAuthenticator::kLocalaccountRetryIntervalMs = 20;
const int kPassHashLen = 32;
ParallelAuthenticator::ParallelAuthenticator(LoginStatusConsumer* consumer)
: Authenticator(consumer),
already_reported_success_(false),
checked_for_localaccount_(false) {
CHECK(chromeos::CrosLibrary::Get()->EnsureLoaded());
// If not already owned, this is a no-op. If it is, this loads the owner's
// public key off of disk.
OwnershipService::GetSharedInstance()->StartLoadOwnerKeyAttempt();
}
ParallelAuthenticator::~ParallelAuthenticator() {}
bool ParallelAuthenticator::AuthenticateToLogin(
Profile* profile,
const std::string& username,
const std::string& password,
const std::string& login_token,
const std::string& login_captcha) {
std::string canonicalized = Authenticator::Canonicalize(username);
current_state_.reset(
new AuthAttemptState(canonicalized,
password,
HashPassword(password),
login_token,
login_captcha,
!UserManager::Get()->IsKnownUser(canonicalized)));
mounter_ = CryptohomeOp::CreateMountAttempt(current_state_.get(),
this,
false /* don't create */);
current_online_ = new OnlineAttempt(current_state_.get(), this);
// Sadly, this MUST be on the UI thread due to sending DBus traffic :-/
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(mounter_.get(), &CryptohomeOp::Initiate));
current_online_->Initiate(profile);
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableMethod(this,
&ParallelAuthenticator::LoadLocalaccount,
std::string(kLocalaccountFile)));
return true;
}
bool ParallelAuthenticator::AuthenticateToUnlock(const std::string& username,
const std::string& password) {
current_state_.reset(
new AuthAttemptState(Authenticator::Canonicalize(username),
HashPassword(password)));
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableMethod(this,
&ParallelAuthenticator::LoadLocalaccount,
std::string(kLocalaccountFile)));
key_checker_ = CryptohomeOp::CreateCheckKeyAttempt(current_state_.get(),
this);
// Sadly, this MUST be on the UI thread due to sending DBus traffic :-/
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(key_checker_.get(), &CryptohomeOp::Initiate));
return true;
}
void ParallelAuthenticator::LoginOffTheRecord() {
current_state_.reset(new AuthAttemptState("", "", "", "", "", false));
guest_mounter_ =
CryptohomeOp::CreateMountGuestAttempt(current_state_.get(), this);
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
guest_mounter_->Initiate();
}
void ParallelAuthenticator::OnLoginSuccess(
const GaiaAuthConsumer::ClientLoginResult& credentials,
bool request_pending) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
VLOG(1) << "Login success";
// Send notification of success
AuthenticationNotificationDetails details(true);
NotificationService::current()->Notify(
NotificationType::LOGIN_AUTHENTICATION,
NotificationService::AllSources(),
Details<AuthenticationNotificationDetails>(&details));
{
base::AutoLock for_this_block(success_lock_);
already_reported_success_ = true;
}
consumer_->OnLoginSuccess(current_state_->username,
current_state_->password,
credentials,
request_pending);
}
void ParallelAuthenticator::OnOffTheRecordLoginSuccess() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// Send notification of success
AuthenticationNotificationDetails details(true);
NotificationService::current()->Notify(
NotificationType::LOGIN_AUTHENTICATION,
NotificationService::AllSources(),
Details<AuthenticationNotificationDetails>(&details));
consumer_->OnOffTheRecordLoginSuccess();
}
void ParallelAuthenticator::OnPasswordChangeDetected(
const GaiaAuthConsumer::ClientLoginResult& credentials) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
consumer_->OnPasswordChangeDetected(credentials);
}
void ParallelAuthenticator::CheckLocalaccount(const LoginFailure& error) {
{
base::AutoLock for_this_block(localaccount_lock_);
VLOG(2) << "Checking localaccount";
if (!checked_for_localaccount_) {
BrowserThread::PostDelayedTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableMethod(this,
&ParallelAuthenticator::CheckLocalaccount,
error),
kLocalaccountRetryIntervalMs);
return;
}
}
if (!localaccount_.empty() && localaccount_ == current_state_->username) {
// Success. Go mount a tmpfs for the profile, if necessary.
if (!current_state_->unlock) {
guest_mounter_ =
CryptohomeOp::CreateMountGuestAttempt(current_state_.get(), this);
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(guest_mounter_.get(), &CryptohomeOp::Initiate));
} else {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::OnLoginSuccess,
GaiaAuthConsumer::ClientLoginResult(), false));
}
} else {
// Not the localaccount. Fail, passing along cached error info.
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::OnLoginFailure, error));
}
}
void ParallelAuthenticator::OnLoginFailure(const LoginFailure& error) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// Send notification of failure
AuthenticationNotificationDetails details(false);
NotificationService::current()->Notify(
NotificationType::LOGIN_AUTHENTICATION,
NotificationService::AllSources(),
Details<AuthenticationNotificationDetails>(&details));
LOG(WARNING) << "Login failed: " << error.GetErrorString();
consumer_->OnLoginFailure(error);
}
void ParallelAuthenticator::RecoverEncryptedData(
const std::string& old_password,
const GaiaAuthConsumer::ClientLoginResult& credentials) {
std::string old_hash = HashPassword(old_password);
key_migrator_ = CryptohomeOp::CreateMigrateAttempt(current_state_.get(),
this,
true,
old_hash);
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
NewRunnableMethod(this,
&ParallelAuthenticator::ResyncRecoverHelper,
key_migrator_));
}
void ParallelAuthenticator::ResyncEncryptedData(
const GaiaAuthConsumer::ClientLoginResult& credentials) {
data_remover_ =
CryptohomeOp::CreateRemoveAttempt(current_state_.get(), this);
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
NewRunnableMethod(this,
&ParallelAuthenticator::ResyncRecoverHelper,
data_remover_));
}
void ParallelAuthenticator::ResyncRecoverHelper(CryptohomeOp* to_initiate) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
current_state_->ResetCryptohomeStatus();
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(to_initiate, &CryptohomeOp::Initiate));
}
void ParallelAuthenticator::RetryAuth(Profile* profile,
const std::string& username,
const std::string& password,
const std::string& login_token,
const std::string& login_captcha) {
reauth_state_.reset(
new AuthAttemptState(Authenticator::Canonicalize(username),
password,
HashPassword(password),
login_token,
login_captcha,
false /* not a new user */));
current_online_ = new OnlineAttempt(reauth_state_.get(), this);
current_online_->Initiate(profile);
}
void ParallelAuthenticator::Resolve() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
bool request_pending = false;
bool create = false;
ParallelAuthenticator::AuthState state = ResolveState();
VLOG(1) << "Resolved state to: " << state;
switch (state) {
case CONTINUE:
case POSSIBLE_PW_CHANGE:
case NO_MOUNT:
// These are intermediate states; we need more info from a request that
// is still pending.
break;
case FAILED_MOUNT:
// In this case, whether login succeeded or not, we can't log
// the user in because their data is horked. So, override with
// the appropriate failure.
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(
this,
&ParallelAuthenticator::OnLoginFailure,
LoginFailure(LoginFailure::COULD_NOT_MOUNT_CRYPTOHOME)));
break;
case FAILED_REMOVE:
// In this case, we tried to remove the user's old cryptohome at her
// request, and the remove failed.
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::OnLoginFailure,
LoginFailure(LoginFailure::DATA_REMOVAL_FAILED)));
break;
case FAILED_TMPFS:
// In this case, we tried to mount a tmpfs for BWSI or the localaccount
// user and failed.
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::OnLoginFailure,
LoginFailure(LoginFailure::COULD_NOT_MOUNT_TMPFS)));
break;
case CREATE_NEW:
create = true;
case RECOVER_MOUNT:
current_state_->ResetCryptohomeStatus();
mounter_ = CryptohomeOp::CreateMountAttempt(current_state_.get(),
this,
create);
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(mounter_.get(), &CryptohomeOp::Initiate));
break;
case NEED_OLD_PW:
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this,
&ParallelAuthenticator::OnPasswordChangeDetected,
current_state_->credentials()));
break;
case ONLINE_FAILED:
// In this case, we know online login was rejected because the account
// is disabled or something similarly fatal. Sending the user through
// the same path they get when their password is rejected is cleaner
// for now.
// TODO(cmasone): optimize this so that we don't send the user through
// the 'changed password' path when we know doing so won't succeed.
case NEED_NEW_PW:
{
base::AutoLock for_this_block(success_lock_);
if (!already_reported_success_) {
// This allows us to present the same behavior for "online:
// fail, offline: ok", regardless of the order in which we
// receive the results. There will be cases in which we get
// the online failure some time after the offline success,
// so we just force all cases in this category to present like this:
// OnLoginSuccess(..., ..., true) -> OnLoginFailure().
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::OnLoginSuccess,
current_state_->credentials(), true));
}
}
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::OnLoginFailure,
(reauth_state_.get() ?
reauth_state_->online_outcome() :
current_state_->online_outcome())));
break;
case HAVE_NEW_PW:
key_migrator_ =
CryptohomeOp::CreateMigrateAttempt(reauth_state_.get(),
this,
true,
current_state_->ascii_hash);
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(key_migrator_.get(), &CryptohomeOp::Initiate));
break;
case OFFLINE_LOGIN:
VLOG(2) << "Offline login";
request_pending = !current_state_->online_complete();
// Fall through.
case UNLOCK:
// Fall through.
case ONLINE_LOGIN:
VLOG(2) << "Online login";
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::OnLoginSuccess,
current_state_->credentials(), request_pending));
break;
case LOCAL_LOGIN:
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
NewRunnableMethod(
this,
&ParallelAuthenticator::OnOffTheRecordLoginSuccess));
break;
case LOGIN_FAILED:
current_state_->ResetCryptohomeStatus();
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableMethod(this, &ParallelAuthenticator::CheckLocalaccount,
current_state_->online_outcome()));
break;
default:
NOTREACHED();
break;
}
}
ParallelAuthenticator::AuthState ParallelAuthenticator::ResolveState() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
// If we haven't mounted the user's home dir yet, we can't be done.
// We never get past here if a cryptohome op is still pending.
// This is an important invariant.
if (!current_state_->cryptohome_complete())
return CONTINUE;
AuthState state = (reauth_state_.get() ? ResolveReauthState() : CONTINUE);
if (state != CONTINUE)
return state;
if (current_state_->cryptohome_outcome())
state = ResolveCryptohomeSuccessState();
else
state = ResolveCryptohomeFailureState();
DCHECK(current_state_->cryptohome_complete()); // Ensure invariant holds.
key_migrator_ = NULL;
data_remover_ = NULL;
guest_mounter_ = NULL;
key_checker_ = NULL;
if (state != POSSIBLE_PW_CHANGE &&
state != NO_MOUNT &&
state != OFFLINE_LOGIN)
return state;
if (current_state_->online_complete()) {
if (current_state_->online_outcome().reason() == LoginFailure::NONE) {
// Online attempt succeeded as well, so combine the results.
return ResolveOnlineSuccessState(state);
}
// Online login attempt was rejected or failed to occur.
return ResolveOnlineFailureState(state);
}
// if online isn't complete yet, just return the offline result.
return state;
}
ParallelAuthenticator::AuthState
ParallelAuthenticator::ResolveReauthState() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
if (reauth_state_->cryptohome_complete()) {
if (!reauth_state_->cryptohome_outcome()) {
// If we've tried to migrate and failed, log the error and just wait
// til next time the user logs in to migrate their cryptohome key.
LOG(ERROR) << "Failed to migrate cryptohome key: "
<< reauth_state_->cryptohome_code();
}
reauth_state_.reset(NULL);
return ONLINE_LOGIN;
}
// Haven't tried the migrate yet, must be processing the online auth attempt.
if (!reauth_state_->online_complete()) {
NOTREACHED(); // Shouldn't be here at all, if online reauth isn't done!
return CONTINUE;
}
return (reauth_state_->online_outcome().reason() == LoginFailure::NONE) ?
HAVE_NEW_PW : NEED_NEW_PW;
}
ParallelAuthenticator::AuthState
ParallelAuthenticator::ResolveCryptohomeFailureState() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
if (data_remover_.get())
return FAILED_REMOVE;
if (guest_mounter_.get())
return FAILED_TMPFS;
if (key_migrator_.get())
return NEED_OLD_PW;
if (key_checker_.get())
return LOGIN_FAILED;
if (current_state_->cryptohome_code() ==
chromeos::kCryptohomeMountErrorKeyFailure) {
// If we tried a mount but they used the wrong key, we may need to
// ask the user for her old password. We'll only know once we've
// done the online check.
return POSSIBLE_PW_CHANGE;
}
if (current_state_->cryptohome_code() ==
chromeos::kCryptohomeMountErrorUserDoesNotExist) {
// If we tried a mount but the user did not exist, then we should wait
// for online login to succeed and try again with the "create" flag set.
return NO_MOUNT;
}
return FAILED_MOUNT;
}
ParallelAuthenticator::AuthState
ParallelAuthenticator::ResolveCryptohomeSuccessState() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
if (data_remover_.get())
return CREATE_NEW;
if (guest_mounter_.get())
return LOCAL_LOGIN;
if (key_migrator_.get())
return RECOVER_MOUNT;
if (key_checker_.get())
return UNLOCK;
return OFFLINE_LOGIN;
}
ParallelAuthenticator::AuthState
ParallelAuthenticator::ResolveOnlineFailureState(
ParallelAuthenticator::AuthState offline_state) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
if (offline_state == OFFLINE_LOGIN) {
if (current_state_->online_outcome().error().state() ==
GoogleServiceAuthError::CONNECTION_FAILED) {
// Couldn't do an online check, so just go with the offline result.
return OFFLINE_LOGIN;
}
// Otherwise, online login was rejected!
if (current_state_->online_outcome().error().state() ==
GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) {
return NEED_NEW_PW;
}
return ONLINE_FAILED;
}
return LOGIN_FAILED;
}
ParallelAuthenticator::AuthState
ParallelAuthenticator::ResolveOnlineSuccessState(
ParallelAuthenticator::AuthState offline_state) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
switch (offline_state) {
case POSSIBLE_PW_CHANGE:
return NEED_OLD_PW;
case NO_MOUNT:
return CREATE_NEW;
case OFFLINE_LOGIN:
return ONLINE_LOGIN;
default:
NOTREACHED();
return offline_state;
}
}
void ParallelAuthenticator::LoadSystemSalt() {
if (!system_salt_.empty())
return;
system_salt_ = CrosLibrary::Get()->GetCryptohomeLibrary()->GetSystemSalt();
CHECK(!system_salt_.empty());
CHECK_EQ(system_salt_.size() % 2, 0U);
}
void ParallelAuthenticator::LoadLocalaccount(const std::string& filename) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
{
base::AutoLock for_this_block(localaccount_lock_);
if (checked_for_localaccount_)
return;
}
FilePath localaccount_file;
std::string localaccount;
if (PathService::Get(base::DIR_EXE, &localaccount_file)) {
localaccount_file = localaccount_file.Append(filename);
VLOG(2) << "Looking for localaccount in " << localaccount_file.value();
ReadFileToString(localaccount_file, &localaccount);
TrimWhitespaceASCII(localaccount, TRIM_TRAILING, &localaccount);
VLOG(1) << "Loading localaccount: " << localaccount;
} else {
VLOG(1) << "Assuming no localaccount";
}
SetLocalaccount(localaccount);
}
void ParallelAuthenticator::SetLocalaccount(const std::string& new_name) {
localaccount_ = new_name;
{ // extra braces for clarity about AutoLock scope.
base::AutoLock for_this_block(localaccount_lock_);
checked_for_localaccount_ = true;
}
}
std::string ParallelAuthenticator::HashPassword(const std::string& password) {
// Get salt, ascii encode, update sha with that, then update with ascii
// of password, then end.
std::string ascii_salt = SaltAsAscii();
unsigned char passhash_buf[kPassHashLen];
char ascii_buf[kPassHashLen + 1];
// Hash salt and password
SHA256Context ctx;
SHA256_Begin(&ctx);
SHA256_Update(&ctx,
reinterpret_cast<const unsigned char*>(ascii_salt.data()),
static_cast<unsigned int>(ascii_salt.length()));
SHA256_Update(&ctx,
reinterpret_cast<const unsigned char*>(password.data()),
static_cast<unsigned int>(password.length()));
SHA256_End(&ctx,
passhash_buf,
NULL,
static_cast<unsigned int>(sizeof(passhash_buf)));
std::vector<unsigned char> passhash(passhash_buf,
passhash_buf + sizeof(passhash_buf));
BinaryToHex(passhash,
passhash.size() / 2, // only want top half, at least for now.
ascii_buf,
sizeof(ascii_buf));
return std::string(ascii_buf, sizeof(ascii_buf) - 1);
}
std::string ParallelAuthenticator::SaltAsAscii() {
LoadSystemSalt(); // no-op if it's already loaded.
unsigned int salt_len = system_salt_.size();
char ascii_salt[2 * salt_len + 1];
if (ParallelAuthenticator::BinaryToHex(system_salt_,
salt_len,
ascii_salt,
sizeof(ascii_salt))) {
return std::string(ascii_salt, sizeof(ascii_salt) - 1);
}
return std::string();
}
// static
bool ParallelAuthenticator::BinaryToHex(
const std::vector<unsigned char>& binary,
const unsigned int binary_len,
char* hex_string,
const unsigned int len) {
if (len < 2*binary_len)
return false;
memset(hex_string, 0, len);
for (uint i = 0, j = 0; i < binary_len; i++, j+=2)
snprintf(hex_string + j, len - j, "%02x", binary[i]);
return true;
}
} // namespace chromeos