// Copyright 2014 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 "chromeos/login/auth/cryptohome_authenticator.h"

#include <stdint.h>

#include <vector>

#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "chromeos/chromeos_switches.h"
#include "chromeos/cryptohome/async_method_caller.h"
#include "chromeos/cryptohome/cryptohome_parameters.h"
#include "chromeos/cryptohome/cryptohome_util.h"
#include "chromeos/cryptohome/homedir_methods.h"
#include "chromeos/cryptohome/system_salt_getter.h"
#include "chromeos/dbus/cryptohome_client.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/login/auth/auth_status_consumer.h"
#include "chromeos/login/auth/key.h"
#include "chromeos/login/auth/user_context.h"
#include "chromeos/login/login_state.h"
#include "chromeos/login_event_recorder.h"
#include "components/account_id/account_id.h"
#include "components/device_event_log/device_event_log.h"
#include "components/user_manager/known_user.h"
#include "components/user_manager/user_names.h"
#include "components/user_manager/user_type.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace chromeos {

namespace {

// The label used for the key derived from the user's GAIA credentials.
const char kCryptohomeGAIAKeyLabel[] = "gaia";

// The label used for the key generated by Cryptohome for public mount.
const char kCryptohomePublicMountKeyLabel[] = "publicmount";

// The name under which the type of key generated from the user's GAIA
// credentials is stored.
const char kKeyProviderDataTypeName[] = "type";

// The name under which the salt used to generate a key from the user's GAIA
// credentials is stored.
const char kKeyProviderDataSaltName[] = "salt";

// Name of UMA histogram.
const char kCryptohomeMigrationToGaiaId[] = "Cryptohome.MigrationToGaiaId";

// This enum is used to define the buckets for an enumerated UMA histogram.
// Hence,
//   (a) existing enumerated constants should never be deleted or reordered, and
//   (b) new constants should only be appended at the end of the enumeration.
//
// This must be kept in sync with enum CryptohomeMigrationToGaiaId in
// histograms.xml .
enum CryptohomeMigrationToGaiaId {
  NOT_STARTED = 0,
  ALREADY_MIGRATED = 1,
  SUCCESS = 2,
  FAILURE = 3,
  ENTRIES_COUNT
};

// Report to UMA.
void UMACryptohomeMigrationToGaiaId(const CryptohomeMigrationToGaiaId status) {
  UMA_HISTOGRAM_ENUMERATION(kCryptohomeMigrationToGaiaId, status,
                            CryptohomeMigrationToGaiaId::ENTRIES_COUNT);
}

// Hashes |key| with |system_salt| if it its type is KEY_TYPE_PASSWORD_PLAIN.
// Returns the keys unmodified otherwise.
std::unique_ptr<Key> TransformKeyIfNeeded(const Key& key,
                                          const std::string& system_salt) {
  std::unique_ptr<Key> result(new Key(key));
  if (result->GetKeyType() == Key::KEY_TYPE_PASSWORD_PLAIN)
    result->Transform(Key::KEY_TYPE_SALTED_SHA256_TOP_HALF, system_salt);

  return result;
}

// Records get hash status and calls resolver->Resolve().
void TriggerResolveHash(const base::WeakPtr<AuthAttemptState>& attempt,
                        scoped_refptr<CryptohomeAuthenticator> resolver,
                        bool success,
                        const std::string& username_hash) {
  if (success)
    attempt->RecordUsernameHash(username_hash);
  else
    attempt->RecordUsernameHashFailed();
  resolver->Resolve();
}

// Records an error in accessing the user's cryptohome with the given key and
// calls resolver->Resolve() after adding a login time marker.
void RecordKeyErrorAndResolve(const base::WeakPtr<AuthAttemptState>& attempt,
                              scoped_refptr<CryptohomeAuthenticator> resolver) {
  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker("CryptohomeMount-End",
                                                          false);
  attempt->RecordCryptohomeStatus(cryptohome::MOUNT_ERROR_KEY_FAILURE);
  resolver->Resolve();
}

// Callback invoked when cryptohome's GetSantiziedUsername() method has
// finished.
void OnGetSanitizedUsername(
    base::OnceCallback<void(bool, const std::string&)> callback,
    base::Optional<std::string> result) {
  std::move(callback).Run(result.has_value(), result.value_or(std::string()));
}

// Callback invoked when a crypotyhome *Ex method, which only returns a
// base::Reply, finishes.
void OnBaseReplyMethod(const base::WeakPtr<AuthAttemptState>& attempt,
                       scoped_refptr<CryptohomeAuthenticator> resolver,
                       const std::string& time_marker,
                       base::Optional<cryptohome::BaseReply> reply) {
  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(time_marker, false);
  attempt->RecordCryptohomeStatus(BaseReplyToMountError(reply));
  resolver->Resolve();
}

// Callback invoked when cryptohome's MountEx() method has finished.
void OnMount(const base::WeakPtr<AuthAttemptState>& attempt,
             scoped_refptr<CryptohomeAuthenticator> resolver,
             base::Optional<cryptohome::BaseReply> reply) {
  const bool public_mount = attempt->user_context.GetUserType() ==
                                user_manager::USER_TYPE_KIOSK_APP ||
                            attempt->user_context.GetUserType() ==
                                user_manager::USER_TYPE_ARC_KIOSK_APP;

  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(
      public_mount ? "CryptohomeMountPublic-End" : "CryptohomeMount-End",
      false);
  attempt->RecordCryptohomeStatus(MountExReplyToMountError(reply));
  if (attempt->cryptohome_code() == cryptohome::MOUNT_ERROR_NONE) {
    attempt->RecordUsernameHash(MountExReplyToMountHash(reply.value()));
  } else {
    LOGIN_LOG(ERROR) << "MountEx failed. Error: " << attempt->cryptohome_code();
    attempt->RecordUsernameHashFailed();
  }
  resolver->Resolve();
}

// Calls cryptohome's MountEx() method. The key in |attempt->user_context| must
// not be a plain text password. If the user provided a plain text password,
// that password must be transformed to another key type (by salted hashing)
// before calling this method.
void DoMount(const base::WeakPtr<AuthAttemptState>& attempt,
             scoped_refptr<CryptohomeAuthenticator> resolver,
             bool ephemeral,
             bool create_if_nonexistent) {
  const Key* key = attempt->user_context.GetKey();
  // If the |key| is a plain text password, crash rather than attempting to
  // mount the cryptohome with a plain text password.
  CHECK_NE(Key::KEY_TYPE_PASSWORD_PLAIN, key->GetKeyType());

  // Set state that username_hash is requested here so that test implementation
  // that returns directly would not generate 2 OnLoginSucces() calls.
  attempt->UsernameHashRequested();

  cryptohome::MountRequest mount;
  if (ephemeral)
    mount.set_require_ephemeral(true);
  if (create_if_nonexistent) {
    cryptohome::KeyDefinitionToKey(
        cryptohome::KeyDefinition::CreateForPassword(key->GetSecret(),
                                                     kCryptohomeGAIAKeyLabel,
                                                     cryptohome::PRIV_DEFAULT),
        mount.mutable_create()->add_keys());
  }
  if (attempt->user_context.IsForcingDircrypto())
    mount.set_force_dircrypto_if_available(true);
  cryptohome::AuthorizationRequest auth;
  cryptohome::Key* auth_key = auth.mutable_key();
  // Don't set the authorization's key label, implicitly setting it to an
  // empty string, which is a wildcard allowing any key to match. This is
  // necessary because cryptohomes created by Chrome OS M38 and older will have
  // a legacy key with no label while those created by Chrome OS M39 and newer
  // will have a key with the label kCryptohomeGAIAKeyLabel.
  //
  // This logic does not apply to PIN and weak keys in general, as those do not
  // authenticate against a wildcard label.
  if (attempt->user_context.IsUsingPin())
    auth_key->mutable_data()->set_label(key->GetLabel());
  auth_key->set_secret(key->GetSecret());
  DBusThreadManager::Get()->GetCryptohomeClient()->MountEx(
      cryptohome::CreateAccountIdentifierFromAccountId(
          attempt->user_context.GetAccountId()),
      auth, mount, base::BindOnce(&OnMount, attempt, resolver));
}

// Handle cryptohome migration status.
void OnCryptohomeRenamed(const base::WeakPtr<AuthAttemptState>& attempt,
                         scoped_refptr<CryptohomeAuthenticator> resolver,
                         bool ephemeral,
                         bool create_if_nonexistent,
                         base::Optional<cryptohome::BaseReply> reply) {
  cryptohome::MountError return_code = cryptohome::BaseReplyToMountError(reply);
  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(
      "CryptohomeRename-End", false);
  const AccountId& account_id = attempt->user_context.GetAccountId();
  if (return_code == cryptohome::MOUNT_ERROR_NONE) {
    cryptohome::SetGaiaIdMigrationStatusDone(account_id);
    UMACryptohomeMigrationToGaiaId(CryptohomeMigrationToGaiaId::SUCCESS);
  } else {
    LOG(ERROR) << "Failed to rename cryptohome for account of type "
               << AccountId::AccountTypeToString(account_id.GetAccountType())
               << " (return_code=" << return_code << ")";
    // If rename fails, we can still use legacy cryptohome identifier.
    // Proceed to DoMount.
    UMACryptohomeMigrationToGaiaId(CryptohomeMigrationToGaiaId::FAILURE);
  }
  DoMount(attempt, resolver, ephemeral, create_if_nonexistent);
}

// This method migrates cryptohome identifier to gaia id (if needed),
// and then calls Mount.
void EnsureCryptohomeMigratedToGaiaId(
    const base::WeakPtr<AuthAttemptState>& attempt,
    scoped_refptr<CryptohomeAuthenticator> resolver,
    bool ephemeral,
    bool create_if_nonexistent) {
  // Set the migration flag for Active Directory accounts since they're using
  // the account id key as cryptohome id.
  const AccountId& account_id = attempt->user_context.GetAccountId();
  if (account_id.GetAccountType() == AccountType::ACTIVE_DIRECTORY) {
    cryptohome::SetGaiaIdMigrationStatusDone(account_id);
  }

  // Only Google accounts have to be migrated.
  if (account_id.GetAccountType() != AccountType::GOOGLE) {
    DoMount(attempt, resolver, ephemeral, create_if_nonexistent);
    return;
  }

  const bool is_gaiaid_migration_started = switches::IsGaiaIdMigrationStarted();
  if (!is_gaiaid_migration_started) {
    UMACryptohomeMigrationToGaiaId(CryptohomeMigrationToGaiaId::NOT_STARTED);
    DoMount(attempt, resolver, ephemeral, create_if_nonexistent);
    return;
  }
  const bool already_migrated =
      cryptohome::GetGaiaIdMigrationStatus(account_id);
  const bool has_account_key = account_id.HasAccountIdKey();

  bool need_migration = false;
  if (!create_if_nonexistent && !already_migrated) {
    if (has_account_key) {
      need_migration = true;
    } else {
      LOG(WARNING)
          << "Google account has no gaia id. Cryptohome migration skipped.";
    }
  }
  if (need_migration) {
    chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(
        "CryptohomeRename-Start", false);
    cryptohome::AccountIdentifier cryptohome_id_from;
    cryptohome_id_from.set_account_id(account_id.GetUserEmail());  // Migrated
    cryptohome::AccountIdentifier cryptohome_id_to;
    cryptohome_id_to.set_account_id(account_id.GetAccountIdKey());

    DBusThreadManager::Get()->GetCryptohomeClient()->RenameCryptohome(
        cryptohome_id_from, cryptohome_id_to,
        base::BindOnce(&OnCryptohomeRenamed, attempt, resolver, ephemeral,
                       create_if_nonexistent));
    return;
  }
  if (!already_migrated && has_account_key) {
    // Mark new users migrated.
    cryptohome::SetGaiaIdMigrationStatusDone(account_id);
  }
  if (already_migrated) {
    UMACryptohomeMigrationToGaiaId(
        CryptohomeMigrationToGaiaId::ALREADY_MIGRATED);
  }

  DoMount(attempt, resolver, ephemeral, create_if_nonexistent);
}

// Callback invoked when the system salt has been retrieved. Transforms the key
// in |attempt->user_context| using Chrome's default hashing algorithm and the
// system salt, then calls MountEx().
void OnGetSystemSalt(const base::WeakPtr<AuthAttemptState>& attempt,
                     scoped_refptr<CryptohomeAuthenticator> resolver,
                     bool ephemeral,
                     bool create_if_nonexistent,
                     const std::string& system_salt) {
  DCHECK_EQ(Key::KEY_TYPE_PASSWORD_PLAIN,
            attempt->user_context.GetKey()->GetKeyType());

  attempt->user_context.GetKey()->Transform(
      Key::KEY_TYPE_SALTED_SHA256_TOP_HALF, system_salt);

  EnsureCryptohomeMigratedToGaiaId(attempt, resolver, ephemeral,
                                   create_if_nonexistent);
}

// Callback invoked when cryptohome's GetKeyDataEx() method has finished.
// * If GetKeyDataEx() returned metadata indicating the hashing algorithm and
//   salt that were used to generate the key for this user's cryptohome,
//   transforms the key in |attempt->user_context| with the same parameters.
// * Otherwise, starts the retrieval of the system salt so that the key in
//   |attempt->user_context| can be transformed with Chrome's default hashing
//   algorithm and the system salt.
// The resulting key is then passed to cryptohome's MountEx().
void OnGetKeyDataEx(const base::WeakPtr<AuthAttemptState>& attempt,
                    scoped_refptr<CryptohomeAuthenticator> resolver,
                    bool ephemeral,
                    bool create_if_nonexistent,
                    base::Optional<cryptohome::BaseReply> reply) {
  if (cryptohome::GetKeyDataReplyToMountError(reply) ==
      cryptohome::MOUNT_ERROR_NONE) {
    std::vector<cryptohome::KeyDefinition> key_definitions =
        cryptohome::GetKeyDataReplyToKeyDefinitions(reply);
    if (key_definitions.size() == 1) {
      const cryptohome::KeyDefinition& key_definition = key_definitions.front();
      DCHECK_EQ(kCryptohomeGAIAKeyLabel, key_definition.label);

      // Extract the key type and salt from |key_definition|, if present.
      std::unique_ptr<int64_t> type;
      std::unique_ptr<std::string> salt;
      for (std::vector<cryptohome::KeyDefinition::ProviderData>::const_iterator
               it = key_definition.provider_data.begin();
           it != key_definition.provider_data.end(); ++it) {
        if (it->name == kKeyProviderDataTypeName) {
          if (it->number)
            type.reset(new int64_t(*it->number));
          else
            NOTREACHED();
        } else if (it->name == kKeyProviderDataSaltName) {
          if (it->bytes)
            salt.reset(new std::string(*it->bytes));
          else
            NOTREACHED();
        }
      }

      if (type) {
        if (*type < 0 || *type >= Key::KEY_TYPE_COUNT) {
          LOGIN_LOG(ERROR) << "Invalid key type: " << *type;
          RecordKeyErrorAndResolve(attempt, resolver);
          return;
        }

        if (!salt) {
          LOGIN_LOG(ERROR) << "Missing salt.";
          RecordKeyErrorAndResolve(attempt, resolver);
          return;
        }

        attempt->user_context.GetKey()->Transform(
            static_cast<Key::KeyType>(*type), *salt);
        EnsureCryptohomeMigratedToGaiaId(attempt, resolver, ephemeral,
                                         create_if_nonexistent);
        return;
      }
    } else {
      LOGIN_LOG(EVENT) << "GetKeyDataEx() returned " << key_definitions.size()
                       << " entries.";
    }
  }

  SystemSaltGetter::Get()->GetSystemSalt(base::Bind(
      &OnGetSystemSalt, attempt, resolver, ephemeral, create_if_nonexistent));
}

// Starts the process that will mount a user's cryptohome.
// * If the key in |attempt->user_context| is not a plain text password,
//   cryptohome's MountEx() method is called directly with the key.
// * Otherwise, the key must be transformed (by salted hashing) before being
//   passed to MountEx(). In that case, cryptohome's GetKeyDataEx() method is
//   called to retrieve metadata indicating the hashing algorithm and salt that
//   were used to generate the key for this user's cryptohome and the key is
//   transformed accordingly before calling MountEx().
void StartMount(const base::WeakPtr<AuthAttemptState>& attempt,
                scoped_refptr<CryptohomeAuthenticator> resolver,
                bool ephemeral,
                bool create_if_nonexistent) {
  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(
      "CryptohomeMount-Start", false);

  if (attempt->user_context.GetKey()->GetKeyType() !=
      Key::KEY_TYPE_PASSWORD_PLAIN) {
    EnsureCryptohomeMigratedToGaiaId(attempt, resolver, ephemeral,
                                     create_if_nonexistent);
    return;
  }

  cryptohome::GetKeyDataRequest request;
  request.mutable_key()->mutable_data()->set_label(kCryptohomeGAIAKeyLabel);
  DBusThreadManager::Get()->GetCryptohomeClient()->GetKeyDataEx(
      cryptohome::CreateAccountIdentifierFromAccountId(
          attempt->user_context.GetAccountId()),
      cryptohome::AuthorizationRequest(), request,
      base::BindOnce(&OnGetKeyDataEx, attempt, resolver, ephemeral,
                     create_if_nonexistent));
}

// Calls cryptohome's mount method for guest and also get the user hash from
// cryptohome.
void MountGuestAndGetHash(const base::WeakPtr<AuthAttemptState>& attempt,
                          scoped_refptr<CryptohomeAuthenticator> resolver) {
  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(
      "CryptohomeMountGuest-Start", false);
  attempt->UsernameHashRequested();

  DBusThreadManager::Get()->GetCryptohomeClient()->MountGuestEx(
      cryptohome::MountGuestRequest(),
      base::BindOnce(&OnBaseReplyMethod, attempt, resolver,
                     "CryptohomeMountGuest-End"));

  DBusThreadManager::Get()->GetCryptohomeClient()->GetSanitizedUsername(
      cryptohome::CreateAccountIdentifierFromAccountId(
          attempt->user_context.GetAccountId()),
      base::BindOnce(&OnGetSanitizedUsername,
                     base::BindOnce(&TriggerResolveHash, attempt, resolver)));
}

// Calls cryptohome's MountEx method with the public_mount option.
void MountPublic(const base::WeakPtr<AuthAttemptState>& attempt,
                 scoped_refptr<CryptohomeAuthenticator> resolver,
                 bool force_dircrypto_if_available) {
  cryptohome::MountRequest mount;
  if (force_dircrypto_if_available)
    mount.set_force_dircrypto_if_available(true);
  mount.set_public_mount(true);
  // Set the request to create a new homedir when missing.
  cryptohome::KeyDefinitionToKey(
      cryptohome::KeyDefinition::CreateForPassword(
          std::string(), kCryptohomePublicMountKeyLabel,
          cryptohome::PRIV_DEFAULT),
      mount.mutable_create()->add_keys());

  // For public mounts, authorization secret is filled by cryptohomed, hence it
  // is left empty. Authentication's key label is also set to an empty string,
  // which is a wildcard allowing any key to match to allow cryptohomes created
  // in a legacy way. (See comments in DoMount.)
  DBusThreadManager::Get()->GetCryptohomeClient()->MountEx(
      cryptohome::CreateAccountIdentifierFromAccountId(
          attempt->user_context.GetAccountId()),
      cryptohome::AuthorizationRequest(), mount,
      base::BindOnce(&OnMount, attempt, resolver));
}

// Calls cryptohome's key migration method.
void Migrate(const base::WeakPtr<AuthAttemptState>& attempt,
             scoped_refptr<CryptohomeAuthenticator> resolver,
             bool passing_old_hash,
             const std::string& old_password,
             const std::string& system_salt) {
  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(
      "CryptohomeMigrate-Start", false);

  cryptohome::AccountIdentifier account_id =
      cryptohome::CreateAccountIdentifierFromAccountId(
          attempt->user_context.GetAccountId());

  cryptohome::AuthorizationRequest auth_request;
  cryptohome::MigrateKeyRequest migrate_request;
  // TODO(bartfab): Retrieve the hashing algorithm and salt to use for |old_key|
  // from cryptohomed.
  std::unique_ptr<Key> old_key =
      TransformKeyIfNeeded(Key(old_password), system_salt);
  std::unique_ptr<Key> new_key =
      TransformKeyIfNeeded(*attempt->user_context.GetKey(), system_salt);
  if (passing_old_hash) {
    auth_request.mutable_key()->set_secret(old_key->GetSecret());
    migrate_request.set_secret(new_key->GetSecret());
  } else {
    auth_request.mutable_key()->set_secret(new_key->GetSecret());
    migrate_request.set_secret(old_key->GetSecret());
  }

  DBusThreadManager::Get()->GetCryptohomeClient()->MigrateKeyEx(
      account_id, auth_request, migrate_request,
      base::BindOnce(&OnBaseReplyMethod, attempt, resolver,
                     "CryptohomeMigrate-End"));
}

// Calls cryptohome's remove method.
void Remove(const base::WeakPtr<AuthAttemptState>& attempt,
            scoped_refptr<CryptohomeAuthenticator> resolver) {
  chromeos::LoginEventRecorder::Get()->AddLoginTimeMarker(
      "CryptohomeRemove-Start", false);

  cryptohome::AccountIdentifier account_id;
  account_id.set_account_id(
      cryptohome::Identification(attempt->user_context.GetAccountId()).id());

  DBusThreadManager::Get()->GetCryptohomeClient()->RemoveEx(
      account_id, base::BindOnce(&OnBaseReplyMethod, attempt, resolver,
                                 "CryptohomeRemove-End"));
}

void OnKeyChecked(const base::WeakPtr<AuthAttemptState>& attempt,
                  scoped_refptr<CryptohomeAuthenticator> resolver,
                  base::Optional<cryptohome::BaseReply> reply) {
  attempt->RecordCryptohomeStatus(BaseReplyToMountError(reply));
  resolver->Resolve();
}

// Calls cryptohome's key check method.
void CheckKey(const base::WeakPtr<AuthAttemptState>& attempt,
              scoped_refptr<CryptohomeAuthenticator> resolver,
              const std::string& system_salt) {
  std::unique_ptr<Key> key =
      TransformKeyIfNeeded(*attempt->user_context.GetKey(), system_salt);
  cryptohome::AuthorizationRequest auth;
  auth.mutable_key()->set_secret(key->GetSecret());
  DBusThreadManager::Get()->GetCryptohomeClient()->CheckKeyEx(
      cryptohome::CreateAccountIdentifierFromAccountId(
          attempt->user_context.GetAccountId()),
      auth, cryptohome::CheckKeyRequest(),
      base::BindOnce(&OnKeyChecked, attempt, resolver));
}

}  // namespace

