chromium/ios/chrome/browser/credential_provider/model/credential_provider_service.mm

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

#import "ios/chrome/browser/credential_provider/model/credential_provider_service.h"

#import <AuthenticationServices/AuthenticationServices.h>

#import "base/check.h"
#import "base/metrics/histogram_functions.h"
#import "base/notreached.h"
#import "base/strings/strcat.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "build/build_config.h"
#import "components/affiliations/core/browser/affiliation_service.h"
#import "components/affiliations/core/browser/affiliation_utils.h"
#import "components/password_manager/core/browser/affiliation/affiliated_match_helper.h"
#import "components/password_manager/core/browser/password_manager_util.h"
#import "components/password_manager/core/browser/password_store/password_store_change.h"
#import "components/password_manager/core/browser/password_store/password_store_interface.h"
#import "components/password_manager/core/browser/password_store/password_store_util.h"
#import "components/password_manager/core/browser/password_sync_util.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/core/common/password_manager_pref_names.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_service_utils.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/credential_provider/model/archivable_credential+password_form.h"
#import "ios/chrome/browser/credential_provider/model/credential_provider_util.h"
#import "ios/chrome/browser/credential_provider/model/features.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/credential_provider/ASPasskeyCredentialIdentity+credential.h"
#import "ios/chrome/common/credential_provider/ASPasswordCredentialIdentity+credential.h"
#import "ios/chrome/common/credential_provider/archivable_credential+passkey.h"
#import "ios/chrome/common/credential_provider/constants.h"
#import "ios/chrome/common/credential_provider/credential_store.h"

namespace {

using affiliations::AffiliationService;
using password_manager::AffiliatedMatchHelper;
using password_manager::PasswordForm;
using password_manager::PasswordStoreChange;
using password_manager::PasswordStoreChangeList;
using password_manager::PasswordStoreInterface;

// ASCredentialIdentityStoreError enum to report UMA metrics. Must be in sync
// with iOSCredentialIdentityStoreErrorForReporting in
// tools/metrics/histograms/enums.xml.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class CredentialIdentityStoreErrorForReporting {
  kUnknownError,
  kInternal,
  kDisabled,
  kBusy,
  kMaxValue = kBusy
};

// Converts a UIKit interface style to an interface style for reporting.
CredentialIdentityStoreErrorForReporting
ErrorForReportingForASCredentialIdentityStoreErrorCode(
    ASCredentialIdentityStoreErrorCode errorCode) {
  switch (errorCode) {
    case ASCredentialIdentityStoreErrorCodeInternalError:
      return CredentialIdentityStoreErrorForReporting::kInternal;
    case ASCredentialIdentityStoreErrorCodeStoreDisabled:
      return CredentialIdentityStoreErrorForReporting::kDisabled;
    case ASCredentialIdentityStoreErrorCodeStoreBusy:
      return CredentialIdentityStoreErrorForReporting::kBusy;
  }
  return CredentialIdentityStoreErrorForReporting::kUnknownError;
}

void SyncASIdentityStore(id<CredentialStore> credential_store) {
  auto stateCompletion = ^(ASCredentialIdentityStoreState* state) {
#if !defined(NDEBUG)
    dispatch_assert_queue_not(dispatch_get_main_queue());
#endif  // !defined(NDEBUG)
    if (state.enabled) {
      NSArray<id<Credential>>* credentials = credential_store.credentials;
      auto replaceCompletion = ^(BOOL success, NSError* error) {
        // Sometimes ASCredentialIdentityStore fails. Log this to measure the
        // impact of these failures and move on.
        if (!success) {
          ASCredentialIdentityStoreErrorCode code =
              static_cast<ASCredentialIdentityStoreErrorCode>(error.code);
          CredentialIdentityStoreErrorForReporting errorForReporting =
              ErrorForReportingForASCredentialIdentityStoreErrorCode(code);
          base::UmaHistogramEnumeration(
              "IOS.CredentialExtension.Service.Error."
              "ReplaceCredentialIdentitiesWithIdentities",
              errorForReporting);
        }
      };
      if (@available(iOS 17.0, *)) {
        NSMutableArray<id<ASCredentialIdentity>>* storeIdentities =
            [NSMutableArray arrayWithCapacity:credentials.count];
        for (id<Credential> credential in credentials) {
          if (credential.isPasskey) {
            [storeIdentities addObject:[[ASPasskeyCredentialIdentity alloc]
                                           cr_initWithCredential:credential]];
          } else {
            [storeIdentities addObject:[[ASPasswordCredentialIdentity alloc]
                                           cr_initWithCredential:credential]];
          }
        }
        [ASCredentialIdentityStore.sharedStore
            replaceCredentialIdentityEntries:storeIdentities
                                  completion:replaceCompletion];
      } else {
        NSMutableArray<ASPasswordCredentialIdentity*>* storeIdentities =
            [NSMutableArray arrayWithCapacity:credentials.count];
        for (id<Credential> credential in credentials) {
          [storeIdentities addObject:[[ASPasswordCredentialIdentity alloc]
                                         cr_initWithCredential:credential]];
        }
        [ASCredentialIdentityStore.sharedStore
            replaceCredentialIdentitiesWithIdentities:storeIdentities
                                           completion:replaceCompletion];
      }
    }
  };
  [ASCredentialIdentityStore.sharedStore
      getCredentialIdentityStoreStateWithCompletion:stateCompletion];
}

