chromium/chrome/browser/password_manager/android/built_in_backend_to_android_backend_migrator.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/password_manager/android/built_in_backend_to_android_backend_migrator.h"

#include <optional>
#include <string>

#include "base/barrier_callback.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/password_manager/android/password_store_android_backend.h"
#include "chrome/browser/password_manager/android/password_store_android_backend_api_error_codes.h"
#include "components/browser_sync/sync_to_signin_migration.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_store/password_store_backend.h"
#include "components/password_manager/core/browser/password_store/password_store_backend_error.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/signin_pref_names.h"
#include "components/sync/base/pref_names.h"

namespace password_manager {

namespace {

// Threshold for the next migration attempt. This is needed in order to prevent
// clients from spamming GMS Core API.
constexpr base::TimeDelta kMigrationThreshold = base::Days(1);

// The required migration version. If the version saved in
// `prefs::kCurrentMigrationVersionToGoogleMobileServices` is lower than
// 'kRequiredMigrationVersion', passwords will be re-uploaded. Currently set to
// the initial migration version.
constexpr int kRequiredMigrationVersion = 1;

constexpr char kMetricInfix[] =
    "PasswordManager.UnifiedPasswordManager.MigrationForLocalUsers.";

// Returns true if the initial migration to the android backend has happened.
bool HasMigratedToTheAndroidBackend(PrefService* prefs) {
  return prefs->GetInteger(
             prefs::kCurrentMigrationVersionToGoogleMobileServices) >=
         kRequiredMigrationVersion;
}

bool IsBlocklistedFormWithValues(const PasswordForm& form) {
  return form.blocked_by_user &&
         (!form.username_value.empty() || !form.password_value.empty());
}

std::string BackendOperationToString(
    BuiltInBackendToAndroidBackendMigrator::BackendOperationForMigration
        backend_operation) {
  switch (backend_operation) {
    case BuiltInBackendToAndroidBackendMigrator::BackendOperationForMigration::
        kAddLogin:
      return "AddLogin";
    case BuiltInBackendToAndroidBackendMigrator::BackendOperationForMigration::
        kUpdateLogin:
      return "UpdateLogin";
    case BuiltInBackendToAndroidBackendMigrator::BackendOperationForMigration::
        kRemoveLogin:
      return "RemoveLogin";
    case BuiltInBackendToAndroidBackendMigrator::BackendOperationForMigration::
        kGetAllLogins:
      return "GetAllLogins";
  }
}

void ResetUnenrollmentStatus(PrefService* prefs) {
  prefs->ClearPref(
      password_manager::prefs::kUnenrolledFromGoogleMobileServicesDueToErrors);
}

bool IsPasswordSyncEnabled(PrefService* pref_service) {
  switch (browser_sync::GetSyncToSigninMigrationDataTypeDecision(
      pref_service, syncer::PASSWORDS,
      syncer::prefs::internal::kSyncPasswords)) {
    // In particular, in the
    // prefs::UseUpmLocalAndSeparateStoresState::kOffAndMigrationPending
    // state, kDontMigrateTypeNotActive is reported and we wish to return true.
    case browser_sync::SyncToSigninMigrationDataTypeDecision::
        kDontMigrateTypeNotActive:
    case browser_sync::SyncToSigninMigrationDataTypeDecision::kMigrate:
      return true;
    case browser_sync::SyncToSigninMigrationDataTypeDecision::
        kDontMigrateTypeDisabled:
      return false;
  }
}

void SchedulePostMigrationBottomSheet(PrefService* prefs) {
  // There is no need to show the sheet if no passwords were migrated.
  if (prefs->GetBoolean(prefs::kEmptyProfileStoreLoginDatabase)) {
    return;
  }

  // As part of M4 syncing users who were unenrolled migrate their passwords to
  // local GMSCore storage. They shouldn't see the sheet either.
  if (IsPasswordSyncEnabled(prefs) &&
      (prefs->GetBoolean(
           prefs::kUnenrolledFromGoogleMobileServicesDueToErrors) ||
       prefs->GetInteger(
           prefs::kCurrentMigrationVersionToGoogleMobileServices) == 0)) {
    return;
  }

  prefs->SetBoolean(prefs::kShouldShowPostPasswordMigrationSheetAtStartup,
                    true);
}

}  // namespace

struct BuiltInBackendToAndroidBackendMigrator::IsPasswordLess {
  bool operator()(const PasswordForm* lhs, const PasswordForm* rhs) const {
    return PasswordFormUniqueKey(*lhs) < PasswordFormUniqueKey(*rhs);
  }
};

struct BuiltInBackendToAndroidBackendMigrator::BackendAndLoginsResults {
  raw_ptr<PasswordStoreBackend> backend;
  LoginsResultOrError logins_result;