CryptohomeAuthenticator::CryptohomeAuthenticator(
    scoped_refptr<base::TaskRunner> task_runner,
    AuthStatusConsumer* consumer)
    : Authenticator(consumer),
      task_runner_(task_runner),
      migrate_attempted_(false),
      remove_attempted_(false),
      resync_attempted_(false),
      ephemeral_mount_attempted_(false),
      check_key_attempted_(false),
      already_reported_success_(false),
      owner_is_verified_(false),
      user_can_login_(false),
      remove_user_data_on_failure_(false),
      delayed_login_failure_(NULL) {}

void CryptohomeAuthenticator::AuthenticateToLogin(
    content::BrowserContext* context,
    const UserContext& user_context) {
  DCHECK(user_context.GetUserType() == user_manager::USER_TYPE_REGULAR ||
         user_context.GetUserType() == user_manager::USER_TYPE_CHILD ||
         user_context.GetUserType() ==
             user_manager::USER_TYPE_ACTIVE_DIRECTORY);
  authentication_context_ = context;
  current_state_.reset(new AuthAttemptState(user_context,
                                            false,  // unlock
                                            false,  // online_complete
                                            !IsKnownUser(user_context)));
  // Reset the verified flag.
  owner_is_verified_ = false;

  StartMount(current_state_->AsWeakPtr(),
             scoped_refptr<CryptohomeAuthenticator>(this),
             false /* ephemeral */, false /* create_if_nonexistent */);
}