bool CanSendHistoryData(syncer::SyncService* sync_service) {
  // SESSIONS and HISTORY both contain history-like data, so it's sufficient if
  // either of them is being uploaded.
  return syncer::GetUploadToGoogleState(sync_service,
                                        syncer::DataType::SESSIONS) ==
             syncer::UploadState::ACTIVE ||
         syncer::GetUploadToGoogleState(sync_service,
                                        syncer::DataType::HISTORY) ==
             syncer::UploadState::ACTIVE;
}

void RecordNumberFaviconsFetched(size_t fetched_favicon_count) {
  base::UmaHistogramCounts10000("IOS.CredentialExtension.NumberFaviconsFetched",
                                fetched_favicon_count);
}

}  // namespace

CredentialProviderService::CredentialProviderService(
    PrefService* prefs,
    scoped_refptr<PasswordStoreInterface> profile_password_store,
    scoped_refptr<PasswordStoreInterface> account_password_store,
    webauthn::PasskeyModel* passkey_model,
    id<MutableCredentialStore> credential_store,
    signin::IdentityManager* identity_manager,
    syncer::SyncService* sync_service,
    affiliations::AffiliationService* affiliation_service,
    FaviconLoader* favicon_loader)
    : prefs_(prefs),
      profile_password_store_(profile_password_store),
      account_password_store_(account_password_store),
      passkey_model_(passkey_model),
      identity_manager_(identity_manager),
      sync_service_(sync_service),
      affiliated_helper_(
          std::make_unique<AffiliatedMatchHelper>(affiliation_service)),
      favicon_loader_(favicon_loader),
      dual_credential_store_(credential_store) {
  CHECK(profile_password_store_);
  CHECK(identity_manager_);
  CHECK(sync_service_);
  CHECK(favicon_loader_);
  CHECK(dual_credential_store_);

  profile_password_store_->AddObserver(this);
  if (account_password_store_) {
    account_password_store_->AddObserver(this);
  }
  if (passkey_model_) {
    passkey_model_->AddObserver(this);
  }

  UpdateAccountId();
  UpdateUserEmail();

  identity_manager_->AddObserver(this);
  sync_service_->AddObserver(this);

  // This class should usually handle incremental PasswordStore updates in
  // OnLoginsChanged(), but there could be bugs. E.g. maybe an update is fired
  // before the observer is added. So re-write the data on startup as a
  // safeguard. Post a task for performance.
  // Note: in reality this re-write does the same IO work as saving a new
  // password. The implementations of MutableCredentialStore write *every*
  // password to disk, even in OnLoginsChanged().
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&CredentialProviderService::RequestSyncAllCredentials,
                     weak_ptr_factory_.GetWeakPtr()),
      base::Seconds(5));

  saving_passwords_enabled_.Init(
      password_manager::prefs::kCredentialsEnableService, prefs,
      base::BindRepeating(
          &CredentialProviderService::OnSavingPasswordsEnabledChanged,
          base::Unretained(this)));

  // Make sure the initial value of the pref is stored.
  OnSavingPasswordsEnabledChanged();
}

CredentialProviderService::~CredentialProviderService() {}

void CredentialProviderService::Shutdown() {
  profile_password_store_->RemoveObserver(this);
  if (account_password_store_) {
    account_password_store_->RemoveObserver(this);
  }
  if (passkey_model_) {
    passkey_model_->RemoveObserver(this);
  }
  identity_manager_->RemoveObserver(this);
  sync_service_->RemoveObserver(this);
}

