// Copyright 2023 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/password_manager_android_util.h"
#include <string>
#include "base/android/build_info.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/password_manager/android/password_manager_eviction_util.h"
#include "chrome/browser/password_manager/android/password_manager_util_bridge.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_manager_buildflags.h"
#include "components/password_manager/core/browser/password_manager_constants.h"
#include "components/password_manager/core/browser/password_sync_util.h"
#include "components/password_manager/core/browser/split_stores_and_local_upm.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/sync/base/pref_names.h"
#include "components/version_info/android/channel_getter.h"
#include "third_party/abseil-cpp/absl/base/attributes.h"
using password_manager::prefs::kCurrentMigrationVersionToGoogleMobileServices;
using password_manager::prefs::kPasswordsUseUPMLocalAndSeparateStores;
using password_manager::prefs::UseUpmLocalAndSeparateStoresState;
using password_manager::prefs::UseUpmLocalAndSeparateStoresState::kOff;
using password_manager::prefs::UseUpmLocalAndSeparateStoresState::
kOffAndMigrationPending;
using password_manager::prefs::UseUpmLocalAndSeparateStoresState::kOn;
namespace password_manager_android_util {
namespace {
enum class UserType {
kSyncing,
kNonSyncingAndMigrationNeeded,
kNonSyncingAndNoMigrationNeeded,
};
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. Keep in sync with the corresponding
// enum in tools/metrics/histograms/metadata/password/enums.xml.
enum class ActivationError {
kNone = 0,
// (Deprecated) kUnenrolled = 1,
// (Deprecated) kInitialUpmMigrationMissing = 2,
kLoginDbFileMoveFailed = 3,
kOutdatedGmsCore = 4,
// (Deprecated) kFlagDisabled = 5,
kMigrationWarningUnacknowledged = 6,
kMaxValue = kMigrationWarningUnacknowledged,
};
// Set on startup before the local passwords migration starts.
bool last_migration_attempt_failed = false;
bool IsPasswordSyncEnabled(PrefService* pref_service) {
// It's not possible to ask the SyncService whether password sync is enabled,
// the object wasn't created yet. Instead, that information is written to a
// pref during the previous execution and read now.
switch (browser_sync::GetSyncToSigninMigrationDataTypeDecision(
pref_service, syncer::PASSWORDS,
syncer::prefs::internal::kSyncPasswords)) {
// `kDontMigrateTypeNotActive` is handled same as if the data type was
// active, because all that matters is the user's choice to sync the type.
case browser_sync::SyncToSigninMigrationDataTypeDecision::
kDontMigrateTypeNotActive:
case browser_sync::SyncToSigninMigrationDataTypeDecision::kMigrate:
return true;
case browser_sync::SyncToSigninMigrationDataTypeDecision::
kDontMigrateTypeDisabled:
return false;
}
}
bool HasMinGmsVersion() {
std::string gms_version_str =
base::android::BuildInfo::GetInstance()->gms_version_code();
int gms_version = 0;
// gms_version_code() must be converted to int for comparison, because it can
// have legacy values "3(...)" and those evaluate > "2023(...)".
return base::StringToInt(gms_version_str, &gms_version) &&
gms_version >= password_manager::GetLocalUpmMinGmsVersion();
}
bool ShouldDelayMigrationUntillMigrationWarningIsAcknowledged(
PrefService* pref_service) {
// The migration warning is only relevant for non-stable channels.
version_info::Channel channel = version_info::android::GetChannel();
if (channel == version_info::Channel::STABLE) {
return false;
}
// If there are no passwords to migrate and migration is still needed for
// settings, there is no need to acknowledge the password migration warning.
if (pref_service->GetBoolean(
password_manager::prefs::kEmptyProfileStoreLoginDatabase)) {
return false;
}
// There is no warning shown on automotive.
if (base::android::BuildInfo::GetInstance()->is_automotive()) {
return false;
}
if (!base::FeatureList::IsEnabled(
password_manager::features::
kUnifiedPasswordManagerLocalPasswordsMigrationWarning)) {
return false;
}
return !pref_service->GetBoolean(
password_manager::prefs::kUserAcknowledgedLocalPasswordsMigrationWarning);
}
bool HasCustomPasswordSettings(PrefService* pref_service) {
bool has_custom_enable_service_setting =
!pref_service
->FindPreference(password_manager::prefs::kCredentialsEnableService)
->IsDefaultValue();
bool has_custom_auto_signin_setting =
!pref_service
->FindPreference(
password_manager::prefs::kCredentialsEnableAutosignin)
->IsDefaultValue();
return has_custom_enable_service_setting || has_custom_auto_signin_setting;
}
bool MustMigrateLocalPasswordsOrSettingsOnActivation(
PrefService* pref_service,
const base::FilePath& login_db_directory) {
CHECK(!IsPasswordSyncEnabled(pref_service));
// It's not possible to ask the (profile) PasswordStore whether it is empty,
// the object wasn't created yet. Instead, that information is written to the
// kEmptyProfileStoreLoginDatabase pref during the previous execution and read
// now. The pref is false by default, so a migration is required in doubt.
bool has_passwords_in_profile_login_db =
!pref_service->GetBoolean(
password_manager::prefs::kEmptyProfileStoreLoginDatabase) &&
base::PathExists(login_db_directory.Append(
password_manager::kLoginDataForProfileFileName));
return HasCustomPasswordSettings(pref_service) ||
has_passwords_in_profile_login_db;
}
UserType GetUserType(PrefService* pref_service,
const base::FilePath& login_db_directory) {
if (IsPasswordSyncEnabled(pref_service)) {
return UserType::kSyncing;
}
return MustMigrateLocalPasswordsOrSettingsOnActivation(pref_service,
login_db_directory)
? UserType::kNonSyncingAndMigrationNeeded
: UserType::kNonSyncingAndNoMigrationNeeded;
}
void RecordActivationError(UserType user_type, ActivationError error) {
const char kHistogramPrefix[] = "PasswordManager.LocalUpmActivationError";
const char* suffix = nullptr;
switch (user_type) {
case UserType::kNonSyncingAndMigrationNeeded:
suffix = ".NonSyncingWithMigration";
break;
case UserType::kNonSyncingAndNoMigrationNeeded:
suffix = ".NonSyncingNoMigration";
break;
case UserType::kSyncing:
suffix = ".Syncing";
break;
}
CHECK(suffix);
base::UmaHistogramEnumeration(base::StrCat({kHistogramPrefix, suffix}),
error);
base::UmaHistogramEnumeration(kHistogramPrefix, error);
}
// Must only be called if the state pref is kOff, to set it to kOn or
// kOffAndMigrationPending if all the preconditions are satisfied.
void MaybeActivateSplitStoresAndLocalUpm(
PrefService* pref_service,
const base::FilePath& login_db_directory) {
CHECK_EQ(GetSplitStoresAndLocalUpmPrefValue(pref_service), kOff);
UserType user_type = GetUserType(pref_service, login_db_directory);
if (!HasMinGmsVersion()) {
RecordActivationError(user_type, ActivationError::kOutdatedGmsCore);
return;
}
UseUpmLocalAndSeparateStoresState state_to_set_on_success = kOn;
ActivationError error = ActivationError::kNone;
switch (user_type) {
case UserType::kNonSyncingAndNoMigrationNeeded:
break;
case UserType::kNonSyncingAndMigrationNeeded:
if (ShouldDelayMigrationUntillMigrationWarningIsAcknowledged(
pref_service)) {
error = ActivationError::kMigrationWarningUnacknowledged;
break;
}
state_to_set_on_success = kOffAndMigrationPending;
break;
case UserType::kSyncing: {
// kCurrentMigrationVersionToGoogleMobileServices is only 0 or 1.
if (password_manager_upm_eviction::IsCurrentUserEvicted(pref_service) ||
pref_service->GetInteger(
kCurrentMigrationVersionToGoogleMobileServices) == 0) {
// Initial UPM was not activated properly. Attempt to migrate passwords
// to local GMSCore.
state_to_set_on_success = kOffAndMigrationPending;
break;
}
// Move the "profile" login DB to the "account" path, the latter is the
// synced one after activation. We could rely on a redownload instead, but
// a) this is a safety net, and b)it spares traffic.
base::FilePath profile_db_path = login_db_directory.Append(
password_manager::kLoginDataForProfileFileName);
if (!base::ReplaceFile(
profile_db_path,
login_db_directory.Append(
password_manager::kLoginDataForAccountFileName),
/*error=*/nullptr)) {
error = ActivationError::kLoginDbFileMoveFailed;
break;
}
break;
}
}
RecordActivationError(user_type, error);
if (error == ActivationError::kNone) {
pref_service->SetInteger(kPasswordsUseUPMLocalAndSeparateStores,
static_cast<int>(state_to_set_on_success));
}
}
// Called on startup from `MaybeDeactivateSplitStoresAndLocalUpm` to delete the
// login data files for users migrated to UPM. Must only be called if the value
// of the state pref `PasswordsUseUPMLocalAndSeparateStores` is `On` and there
// is no need for deactivation of local UPM.
void MaybeDeleteLoginDataFiles(PrefService* prefs,
const base::FilePath& login_db_directory) {
CHECK(password_manager::UsesSplitStoresAndUPMForLocal(prefs));
base::FilePath profile_db_path =
login_db_directory.Append(password_manager::kLoginDataForProfileFileName);
base::FilePath account_db_path =
login_db_directory.Append(password_manager::kLoginDataForAccountFileName);
base::FilePath profile_db_journal_path = login_db_directory.Append(
password_manager::kLoginDataJournalForProfileFileName);
base::FilePath account_db_journal_path = login_db_directory.Append(
password_manager::kLoginDataJournalForAccountFileName);
// Delete the login data files for the user migrated to UPM.
// In the unlikely case that the deletion operation fails, it will be
// retried upon next startup as part of
// `MaybeDeactivateSplitStoresAndLocalUpm`.
if (PathExists(profile_db_path)) {
bool success = base::DeleteFile(profile_db_path);
base::UmaHistogramBoolean("PasswordManager.ProfileLoginData.RemovalStatus",
success);
if (success) {
prefs->SetBoolean(
password_manager::prefs::kEmptyProfileStoreLoginDatabase, true);
}
}
base::DeleteFile(profile_db_journal_path);
if (PathExists(account_db_path)) {
bool success = base::DeleteFile(account_db_path);
base::UmaHistogramBoolean("PasswordManager.AccountLoginData.RemovalStatus",
success);
}
base::DeleteFile(account_db_journal_path);
}
// Must only be called if the state pref is kOn or kOffAndMigrationPending, to
// set it to kOff if the user downgraded GmsCore. Any passwords saved to GmsCore
// while in kOn will stay in GmsCore and become available again on the next
// successful activation; they will not be migrated back to the LoginDB. If the
// user is syncing, this function tries to undo [1] the Login DB file move done
// in MaybeActivateSplitStoresAndLocalUpm(), and aborts on failure [2].
//
// [1] In truth, this is only an "undo" if the user was already syncing *before*
// the activation. In rare cases, they might have been signed out with saved
// passwords, activated, enabled sync and now get deactivated. If so, this
// function overwrites a non-empty profile Login DB. That's fine: the content
// got migrated to GmsCore and will become available again on the next
// successful activation.
//
// [2] In hindsight, this is questionable, because the user stays marked as
// activated even though they can't use GmsCore APIs.
void MaybeDeactivateSplitStoresAndLocalUpm(
PrefService* pref_service,
const base::FilePath& login_db_directory) {
CHECK_NE(GetSplitStoresAndLocalUpmPrefValue(pref_service), kOff);
// Continue recording the metric for previously activated users. so they show
// up on the dashboard no matter the aggregation window. One caveat is the
// state recorded now might not be the same one where the user got activated
// E.g. they might have gone from syncing to non-syncing. Also the recording
// here ignores the possibility that rollback fails due to base::ReplaceFile()
// below, but that should be negligible.
RecordActivationError(GetUserType(pref_service, login_db_directory),
HasMinGmsVersion() ? ActivationError::kNone
: ActivationError::kOutdatedGmsCore);
if (HasMinGmsVersion()) {
// GmsCore was not downgraded, no need to deactivate.
if (GetSplitStoresAndLocalUpmPrefValue(pref_service) == kOn &&
base::FeatureList::IsEnabled(
password_manager::features::
kClearLoginDatabaseForAllMigratedUPMUsers)) {
MaybeDeleteLoginDataFiles(pref_service, login_db_directory);
}
return;
}
// GmsCore was downgraded, so from here on the function wants to deactivate.
base::FilePath profile_db_path =
login_db_directory.Append(password_manager::kLoginDataForProfileFileName);
base::FilePath account_db_path =
login_db_directory.Append(password_manager::kLoginDataForAccountFileName);
if (GetSplitStoresAndLocalUpmPrefValue(pref_service) == kOn &&
IsPasswordSyncEnabled(pref_service) &&
base::PathExists(account_db_path) &&
!base::ReplaceFile(account_db_path, profile_db_path, /*error=*/nullptr)) {
// See point [2] above.
return;
}
pref_service->SetInteger(kPasswordsUseUPMLocalAndSeparateStores,
static_cast<int>(kOff));
}
bool HasPasswordsInProfileStore(PrefService* pref_service) {
int total_passwords_in_profile_store = pref_service->GetInteger(
password_manager::prefs::kTotalPasswordsAvailableForProfile);
return total_passwords_in_profile_store > 0;
}
} // namespace
UseUpmLocalAndSeparateStoresState GetSplitStoresAndLocalUpmPrefValue(
PrefService* pref_service) {
auto value = static_cast<UseUpmLocalAndSeparateStoresState>(
pref_service->GetInteger(kPasswordsUseUPMLocalAndSeparateStores));
switch (value) {
case kOff:
case kOffAndMigrationPending:
case kOn:
return value;
}
NOTREACHED();
}
bool AreMinUpmRequirementsMet() {
if (!IsInternalBackendPresent()) {
return false;
}
int gms_version = 0;
// GMSCore version could not be parsed, probably no GMSCore installed.
if (!base::StringToInt(
base::android::BuildInfo::GetInstance()->gms_version_code(),
&gms_version)) {
return false;
}
// If the GMSCore version is pre-UPM an update is required.
return gms_version >= password_manager::kAccountUpmMinGmsVersion;
}
bool ShouldUseUpmWiring(const syncer::SyncService* sync_service,
const PrefService* pref_service) {
bool is_pwd_sync_enabled =
password_manager::sync_util::HasChosenToSyncPasswords(sync_service);
if (is_pwd_sync_enabled &&
password_manager_upm_eviction::IsCurrentUserEvicted(pref_service)) {
return false;
}
if (is_pwd_sync_enabled) {
return true;
}
return password_manager::UsesSplitStoresAndUPMForLocal(pref_service);
}
void SetUsesSplitStoresAndUPMForLocal(
PrefService* pref_service,
const base::FilePath& login_db_directory) {
UseUpmLocalAndSeparateStoresState split_stores_and_local_upm =
GetSplitStoresAndLocalUpmPrefValue(pref_service);
last_migration_attempt_failed =
split_stores_and_local_upm == kOffAndMigrationPending ? true : false;
if (split_stores_and_local_upm != kOff) {
MaybeDeactivateSplitStoresAndLocalUpm(pref_service, login_db_directory);
} else {
MaybeActivateSplitStoresAndLocalUpm(pref_service, login_db_directory);
}
// Records false for users who had a migration scheduled but weren't activated
// yet, which is different from RecordActivationError().
base::UmaHistogramBoolean(
"PasswordManager.LocalUpmActivated",
password_manager::UsesSplitStoresAndUPMForLocal(pref_service));
base::UmaHistogramEnumeration(
"PasswordManager.LocalUpmActivationStatus",
GetSplitStoresAndLocalUpmPrefValue(pref_service));
}
PasswordAccessLossWarningType GetPasswordAccessLossWarningType(
PrefService* pref_service) {
// No warning should be displayed to the users, who don't have any passwords
// in the profile store.
if (!HasPasswordsInProfileStore(pref_service)) {
return PasswordAccessLossWarningType::kNone;
}
std::string gms_version_str =
base::android::BuildInfo::GetInstance()->gms_version_code();
int gms_version = 0;
// GMSCore version could not be parsed, probably no GMSCore installed.
if (!base::StringToInt(gms_version_str, &gms_version)) {
return PasswordAccessLossWarningType::kNoGmsCore;
}
// GMSCore version is pre-UPM, update is required.
if (gms_version < password_manager::kAccountUpmMinGmsVersion) {
return PasswordAccessLossWarningType::kNoUpm;
}
// GMSCore version supports the account passwords, but doesn't support local
// passwords. Update is still required.
if (gms_version < password_manager::GetLocalUpmMinGmsVersion()) {
return PasswordAccessLossWarningType::kOnlyAccountUpm;
}
// GMSCore is up to date, but the local passwords migration has failed, so
// manual export/import flow should be done. Checking the
// `SplitStoresAndLocalUpmState` again here because the migration might have
// succeeded in this run.
if (last_migration_attempt_failed &&
GetSplitStoresAndLocalUpmPrefValue(pref_service) ==
kOffAndMigrationPending) {
return PasswordAccessLossWarningType::kNewGmsCoreMigrationFailed;
}
// Everything is fine, no warning will be shown.
return PasswordAccessLossWarningType::kNone;
}
} // namespace password_manager_android_util