void CryptohomeAuthenticator::CompleteLogin(content::BrowserContext* context,
                                            const UserContext& user_context) {
  DCHECK(user_context.GetUserType() == user_manager::USER_TYPE_REGULAR ||
         user_context.GetUserType() == user_manager::USER_TYPE_CHILD ||
         user_context.GetUserType() ==
             user_manager::USER_TYPE_ACTIVE_DIRECTORY);
  authentication_context_ = context;
  current_state_.reset(new AuthAttemptState(user_context,
                                            true,   // unlock
                                            false,  // online_complete
                                            !IsKnownUser(user_context)));

  // Reset the verified flag.
  owner_is_verified_ = false;
  if (!user_manager::known_user::FindPrefs(user_context.GetAccountId(),
                                           nullptr)) {
    // Save logged in user into local state as early as possible.
    user_manager::known_user::SaveKnownUser(user_context.GetAccountId());
  }

  StartMount(current_state_->AsWeakPtr(),
             scoped_refptr<CryptohomeAuthenticator>(this),
             false /* ephemeral */, false /* create_if_nonexistent */);

  // For login completion from extension, we just need to resolve the current
  // auth attempt state, the rest of OAuth related tasks will be done in
  // parallel.
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&CryptohomeAuthenticator::ResolveLoginCompletionStatus,
                     this));
}