void CredentialProviderService::OnLoginsChanged(
    password_manager::PasswordStoreInterface* store,
    const PasswordStoreChangeList& changes) {
  std::vector<PasswordForm> forms_to_add, forms_to_remove;
  for (const PasswordStoreChange& change : changes) {
    if (change.form().blocked_by_user) {
      continue;
    }
    switch (change.type()) {
      case PasswordStoreChange::ADD:
        forms_to_add.push_back(change.form());
        break;
      case PasswordStoreChange::UPDATE:
        // Only act on updates if they involve a password change. This is
        // because using a passwords triggers this code path, since it updates
        // the use count and use date. Ideally we shouldn't care about this, but
        // for now the whole password file is re-written on every change, which
        // is inefficient. Username changes are not considered updates, but
        // instead treated as a new credential (REMOVE then ADD).
        if (!IsCPEPerformanceImprovementsEnabled() ||
            change.password_changed()) {
          forms_to_remove.push_back(change.form());
          forms_to_add.push_back(change.form());
        }
        break;
      case PasswordStoreChange::REMOVE:
        forms_to_remove.push_back(change.form());
        break;
      default:
        NOTREACHED_IN_MIGRATION();
        break;
    }
  }

  if (IsCPEPerformanceImprovementsEnabled()) {
    if (!forms_to_remove.empty()) {
      RemoveCredentials(GetCredentialStore(store), std::move(forms_to_remove));

      // Need to commit the removal to disk if there will not be forms added
      // afterwards.
      if (forms_to_add.empty()) {
        SyncStore();
      }
    }

    if (!forms_to_add.empty()) {
      auto callback = base::BindOnce(
          &CredentialProviderService::OnInjectedAffiliationAfterLoginsChanged,
          weak_ptr_factory_.GetWeakPtr(), base::Unretained(store));

      affiliated_helper_->InjectAffiliationAndBrandingInformation(
          std::move(forms_to_add), std::move(callback));
    }
  } else {
    RemoveCredentials(GetCredentialStore(store), std::move(forms_to_remove));

    auto callback = base::BindOnce(
        &CredentialProviderService::OnInjectedAffiliationAfterLoginsChanged,
        weak_ptr_factory_.GetWeakPtr(), base::Unretained(store));

    affiliated_helper_->InjectAffiliationAndBrandingInformation(
        std::move(forms_to_add), std::move(callback));
  }
}

void CredentialProviderService::RequestSyncAllCredentials() {
  profile_password_store_->GetAutofillableLogins(
      weak_ptr_factory_.GetWeakPtr());
  if (account_password_store_) {
    account_password_store_->GetAutofillableLogins(
        weak_ptr_factory_.GetWeakPtr());
  }
}

void CredentialProviderService::SyncAllCredentials(
    password_manager::PasswordStoreInterface* store,
    password_manager::LoginsResultOrError forms_or_error) {
  std::vector<PasswordForm> forms =
      password_manager::GetLoginsOrEmptyListOnFailure(
          std::move(forms_or_error));

  MemoryCredentialStore* memoryCredentialStore = GetCredentialStore(store);
  AddCredentials(memoryCredentialStore, std::move(forms));
  // We only sync passkeys into the account store.
  if (passkey_model_ && (store == account_password_store_)) {
    AddCredentials(memoryCredentialStore, passkey_model_->GetAllPasskeys());
  }
  SyncStore();
}

void CredentialProviderService::SyncStore() {
  base::UmaHistogramBoolean(kSyncStoreHistogramName, true);

  [dual_credential_store_ removeAllCredentials];
  for (id<Credential> credential in profile_credential_store_.credentials) {
    [dual_credential_store_ addCredential:credential];
  }
  for (id<Credential> credential in account_credential_store_.credentials) {
    [dual_credential_store_ addCredential:credential];
  }

  __weak id<CredentialStore> weak_credential_store = dual_credential_store_;
  [dual_credential_store_ saveDataWithCompletion:^(NSError* error) {
    if (error) {
      return;
    }
    if (weak_credential_store) {
      SyncASIdentityStore(weak_credential_store);
    }
  }];
}

void CredentialProviderService::AddCredentials(
    MemoryCredentialStore* store,
    std::vector<PasswordForm> forms) {
  if (IsCPEPerformanceImprovementsEnabled()) {
    AddCredentialsRefactored(store, forms);
  } else {
    AddCredentialsLegacy(store, forms);
  }
}

