// 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 #include #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(&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(&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(&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; } namespace { bool WasConnectionIssue(const LoginFailure& online_outcome) { return ((online_outcome.reason() == LoginFailure::LOGIN_TIMED_OUT) || (online_outcome.error().state() == GoogleServiceAuthError::CONNECTION_FAILED) || (online_outcome.error().state() == GoogleServiceAuthError::REQUEST_CANCELED)); } } // anonymous namespace ParallelAuthenticator::AuthState ParallelAuthenticator::ResolveOnlineFailureState( ParallelAuthenticator::AuthState offline_state) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); if (offline_state == OFFLINE_LOGIN) { if (WasConnectionIssue(current_state_->online_outcome())) { // 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(ascii_salt.data()), static_cast(ascii_salt.length())); SHA256_Update(&ctx, reinterpret_cast(password.data()), static_cast(password.length())); SHA256_End(&ctx, passhash_buf, NULL, static_cast(sizeof(passhash_buf))); std::vector 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& 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