void CryptohomeAuthenticator::AuthenticateToUnlock(
    const UserContext& user_context) {
  DCHECK_EQ(user_manager::USER_TYPE_REGULAR, user_context.GetUserType());
  current_state_.reset(new AuthAttemptState(user_context,
                                            true,     // unlock
                                            true,     // online_complete
                                            false));  // user_is_new
  remove_user_data_on_failure_ = false;
  check_key_attempted_ = true;
  SystemSaltGetter::Get()->GetSystemSalt(
      base::Bind(&CheckKey, current_state_->AsWeakPtr(),
                 scoped_refptr<CryptohomeAuthenticator>(this)));
}

void CryptohomeAuthenticator::LoginAsSupervisedUser(
    const UserContext& user_context) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  DCHECK_EQ(user_manager::USER_TYPE_SUPERVISED, user_context.GetUserType());

  // TODO(nkostylev): Pass proper value for |user_is_new| or remove (not used).
  current_state_.reset(new AuthAttemptState(user_context,
                                            false,    // unlock
                                            false,    // online_complete
                                            false));  // user_is_new
  remove_user_data_on_failure_ = false;
  StartMount(current_state_->AsWeakPtr(),
             scoped_refptr<CryptohomeAuthenticator>(this),
             false /* ephemeral */, false /* create_if_nonexistent */);
}