  bool HasError() const {
    return absl::holds_alternative<PasswordStoreBackendError>(logins_result);
  }

  std::optional<int> GetApiError() const {
    if (HasError()) {
      return absl::get<PasswordStoreBackendError>(logins_result)
          .android_backend_api_error;
    }
    return std::nullopt;
  }

  // Converts std::vector<std::unique_ptr<PasswordForms>> into
  // base::flat_set<const PasswordForm*> for quick look up comparing only
  // primary keys.
  base::flat_set<const PasswordForm*, IsPasswordLess> GetLogins() {
    DCHECK(!HasError());

    return base::MakeFlatSet<const PasswordForm*, IsPasswordLess>(
        absl::get<LoginsResult>(logins_result), {},
        [](auto& form) { return &form; });
  }

  BackendAndLoginsResults(PasswordStoreBackend* backend,
                          LoginsResultOrError logins)
      : backend(backend), logins_result(std::move(logins)) {}
  BackendAndLoginsResults(BackendAndLoginsResults&&) = default;
  BackendAndLoginsResults& operator=(BackendAndLoginsResults&&) = default;
  BackendAndLoginsResults(const BackendAndLoginsResults&) = delete;
  BackendAndLoginsResults& operator=(const BackendAndLoginsResults&) = delete;
  ~BackendAndLoginsResults() = default;
};

class BuiltInBackendToAndroidBackendMigrator::MigrationMetricsReporter {
 public:
  MigrationMetricsReporter() = default;
  ~MigrationMetricsReporter() = default;

  void ReportMetrics(bool migration_succeeded) {
    base::TimeDelta duration = base::Time::Now() - start_;
    base::UmaHistogramMediumTimes(base::StrCat({kMetricInfix, "Latency"}),
                                  duration);
    base::UmaHistogramBoolean(base::StrCat({kMetricInfix, "Success"}),
                              migration_succeeded);
    base::UmaHistogramCounts1000(
        base::StrCat({kMetricInfix, "UpdateLoginCount"}), update_logins_count_);
    ReportAdditionalMetricsForLocalPasswordsMigration(migration_succeeded);
    metrics_util::LogLocalPwdMigrationProgressState(
        metrics_util::LocalPwdMigrationProgressState::kFinished);
  }

  void ReportAdditionalMetricsForLocalPasswordsMigration(
      bool migration_succeeded) {
    base::UmaHistogramCounts1000(base::StrCat({kMetricInfix, "AddLoginCount"}),
                                 added_logins_count_);
    base::UmaHistogramCounts1000(
        base::StrCat({kMetricInfix, "MigratedLoginsTotalCount"}),
        added_logins_count_ + update_logins_count_);
    if (migration_succeeded &&
        migration_conflict_won_by_android_count_.has_value()) {
      base::UmaHistogramCounts1000(
          base::StrCat({kMetricInfix, "MergeWhereAndroidHasMostRecent"}),
          migration_conflict_won_by_android_count_.value());
    }
  }

  void HandleBackendOperationResult(
      std::string backend_infix,
      BackendOperationForMigration backend_operation,
      bool is_success,
      std::optional<int> api_error) {
    base::UmaHistogramBoolean(
        base::StrCat({kMetricInfix, backend_infix, ".",
                      BackendOperationToString(backend_operation), ".Success"}),
        is_success);
    if (!is_success) {
      if (api_error.has_value()) {
        base::UmaHistogramSparse(
            base::StrCat({kMetricInfix, backend_infix, ".",
                          BackendOperationToString(backend_operation),
                          ".APIError"}),
            api_error.value());
      }
      return;
    }

    switch (backend_operation) {
      case BackendOperationForMigration::kAddLogin:
        added_logins_count_++;
        break;
      case BackendOperationForMigration::kUpdateLogin:
        update_logins_count_++;
        break;
      case BackendOperationForMigration::kRemoveLogin:
      case BackendOperationForMigration::kGetAllLogins:
        break;
    }
  }

  void SetLocalConflictsWonByAndroidCount(int count) {
    migration_conflict_won_by_android_count_ = count;
  }