void CredentialProviderService::AddCredentialsLegacy(
    MemoryCredentialStore* store,
    std::vector<PasswordForm> forms) {
  // User is adding a password (not batch add from user login).
  const bool should_skip_max_verification = forms.size() == 1;
  const bool fallback_to_google_server_allowed =
      CanSendHistoryData(sync_service_);
  CoreAccountInfo account =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  NSString* gaia = base::SysUTF8ToNSString(account.gaia);

  int fetched_favicon_count = 0;

  for (const PasswordForm& form : forms) {
    NSString* favicon_key;
    // Only fetch favicon for valid URL. FaviconLoader::FaviconForPageUrl does
    // not take Android facet URI.
    if (form.url.is_valid()) {
      ++fetched_favicon_count;
      favicon_key = GetFaviconFileKey(form.url);

      // Fetch the favicon and save it to the storage.
      FetchFaviconForURLToPath(favicon_loader_, form.url, favicon_key,
                               should_skip_max_verification,
                               fallback_to_google_server_allowed);
    }

    // Only store password with valid Android facet URI or valid URL.
    if (affiliations::IsValidAndroidFacetURI(form.signon_realm) ||
        form.url.is_valid()) {
      ArchivableCredential* credential =
          [[ArchivableCredential alloc] initWithPasswordForm:form
                                                     favicon:favicon_key
                                                        gaia:gaia];
      DCHECK(credential);
      [store addCredential:credential];
    }
  }

  RecordNumberFaviconsFetched(fetched_favicon_count);
}

void CredentialProviderService::AddCredentialsRefactored(
    MemoryCredentialStore* store,
    std::vector<PasswordForm> forms) {
  // Dont' rate limit the favicon fetch when adding a single password.
  const bool should_skip_max_verification = forms.size() == 1;
  const bool fallback_to_google_server_allowed =
      CanSendHistoryData(sync_service_);
  CoreAccountInfo account =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  NSString* gaia = base::SysUTF8ToNSString(account.gaia);

  // Get the list of existing favicon files, along with their creation date.
  NSDictionary<NSString*, NSDate*>* favicon_dict =
      GetFaviconsListAndFreshness();
  int fetched_favicon_count = 0;

  for (const PasswordForm& form : forms) {
    NSString* favicon_key;
    if (form.url.is_valid()) {
      favicon_key = GetFaviconFileKey(form.url);

      if (ShouldFetchFavicon(favicon_key, favicon_dict)) {
        ++fetched_favicon_count;

        // Fetch the favicon and save it to the storage.
        FetchFaviconForURLToPath(favicon_loader_, form.url, favicon_key,
                                 should_skip_max_verification,
                                 fallback_to_google_server_allowed);
      }
    }

    // Only store password with valid Android facet URI or valid URL.
    if (affiliations::IsValidAndroidFacetURI(form.signon_realm) ||
        form.url.is_valid()) {
      ArchivableCredential* credential =
          [[ArchivableCredential alloc] initWithPasswordForm:form
                                                     favicon:favicon_key
                                                        gaia:gaia];
      DCHECK(credential);
      [store addCredential:credential];
    }
  }

  RecordNumberFaviconsFetched(fetched_favicon_count);
}

void CredentialProviderService::AddCredentials(
    MemoryCredentialStore* store,
    std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys) {
  // User is adding a passkey (not batch add from user login).
  const bool should_skip_max_verification = passkeys.size() == 1;
  const bool fallback_to_google_server = CanSendHistoryData(sync_service_);
  CoreAccountInfo account =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  NSString* gaia = base::SysUTF8ToNSString(account.gaia);

  for (const auto& passkey : passkeys) {
    // Only fetch favicon for valid URL.
    GURL url(base::StrCat(
        {url::kHttpsScheme, url::kStandardSchemeSeparator, passkey.rp_id()}));
    if (url.is_valid()) {
      NSString* favicon_key = GetFaviconFileKey(url);

      // Fetch the favicon and save it to the storage.
      FetchFaviconForURLToPath(favicon_loader_, url, favicon_key,
                               should_skip_max_verification,
                               fallback_to_google_server);

      ArchivableCredential* credential =
          [[ArchivableCredential alloc] initWithFavicon:favicon_key
                                                   gaia:gaia
                                                passkey:passkey];
      DCHECK(credential);
      [store addCredential:credential];
    }
  }
}

void CredentialProviderService::RemoveCredentials(
    MemoryCredentialStore* store,
    std::vector<PasswordForm> forms) {
  for (const auto& form : forms) {
    NSString* recordID = RecordIdentifierForPasswordForm(form);
    DCHECK(recordID);
    [store removeCredentialWithRecordIdentifier:recordID];
  }
}

void CredentialProviderService::RemoveCredentials(
    MemoryCredentialStore* store,
    std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys) {
  for (const auto& passkey : passkeys) {
    NSString* recordID = RecordIdentifierForPasskey(passkey);
    DCHECK(recordID);
    [store removeCredentialWithRecordIdentifier:recordID];
  }
}