void CryptohomeAuthenticator::LoginOffTheRecord() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  current_state_.reset(
      new AuthAttemptState(UserContext(user_manager::USER_TYPE_GUEST,
                                       user_manager::GuestAccountId()),
                           false,    // unlock
                           false,    // online_complete
                           false));  // user_is_new
  remove_user_data_on_failure_ = false;
  ephemeral_mount_attempted_ = true;
  MountGuestAndGetHash(current_state_->AsWeakPtr(),
                       scoped_refptr<CryptohomeAuthenticator>(this));
}

void CryptohomeAuthenticator::LoginAsPublicSession(
    const UserContext& user_context) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  DCHECK_EQ(user_manager::USER_TYPE_PUBLIC_ACCOUNT, user_context.GetUserType());

  current_state_.reset(new AuthAttemptState(user_context,
                                            false,    // unlock
                                            false,    // online_complete
                                            false));  // user_is_new
  remove_user_data_on_failure_ = false;
  ephemeral_mount_attempted_ = true;
  StartMount(current_state_->AsWeakPtr(),
             scoped_refptr<CryptohomeAuthenticator>(this), true /* ephemeral */,
             true /* create_if_nonexistent */);
}

void CryptohomeAuthenticator::LoginAsKioskAccount(
    const AccountId& app_account_id,
    bool use_guest_mount) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());

  const AccountId& account_id =
      use_guest_mount ? user_manager::GuestAccountId() : app_account_id;
  current_state_.reset(new AuthAttemptState(
      UserContext(user_manager::USER_TYPE_KIOSK_APP, account_id),
      false,    // unlock
      false,    // online_complete
      false));  // user_is_new

  remove_user_data_on_failure_ = true;
  if (!use_guest_mount) {
    MountPublic(current_state_->AsWeakPtr(),
                scoped_refptr<CryptohomeAuthenticator>(this),
                false);  // force_dircrypto_if_available
  } else {
    ephemeral_mount_attempted_ = true;
    MountGuestAndGetHash(current_state_->AsWeakPtr(),
                         scoped_refptr<CryptohomeAuthenticator>(this));
  }
}