 private:
  base::Time start_ = base::Time::Now();
  std::optional<int> migration_conflict_won_by_android_count_;
  int added_logins_count_ = 0;
  int update_logins_count_ = 0;
};

BuiltInBackendToAndroidBackendMigrator::BuiltInBackendToAndroidBackendMigrator(
    PasswordStoreBackend* built_in_backend,
    PasswordStoreBackend* android_backend,
    PrefService* prefs)
    : built_in_backend_(built_in_backend),
      android_backend_(android_backend),
      prefs_(prefs) {
  DCHECK(built_in_backend_);
  DCHECK(android_backend_);
  base::UmaHistogramBoolean(
      "PasswordManager.UnifiedPasswordManager.WasMigrationDone",
      HasMigratedToTheAndroidBackend(prefs_));
}

BuiltInBackendToAndroidBackendMigrator::
    ~BuiltInBackendToAndroidBackendMigrator() = default;

void BuiltInBackendToAndroidBackendMigrator::StartMigrationOfLocalPasswords() {
  CHECK(!migration_in_progress_);

  // Don't try to migrate passwords if there was an attempt earlier today.
  base::TimeDelta time_passed_since_last_migration_attempt =
      base::Time::Now() -
      base::Time::FromTimeT(prefs_->GetDouble(
          password_manager::prefs::kTimeOfLastMigrationAttempt));
  if (time_passed_since_last_migration_attempt < kMigrationThreshold) {
    return;
  }

  migration_in_progress_ = true;

  metrics_reporter_ = std::make_unique<MigrationMetricsReporter>();
  TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("passwords",
                                    "UnifiedPasswordManagerMigration", this);

  prefs_->SetDouble(password_manager::prefs::kTimeOfLastMigrationAttempt,
                    base::Time::Now().InSecondsFSinceUnixEpoch());

  LogLocalPwdMigrationProgressState(
      metrics_util::LocalPwdMigrationProgressState::kStarted);
  auto barrier_callback = base::BarrierCallback<BackendAndLoginsResults>(
      2,
      base::BindOnce(&BuiltInBackendToAndroidBackendMigrator::
                         MigrateLocalPasswordsBetweenAndroidAndBuiltInBackends,
                     weak_ptr_factory_.GetWeakPtr()));

  auto bind_backend_to_logins = [](PasswordStoreBackend* backend,
                                   LoginsResultOrError result) {
    return BackendAndLoginsResults(backend, std::move(result));
  };

  auto builtin_backend_callback_chain =
      base::BindOnce(bind_backend_to_logins,
                     base::Unretained(built_in_backend_))
          .Then(barrier_callback);

  // Cleanup blacklisted forms in the built in backend before binding.
  builtin_backend_callback_chain = base::BindOnce(
      &BuiltInBackendToAndroidBackendMigrator::RemoveBlocklistedFormsWithValues,
      weak_ptr_factory_.GetWeakPtr(), base::Unretained(built_in_backend_),
      std::move(builtin_backend_callback_chain));

  built_in_backend_->GetAllLoginsAsync(
      std::move(builtin_backend_callback_chain));

  auto android_backend_callback_chain =
      base::BindOnce(bind_backend_to_logins, base::Unretained(android_backend_))
          .Then(barrier_callback);

  // Cleanup blacklisted forms in the android backend before binding.
  android_backend_callback_chain = base::BindOnce(
      &BuiltInBackendToAndroidBackendMigrator::RemoveBlocklistedFormsWithValues,
      weak_ptr_factory_.GetWeakPtr(), base::Unretained(android_backend_),
      std::move(android_backend_callback_chain));

  android_backend_->GetAllLoginsAsync(
      std::move(android_backend_callback_chain));
}

void BuiltInBackendToAndroidBackendMigrator::OnSyncServiceInitialized(
    syncer::SyncService* sync_service) {
  sync_service_ = sync_service;
}

base::WeakPtr<BuiltInBackendToAndroidBackendMigrator>
BuiltInBackendToAndroidBackendMigrator::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

void BuiltInBackendToAndroidBackendMigrator::
    MigrateLocalPasswordsBetweenAndroidAndBuiltInBackends(
        std::vector<BackendAndLoginsResults> results) {
  DCHECK(metrics_reporter_);
  DCHECK_EQ(2u, results.size());

  if (results[0].HasError() || results[1].HasError()) {
    for (const auto& result : results) {
      metrics_reporter_->HandleBackendOperationResult(
          GetMetricInfixFromBackend(result.backend),
          BackendOperationForMigration::kGetAllLogins, !result.HasError(),
          result.GetApiError());
    }

    MigrationFinished(/*is_success=*/false);
    return;
  }

  base::flat_set<const PasswordForm*, IsPasswordLess> built_in_backend_logins =
      (results[0].backend == built_in_backend_) ? results[0].GetLogins()
                                                : results[1].GetLogins();

  base::flat_set<const PasswordForm*, IsPasswordLess> android_logins =
      (results[0].backend == android_backend_) ? results[0].GetLogins()
                                               : results[1].GetLogins();

  MergeBuiltInBackendIntoAndroidBackend(std::move(built_in_backend_logins),
                                        std::move(android_logins));
}

void BuiltInBackendToAndroidBackendMigrator::
    MergeBuiltInBackendIntoAndroidBackend(
        PasswordFormPtrFlatSet built_in_backend_logins,
        PasswordFormPtrFlatSet android_logins) {
  // For a form |F|, there are 2 cases to handle:
  // 1. If |F| exists only in the |built_in_backend_|, then |F| should be added
  //    to the |android_backend_|.
  // 2. If |F| already exists in both |android_backend_|, then
  //    the most recent version of |F| will be kept in |android_backend_|.
  // No changes are made to the |built_in_backend_|.

  // Callbacks are chained like in a stack way by passing 'callback_chain' as a
  // completion for the next operation. At the end, update pref to mark
  // successful completion.
  base::OnceClosure callbacks_chain =
      base::BindOnce(&BuiltInBackendToAndroidBackendMigrator::MigrationFinished,
                     weak_ptr_factory_.GetWeakPtr(), /*is_success=*/true);
  int migration_conflict_won_by_android_count = 0;
  for (auto* const login : built_in_backend_logins) {
    auto android_login_iter = android_logins.find(login);

    if (android_login_iter == android_logins.end()) {
      // Password from the |built_in_backend_| doesn't exist in the
      // |android_backend_|.
      callbacks_chain = base::BindOnce(
          &BuiltInBackendToAndroidBackendMigrator::AddLoginToBackend,
          weak_ptr_factory_.GetWeakPtr(), android_backend_, *login,
          std::move(callbacks_chain));

      continue;
    }

    // Password from the |built_in_backend_| exists in the |android_backend_|.
    auto* const android_login = (*android_login_iter);

    if (login->password_value == android_login->password_value) {
      // Passwords are identical, nothing else to do.
      continue;
    }

    // Passwords aren't identical. Pick the most recentl one. The most recent is
    // considered the one, which has the newest create, last used or modified
    // date.
    if (std::max({login->date_created, login->date_last_used,
                  login->date_password_modified}) >
        std::max({android_login->date_created, android_login->date_last_used,
                  android_login->date_password_modified})) {
      callbacks_chain = base::BindOnce(
          &BuiltInBackendToAndroidBackendMigrator::UpdateLoginInBackend,
          weak_ptr_factory_.GetWeakPtr(), android_backend_, *login,
          std::move(callbacks_chain));
    } else {
      migration_conflict_won_by_android_count++;
    }
  }
  metrics_reporter_->SetLocalConflictsWonByAndroidCount(
      migration_conflict_won_by_android_count);
  std::move(callbacks_chain).Run();
}

void BuiltInBackendToAndroidBackendMigrator::AddLoginToBackend(
    PasswordStoreBackend* backend,
    const PasswordForm& form,
    base::OnceClosure callback) {
  backend->AddLoginAsync(
      form,
      base::BindOnce(
          &BuiltInBackendToAndroidBackendMigrator::RunCallbackOrAbortMigration,
          weak_ptr_factory_.GetWeakPtr(), std::move(callback),
          GetMetricInfixFromBackend(backend),
          BackendOperationForMigration::kAddLogin));
}

void BuiltInBackendToAndroidBackendMigrator::UpdateLoginInBackend(
    PasswordStoreBackend* backend,
    const PasswordForm& form,
    base::OnceClosure callback) {
  backend->UpdateLoginAsync(
      form,
      base::BindOnce(
          &BuiltInBackendToAndroidBackendMigrator::RunCallbackOrAbortMigration,
          weak_ptr_factory_.GetWeakPtr(), std::move(callback),
          GetMetricInfixFromBackend(backend),
          BackendOperationForMigration::kUpdateLogin));
}

void BuiltInBackendToAndroidBackendMigrator::RemoveLoginFromBackend(
    PasswordStoreBackend* backend,
    const PasswordForm& form,
    base::OnceClosure callback) {
  backend->RemoveLoginAsync(
      FROM_HERE, form,
      base::BindOnce(
          &BuiltInBackendToAndroidBackendMigrator::RunCallbackOrAbortMigration,
          weak_ptr_factory_.GetWeakPtr(), std::move(callback),
          GetMetricInfixFromBackend(backend),
          BackendOperationForMigration::kRemoveLogin));
}

void BuiltInBackendToAndroidBackendMigrator::RunCallbackOrAbortMigration(
    base::OnceClosure callback,
    const std::string& backend_infix,
    BackendOperationForMigration backend_operation,
    PasswordChangesOrError changes_or_error) {
  if (absl::holds_alternative<PasswordStoreBackendError>(changes_or_error)) {
    const PasswordStoreBackendError& error =
        absl::get<PasswordStoreBackendError>(changes_or_error);
    metrics_reporter_->HandleBackendOperationResult(
        backend_infix, backend_operation, /*is_success=*/false,
        error.android_backend_api_error);
    MigrationFinished(/*is_success=*/false);
    return;
  }

  const PasswordChanges& changes = absl::get<PasswordChanges>(changes_or_error);
  // Nullopt changelist is returned on success by the backends that do not
  // provide exact changelist (e.g. Android). This indicates success operation
  // as well as non-empty changelist.
  if (!changes.has_value() || !changes.value().empty()) {
    metrics_reporter_->HandleBackendOperationResult(backend_infix,
                                                    backend_operation,
                                                    /*is_success=*/true,
                                                    /*api_error=*/std::nullopt);
    // The step was successful, continue the migration.
    std::move(callback).Run();
    return;
  }

  // Migration failed.
  // It is unclear what the reason for this could be, but since there
  // was technically no API error, there is none to record.
  metrics_reporter_->HandleBackendOperationResult(
      backend_infix, backend_operation, /*is_success=*/false,
      /*api_error=*/std::nullopt);
  MigrationFinished(/*is_success=*/false);
}

void BuiltInBackendToAndroidBackendMigrator::MigrationFinished(
    bool is_success) {
  DCHECK(metrics_reporter_);
  metrics_reporter_->ReportMetrics(is_success);
  metrics_reporter_.reset();

  if (is_success && migration_in_progress_) {
    SchedulePostMigrationBottomSheet(prefs_);
    prefs_->SetInteger(prefs::kCurrentMigrationVersionToGoogleMobileServices,
                       kRequiredMigrationVersion);
    ResetUnenrollmentStatus(prefs_);
    prefs_->SetInteger(
        prefs::kPasswordsUseUPMLocalAndSeparateStores,
        static_cast<int>(
            password_manager::prefs::UseUpmLocalAndSeparateStoresState::kOn));
  }

  migration_in_progress_ = false;
  TRACE_EVENT_NESTABLE_ASYNC_END0("passwords",
                                  "UnifiedPasswordManagerMigration", this);
}

void BuiltInBackendToAndroidBackendMigrator::RemoveBlocklistedFormsWithValues(
    PasswordStoreBackend* backend,
    LoginsOrErrorReply result_callback,
    LoginsResultOrError logins_or_error) {
  if (absl::holds_alternative<PasswordStoreBackendError>(logins_or_error)) {
    std::move(result_callback).Run(std::move(logins_or_error));
    return;
  }

  LoginsResult all_forms = absl::get<LoginsResult>(std::move(logins_or_error));
  LoginsResult clean_forms;
  LoginsResult forms_to_remove;
  clean_forms.reserve(all_forms.size());
  forms_to_remove.reserve(all_forms.size());

  for (auto& form : all_forms) {
    if (IsBlocklistedFormWithValues(form)) {
      forms_to_remove.push_back(std::move(form));
    } else {
      clean_forms.push_back(std::move(form));
    }
  }

  auto callback_chain =
      base::BindOnce(std::move(result_callback), std::move(clean_forms));

  for (auto& form : forms_to_remove) {
    callback_chain = base::BindOnce(
        &BuiltInBackendToAndroidBackendMigrator::RemoveLoginFromBackend,
        weak_ptr_factory_.GetWeakPtr(), backend, form,
        std::move(callback_chain));
  }

  std::move(callback_chain).Run();
}

std::string BuiltInBackendToAndroidBackendMigrator::GetMetricInfixFromBackend(
    PasswordStoreBackend* backend) {
  return backend == built_in_backend_ ? "BuiltInBackend" : "AndroidBackend";
}

}  // namespace password_manager