void CredentialProviderService::UpdateAccountId() {
  CoreAccountInfo account =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  NSString* account_id = nil;
  if (!account.IsEmpty() &&
      identity_manager_->FindExtendedAccountInfo(account).IsManaged()) {
    account_id = base::SysUTF8ToNSString(account.gaia);
  }
  [app_group::GetGroupUserDefaults()
      setObject:account_id
         forKey:AppGroupUserDefaultsCredentialProviderUserID()];
}

void CredentialProviderService::UpdateUserEmail() {
  std::optional accountForSaving =
      password_manager::sync_util::GetAccountForSaving(prefs_, sync_service_);
  [app_group::GetGroupUserDefaults()
      setObject:accountForSaving ? base::SysUTF8ToNSString(*accountForSaving)
                                 : nil
         forKey:AppGroupUserDefaultsCredentialProviderUserEmail()];
}

void CredentialProviderService::OnGetPasswordStoreResultsOrErrorFrom(
    password_manager::PasswordStoreInterface* store,
    password_manager::LoginsResultOrError results) {
  auto callback =
      base::BindOnce(&CredentialProviderService::SyncAllCredentials,
                     weak_ptr_factory_.GetWeakPtr(), base::Unretained(store));
  affiliated_helper_->InjectAffiliationAndBrandingInformation(
      password_manager::GetLoginsOrEmptyListOnFailure(std::move(results)),
      std::move(callback));
}

void CredentialProviderService::OnPrimaryAccountChanged(
    const signin::PrimaryAccountChangeEvent& event) {
  switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
    case signin::PrimaryAccountChangeEvent::Type::kSet:
    case signin::PrimaryAccountChangeEvent::Type::kCleared:
      UpdateAccountId();
      UpdateUserEmail();
      break;
    case signin::PrimaryAccountChangeEvent::Type::kNone:
      break;
  }
}

void CredentialProviderService::OnLoginsRetained(
    password_manager::PasswordStoreInterface* /*store*/,
    const std::vector<password_manager::PasswordForm>& /*retained_passwords*/) {
}

void CredentialProviderService::OnInjectedAffiliationAfterLoginsChanged(
    password_manager::PasswordStoreInterface* store,
    password_manager::LoginsResultOrError results_or_error) {
  AddCredentials(GetCredentialStore(store),
                 password_manager::GetLoginsOrEmptyListOnFailure(
                     std::move(results_or_error)));
  SyncStore();
}

void CredentialProviderService::OnStateChanged(syncer::SyncService* sync) {
  // When the state changes, it's possible that password syncing has
  // started/stopped, so the user's email must be updated.
  UpdateUserEmail();
}

// PasskeyModel::Observer:
void CredentialProviderService::OnPasskeysChanged(
    const std::vector<webauthn::PasskeyModelChange>& changes) {
  // Passkeys get saved only into the account store.
  if (!account_password_store_) {
    return;
  }

  std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys_to_add;
  std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys_to_remove;
  for (const webauthn::PasskeyModelChange& change : changes) {
    const sync_pb::WebauthnCredentialSpecifics& passkey = change.passkey();
    switch (change.type()) {
      case webauthn::PasskeyModelChange::ChangeType::ADD:
        passkeys_to_add.push_back(passkey);
        break;
      case webauthn::PasskeyModelChange::ChangeType::REMOVE:
        passkeys_to_remove.push_back(passkey);
        break;
      case webauthn::PasskeyModelChange::ChangeType::UPDATE:
        // TODO(crbug.com/330355124): do something more optimal than this.
        passkeys_to_add.push_back(passkey);
        passkeys_to_remove.push_back(passkey);
        break;
      default:
        NOTREACHED();
    }
  }

  if (passkeys_to_add.empty() && passkeys_to_remove.empty()) {
    return;
  }

  if (!passkeys_to_remove.empty()) {
    RemoveCredentials(account_credential_store_, passkeys_to_remove);
  }

  if (!passkeys_to_add.empty()) {
    AddCredentials(account_credential_store_, passkeys_to_add);
  }

  SyncStore();
}

void CredentialProviderService::OnPasskeyModelShuttingDown() {
  if (passkey_model_) {
    passkey_model_->RemoveObserver(this);
  }
  passkey_model_ = nullptr;
}

void CredentialProviderService::OnSavingPasswordsEnabledChanged() {
  [app_group::GetGroupUserDefaults()
      setObject:[NSNumber numberWithBool:saving_passwords_enabled_.GetValue()]
         forKey:AppGroupUserDefaulsCredentialProviderSavingPasswordsEnabled()];
}

MemoryCredentialStore* CredentialProviderService::GetCredentialStore(
    password_manager::PasswordStoreInterface* store) const {
  return store == profile_password_store_ ? profile_credential_store_
                                          : account_credential_store_;
}