void CryptohomeAuthenticator::LoginAsArcKioskAccount(
    const AccountId& app_account_id) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());

  current_state_.reset(new AuthAttemptState(
      UserContext(user_manager::USER_TYPE_ARC_KIOSK_APP, app_account_id),
      false,    // unlock
      false,    // online_complete
      false));  // user_is_new

  remove_user_data_on_failure_ = true;
  MountPublic(current_state_->AsWeakPtr(),
              scoped_refptr<CryptohomeAuthenticator>(this),
              true);  // force_dircrypto_if_available
}

void CryptohomeAuthenticator::OnAuthSuccess() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  VLOG(1) << "Login success";
  // Send notification of success
  chromeos::LoginEventRecorder::Get()->RecordAuthenticationSuccess();
  {
    base::AutoLock for_this_block(success_lock_);
    already_reported_success_ = true;
  }
  if (consumer_)
    consumer_->OnAuthSuccess(current_state_->user_context);
}

void CryptohomeAuthenticator::OnOffTheRecordAuthSuccess() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  chromeos::LoginEventRecorder::Get()->RecordAuthenticationSuccess();
  if (consumer_)
    consumer_->OnOffTheRecordAuthSuccess();
}

void CryptohomeAuthenticator::OnPasswordChangeDetected() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  if (consumer_)
    consumer_->OnPasswordChangeDetected();
}

void CryptohomeAuthenticator::OnOldEncryptionDetected(
    bool has_incomplete_migration) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  if (consumer_) {
    consumer_->OnOldEncryptionDetected(current_state_->user_context,
                                       has_incomplete_migration);
  }
}

void CryptohomeAuthenticator::OnAuthFailure(const AuthFailure& error) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());

  // OnAuthFailure will be called again with the same |error|
  // after the cryptohome has been removed.
  if (remove_user_data_on_failure_) {
    delayed_login_failure_ = &error;
    RemoveEncryptedData();
    return;
  }
  chromeos::LoginEventRecorder::Get()->RecordAuthenticationFailure();
  LOGIN_LOG(ERROR) << "Login failed: " << error.GetErrorString();
  if (consumer_)
    consumer_->OnAuthFailure(error);
}

void CryptohomeAuthenticator::RecoverEncryptedData(
    const std::string& old_password) {
  migrate_attempted_ = true;
  current_state_->ResetCryptohomeStatus();
  SystemSaltGetter::Get()->GetSystemSalt(base::Bind(
      &Migrate, current_state_->AsWeakPtr(),
      scoped_refptr<CryptohomeAuthenticator>(this), true, old_password));
}

void CryptohomeAuthenticator::RemoveEncryptedData() {
  remove_attempted_ = true;
  current_state_->ResetCryptohomeStatus();
  task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&Remove, current_state_->AsWeakPtr(),
                                scoped_refptr<CryptohomeAuthenticator>(this)));
}

void CryptohomeAuthenticator::ResyncEncryptedData() {
  resync_attempted_ = true;
  current_state_->ResetCryptohomeStatus();
  task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&Remove, current_state_->AsWeakPtr(),
                                scoped_refptr<CryptohomeAuthenticator>(this)));
}

bool CryptohomeAuthenticator::VerifyOwner() {
  if (owner_is_verified_)
    return true;
  // Check if policy data is fine and continue in safe mode if needed.
  if (!IsSafeMode()) {
    // Now we can continue with the login and report mount success.
    user_can_login_ = true;
    owner_is_verified_ = true;
    return true;
  }

  CheckSafeModeOwnership(
      current_state_->user_context,
      base::Bind(&CryptohomeAuthenticator::OnOwnershipChecked, this));
  return false;
}

void CryptohomeAuthenticator::OnOwnershipChecked(bool is_owner) {
  // Now we can check if this user is the owner.
  user_can_login_ = is_owner;
  owner_is_verified_ = true;
  Resolve();
}

void CryptohomeAuthenticator::OnUnmount(base::Optional<bool> success) {
  if (!success.has_value() || !success.value()) {
    // Maybe we should reboot immediately here?
    LOGIN_LOG(ERROR) << "Couldn't unmount users home!";
  }
  OnAuthFailure(AuthFailure(AuthFailure::OWNER_REQUIRED));
}

void CryptohomeAuthenticator::Resolve() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  bool create_if_nonexistent = false;
  CryptohomeAuthenticator::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.
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure, this,
                         AuthFailure(AuthFailure::COULD_NOT_MOUNT_CRYPTOHOME)));
      break;
    case FAILED_REMOVE:
      // In this case, we tried to remove the user's old cryptohome at their
      // request, and the remove failed.
      remove_user_data_on_failure_ = false;
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure, this,
                         AuthFailure(AuthFailure::DATA_REMOVAL_FAILED)));
      break;
    case FAILED_TMPFS:
      // In this case, we tried to mount a tmpfs for guest and failed.
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure, this,
                         AuthFailure(AuthFailure::COULD_NOT_MOUNT_TMPFS)));
      break;
    case FAILED_TPM:
      // In this case, we tried to create/mount cryptohome and failed
      // because of the critical TPM error.
      // Chrome will notify user and request reboot.
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure,
                                    this, AuthFailure(AuthFailure::TPM_ERROR)));
      break;
    case FAILED_USERNAME_HASH:
      // In this case, we failed the GetSanitizedUsername request to
      // cryptohomed. This can happen for any login attempt.
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure, this,
                         AuthFailure(AuthFailure::USERNAME_HASH_FAILED)));
      break;
    case REMOVED_DATA_AFTER_FAILURE:
      remove_user_data_on_failure_ = false;
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure,
                                    this, *delayed_login_failure_));
      break;
    case CREATE_NEW:
      create_if_nonexistent = true;
      FALLTHROUGH;
    case RECOVER_MOUNT:
      current_state_->ResetCryptohomeStatus();
      StartMount(current_state_->AsWeakPtr(),
                 scoped_refptr<CryptohomeAuthenticator>(this),
                 false /*ephemeral*/, create_if_nonexistent);
      break;
    case NEED_OLD_PW:
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnPasswordChangeDetected,
                         this));
      break;
    case ONLINE_FAILED:
    case NEED_NEW_PW:
    case HAVE_NEW_PW:
      NOTREACHED() << "Using obsolete ClientLogin code path.";
      break;
    case OFFLINE_LOGIN:
      VLOG(2) << "Offline login";
      FALLTHROUGH;
    case UNLOCK:
      VLOG(2) << "Unlock";
      FALLTHROUGH;
    case ONLINE_LOGIN:
      VLOG(2) << "Online login";
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthSuccess, this));
      break;
    case GUEST_LOGIN:
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnOffTheRecordAuthSuccess,
                         this));
      break;
    case KIOSK_ACCOUNT_LOGIN:
    case PUBLIC_ACCOUNT_LOGIN:
      current_state_->user_context.SetIsUsingOAuth(false);
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthSuccess, this));
      break;
    case SUPERVISED_USER_LOGIN:
      current_state_->user_context.SetIsUsingOAuth(false);
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthSuccess, this));
      break;
    case LOGIN_FAILED:
      current_state_->ResetCryptohomeStatus();
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure,
                                    this, current_state_->online_outcome()));
      break;
    case OWNER_REQUIRED: {
      current_state_->ResetCryptohomeStatus();
      DBusThreadManager::Get()->GetCryptohomeClient()->Unmount(
          base::BindOnce(&CryptohomeAuthenticator::OnUnmount, this));
      break;
    }
    case FAILED_OLD_ENCRYPTION:
    case FAILED_PREVIOUS_MIGRATION_INCOMPLETE:
      // In this case, we tried to create/mount cryptohome and failed
      // because the file system is encrypted in old format.
      // Chrome will show a screen which asks user to migrate the encryption.
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnOldEncryptionDetected,
                         this, state == FAILED_PREVIOUS_MIGRATION_INCOMPLETE));
      break;
    case OFFLINE_NO_MOUNT:
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&CryptohomeAuthenticator::OnAuthFailure, this,
                         AuthFailure(AuthFailure::MISSING_CRYPTOHOME)));
      break;
    default:
      NOTREACHED();
      break;
  }
}

CryptohomeAuthenticator::~CryptohomeAuthenticator() = default;

CryptohomeAuthenticator::AuthState CryptohomeAuthenticator::ResolveState() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  // If we haven't mounted the user's home dir yet or
  // haven't got sanitized username value, we can't be done.
  // We never get past here if any of these two cryptohome ops is still pending.
  // This is an important invariant.
  if (!current_state_->cryptohome_complete() ||
      !current_state_->username_hash_obtained()) {
    return CONTINUE;
  }

  AuthState state = CONTINUE;

  if (current_state_->cryptohome_code() == cryptohome::MOUNT_ERROR_NONE &&
      current_state_->username_hash_valid()) {
    state = ResolveCryptohomeSuccessState();
  } else {
    state = ResolveCryptohomeFailureState();
    LOGIN_LOG(ERROR) << "Cryptohome failure: "
                     << "state(AuthState)=" << state
                     << ", code(cryptohome::MountError)="
                     << current_state_->cryptohome_code();
  }

  DCHECK(current_state_->cryptohome_complete());  // Ensure invariant holds.
  migrate_attempted_ = false;
  remove_attempted_ = false;
  resync_attempted_ = false;
  ephemeral_mount_attempted_ = false;
  check_key_attempted_ = false;

  if (state != POSSIBLE_PW_CHANGE && state != NO_MOUNT &&
      state != OFFLINE_LOGIN)
    return state;

  if (current_state_->online_complete()) {
    if (current_state_->online_outcome().reason() == AuthFailure::NONE) {
      // Online attempt succeeded as well, so combine the results.
      return ResolveOnlineSuccessState(state);
    }
    NOTREACHED() << "Using obsolete ClientLogin code path.";
  }
  // if online isn't complete yet, just return the offline result.
  return state;
}

CryptohomeAuthenticator::AuthState
CryptohomeAuthenticator::ResolveCryptohomeFailureState() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  if (remove_attempted_ || resync_attempted_)
    return FAILED_REMOVE;
  if (ephemeral_mount_attempted_)
    return FAILED_TMPFS;
  if (migrate_attempted_)
    return NEED_OLD_PW;
  if (check_key_attempted_)
    return LOGIN_FAILED;

  if (current_state_->cryptohome_code() ==
      cryptohome::MOUNT_ERROR_TPM_NEEDS_REBOOT) {
    // Critical TPM error detected, reboot needed.
    return FAILED_TPM;
  }

  if (current_state_->cryptohome_code() ==
      cryptohome::MOUNT_ERROR_OLD_ENCRYPTION) {
    return FAILED_OLD_ENCRYPTION;
  }
  if (current_state_->cryptohome_code() ==
      cryptohome::MOUNT_ERROR_PREVIOUS_MIGRATION_INCOMPLETE) {
    return FAILED_PREVIOUS_MIGRATION_INCOMPLETE;
  }

  // Return intermediate states in the following case:
  // when there is an online result to use;
  // This is the case after user finishes Gaia login;
  if (current_state_->online_complete()) {
    if (current_state_->cryptohome_code() ==
        cryptohome::MOUNT_ERROR_KEY_FAILURE) {
      // If we tried a mount but they used the wrong key, we may need to
      // ask the user for their old password.  We'll only know once we've
      // done the online check.
      return POSSIBLE_PW_CHANGE;
    }
    if (current_state_->cryptohome_code() ==
        cryptohome::MOUNT_ERROR_USER_DOES_NOT_EXIST) {
      // 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;
    }
  } else if (current_state_->cryptohome_code() ==
             cryptohome::MOUNT_ERROR_USER_DOES_NOT_EXIST) {
    // If we tried a mount but the user did not exist in the offline flow,
    // surface this as an error.
    return OFFLINE_NO_MOUNT;
  }

  if (!current_state_->username_hash_valid())
    return FAILED_USERNAME_HASH;

  return FAILED_MOUNT;
}

CryptohomeAuthenticator::AuthState
CryptohomeAuthenticator::ResolveCryptohomeSuccessState() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  if (resync_attempted_)
    return CREATE_NEW;
  if (remove_attempted_)
    return REMOVED_DATA_AFTER_FAILURE;
  if (migrate_attempted_)
    return RECOVER_MOUNT;
  if (check_key_attempted_)
    return UNLOCK;

  const user_manager::UserType user_type =
      current_state_->user_context.GetUserType();
  if (user_type == user_manager::USER_TYPE_GUEST)
    return GUEST_LOGIN;
  if (user_type == user_manager::USER_TYPE_PUBLIC_ACCOUNT)
    return PUBLIC_ACCOUNT_LOGIN;
  if (user_type == user_manager::USER_TYPE_KIOSK_APP)
    return KIOSK_ACCOUNT_LOGIN;
  if (user_type == user_manager::USER_TYPE_SUPERVISED)
    return SUPERVISED_USER_LOGIN;

  if (!VerifyOwner())
    return CONTINUE;
  return user_can_login_ ? OFFLINE_LOGIN : OWNER_REQUIRED;
}

CryptohomeAuthenticator::AuthState
CryptohomeAuthenticator::ResolveOnlineSuccessState(
    CryptohomeAuthenticator::AuthState offline_state) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  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 CryptohomeAuthenticator::ResolveLoginCompletionStatus() {
  // Shortcut online state resolution process.
  current_state_->RecordOnlineLoginStatus(AuthFailure::AuthFailureNone());
  Resolve();
}

void CryptohomeAuthenticator::SetOwnerState(bool owner_check_finished,
                                            bool check_result) {
  owner_is_verified_ = owner_check_finished;
  user_can_login_ = check_result;
}

}  // namespace chromeos
