chromium/ios/chrome/browser/ui/settings/password/password_settings_app_interface.mm

// Copyright 2019 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/ui/settings/password/password_settings_app_interface.h"

#import <MaterialComponents/MaterialSnackbar.h>

#import "base/apple/foundation_util.h"
#import "base/location.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_manager_test_utils.h"
#import "components/password_manager/core/browser/password_store/password_store_consumer.h"
#import "components/password_manager/core/browser/password_store/password_store_interface.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/core/common/password_manager_pref_names.h"
#import "components/password_manager/ios/fake_bulk_leak_check_service.h"
#import "components/prefs/pref_service.h"
#import "components/sync/protocol/webauthn_credential_specifics.pb.h"
#import "components/webauthn/core/browser/passkey_model.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_account_password_store_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_bulk_leak_check_service_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/webauthn/model/ios_passkey_model_factory.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/app/mock_reauthentication_module.h"
#import "ios/chrome/test/app/password_test_util.h"
#import "ios/public/provider/chrome/browser/passcode_settings/passcode_settings_api.h"
#import "url/gurl.h"
#import "url/origin.h"

using chrome_test_util::
    SetUpAndReturnMockReauthenticationModuleForPasswordManager;
using password_manager::FakeBulkLeakCheckService;
using password_manager::PasswordForm;

namespace {

constexpr char kEncrypted[] = "encrypted";

scoped_refptr<password_manager::PasswordStoreInterface>
GetPasswordProfileStore() {
  // Ensure that the fails in incognito mode by using IMPLICIT_ACCESS.
  return IOSChromeProfilePasswordStoreFactory::GetForBrowserState(
      chrome_test_util::GetOriginalBrowserState(),
      ServiceAccessType::IMPLICIT_ACCESS);
}

// Gets the account store of the password.
scoped_refptr<password_manager::PasswordStoreInterface>
GetPasswordAccountStore() {
  // Ensure that the fails in incognito mode by using IMPLICIT_ACCESS.
  return IOSChromeAccountPasswordStoreFactory::GetForBrowserState(
      chrome_test_util::GetOriginalBrowserState(),
      ServiceAccessType::IMPLICIT_ACCESS);
}

// Helper to get the passkey store.
webauthn::PasskeyModel* GetPasskeyStore() {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  return IOSPasskeyModelFactory::GetForBrowserState(browser_state);
}

// This class is used to obtain results from the PasswordStore and hence both
// check the success of store updates and ensure that store has finished
// processing.
class FakeStoreConsumer : public password_manager::PasswordStoreConsumer {
 public:
  void OnGetPasswordStoreResults(
      std::vector<std::unique_ptr<password_manager::PasswordForm>> obtained)
      override {
    obtained_ = std::move(obtained);
  }

  // Retrieves all logins from the profile password store and updates
  // `results_`. Returns true if the logins retrieved successfully.
  bool FetchProfileStoreResults() {
    results_.clear();
    ResetObtained();
    GetPasswordProfileStore()->GetAllLogins(weak_ptr_factory_.GetWeakPtr());
    bool responded =
        base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), ^bool {
          return !AreObtainedReset();
        });
    if (responded) {
      AppendObtainedToResults();
    }
    return responded;
  }

  // Retrieves all logins from the account password store and updates
  // `results_`. Returns true if the logins retrieved successfully.
  bool FetchAccountStoreResults() {
    results_.clear();
    ResetObtained();
    GetPasswordAccountStore()->GetAllLogins(weak_ptr_factory_.GetWeakPtr());
    bool responded =
        base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), ^bool {
          return !AreObtainedReset();
        });
    if (responded) {
      AppendObtainedToResults();
    }
    return responded;
  }

  const std::vector<password_manager::PasswordForm>& GetStoreResults() {
    return results_;
  }

 private:
  // Puts `obtained_` in a known state not corresponding to any PasswordStore
  // state.
  void ResetObtained() {
    obtained_.clear();
    obtained_.emplace_back(nullptr);
  }

  // Returns true if `obtained_` are in the reset state.
  bool AreObtainedReset() { return obtained_.size() == 1 && !obtained_[0]; }

  void AppendObtainedToResults() {
    for (const auto& source : obtained_) {
      results_.emplace_back(*source);
    }
    ResetObtained();
  }

  // Temporary cache of obtained store results.
  std::vector<std::unique_ptr<password_manager::PasswordForm>> obtained_;

  // Combination of fillable and blocked credentials from the store.
  std::vector<password_manager::PasswordForm> results_;

  base::WeakPtrFactory<FakeStoreConsumer> weak_ptr_factory_{this};
};

// Saves `form` to the password store and waits until the async processing is
// done.
bool SaveToPasswordProfileStore(const PasswordForm& form) {
  GetPasswordProfileStore()->AddLogin(form);
  // When we retrieve the form from the store, `in_store` should be set.
  password_manager::PasswordForm expected_form = form;
  expected_form.in_store = password_manager::PasswordForm::Store::kProfileStore;

  // Check the result and ensure PasswordStore processed this.
  FakeStoreConsumer consumer;
  if (!consumer.FetchProfileStoreResults()) {
    return false;
  }
  for (const auto& result : consumer.GetStoreResults()) {
    if (testing::Value(
            result, password_manager::EqualsIgnorePrimaryKey(expected_form))) {
      return true;
    }
  }
  return false;
}

// Saves `form` to the password account store and waits until the async
// processing is done.
// Returns true if `form` is saved successfully, otherwise returns false.
bool SaveToPasswordAccountStore(const PasswordForm& form) {
  GetPasswordAccountStore()->AddLogin(form);
  // When we retrieve the form from the store, `in_store` should be set.
  password_manager::PasswordForm expected_form = form;
  expected_form.in_store = password_manager::PasswordForm::Store::kAccountStore;

  // Check the result and ensure PasswordStore processed this.
  FakeStoreConsumer consumer;
  if (!consumer.FetchAccountStoreResults()) {
    return false;
  }
  for (const auto& result : consumer.GetStoreResults()) {
    if (testing::Value(
            result, password_manager::EqualsIgnorePrimaryKey(expected_form))) {
      return true;
    }
  }
  return false;
}

// Creates a PasswordForm with `index` being part of the username, password,
// origin and realm.
PasswordForm CreateSampleFormWithIndex(int index) {
  PasswordForm form;
  form.username_value =
      base::ASCIIToUTF16(base::StringPrintf("concrete username %02d", index));
  form.password_value =
      base::ASCIIToUTF16(base::StringPrintf("concrete password %02d", index));
  form.url = GURL(base::StringPrintf("https://www%02d.example.com", index));
  form.signon_realm = form.url.spec();
  form.date_created = base::Time::Now();
  return form;
}

bool ClearProfilePasswordStore() {
  GetPasswordProfileStore()->RemoveLoginsCreatedBetween(
      FROM_HERE, base::Time(), base::Time(), base::DoNothing());
  FakeStoreConsumer consumer;
  if (!consumer.FetchProfileStoreResults()) {
    return false;
  }
  return consumer.GetStoreResults().empty();
}

bool ClearAccountPasswordStore() {
  GetPasswordAccountStore()->RemoveLoginsCreatedBetween(
      FROM_HERE, base::Time(), base::Time(), base::DoNothing());
  FakeStoreConsumer consumer;
  if (!consumer.FetchAccountStoreResults()) {
    return false;
  }
  return consumer.GetStoreResults().empty();
}

bool ClearPasswordStores() {
  GetPasswordProfileStore()->RemoveLoginsCreatedBetween(
      FROM_HERE, base::Time(), base::Time(), base::DoNothing());
  GetPasswordAccountStore()->RemoveLoginsCreatedBetween(
      FROM_HERE, base::Time(), base::Time(), base::DoNothing());
  FakeStoreConsumer consumer;
  if (!consumer.FetchProfileStoreResults()) {
    return false;
  }
  if (!consumer.FetchAccountStoreResults()) {
    return false;
  }
  return consumer.GetStoreResults().empty();
}

}  // namespace

@implementation PasswordSettingsAppInterface

static std::unique_ptr<ScopedPasswordSettingsReauthModuleOverride>
    _scopedReauthOverride;

// Helper for accessing the scoped override's module.
+ (MockReauthenticationModule*)mockModule {
  DCHECK(_scopedReauthOverride);

  return base::apple::ObjCCastStrict<MockReauthenticationModule>(
      _scopedReauthOverride->module);
}

+ (void)setUpMockReauthenticationModule {
  _scopedReauthOverride =
      SetUpAndReturnMockReauthenticationModuleForPasswordManager();
}

+ (void)removeMockReauthenticationModule {
  _scopedReauthOverride = nullptr;
}

+ (void)mockReauthenticationModuleExpectedResult:
    (ReauthenticationResult)expectedResult {
  [self mockModule].expectedResult = expectedResult;
}

+ (void)mockReauthenticationModuleCanAttempt:(BOOL)canAttempt {
  DCHECK(_scopedReauthOverride);

  [self mockModule].canAttempt = canAttempt;
}

+ (void)mockReauthenticationModuleShouldReturnSynchronously:(BOOL)returnSync {
  [self mockModule].shouldReturnSynchronously = returnSync;
}

+ (void)mockReauthenticationModuleReturnMockedResult {
  [[self mockModule] returnMockedReauthenticationResult];
}

+ (void)dismissSnackBar {
  [MDCSnackbarManager.defaultManager
      dismissAndCallCompletionBlocksWithCategory:@"PasswordsSnackbarCategory"];
}

+ (void)saveExamplePasswordToProfileWithCount:(NSInteger)count {
  for (int i = 1; i <= count; ++i) {
    GetPasswordProfileStore()->AddLogin(CreateSampleFormWithIndex(i));
  }
}

+ (BOOL)saveExamplePasswordToProfileStore:(NSString*)password
                                 username:(NSString*)username
                                   origin:(NSString*)origin {
  PasswordForm example;
  example.username_value = base::SysNSStringToUTF16(username);
  example.password_value = base::SysNSStringToUTF16(password);
  example.url = GURL(base::SysNSStringToUTF16(origin));
  example.signon_realm = example.url.spec();
  example.date_created = base::Time::Now();
  return SaveToPasswordProfileStore(example);
}

+ (BOOL)saveExamplePasswordToAccountStore:(NSString*)password
                                 username:(NSString*)username
                                   origin:(NSString*)origin {
  PasswordForm example;
  example.username_value = base::SysNSStringToUTF16(username);
  example.password_value = base::SysNSStringToUTF16(password);
  example.url = GURL(base::SysNSStringToUTF16(origin));
  example.signon_realm = example.url.spec();
  example.date_created = base::Time::Now();
  return SaveToPasswordAccountStore(example);
}

+ (BOOL)saveExampleNoteToProfileStore:(NSString*)note
                             password:(NSString*)password
                             username:(NSString*)username
                               origin:(NSString*)origin {
  PasswordForm example;
  example.username_value = base::SysNSStringToUTF16(username);
  example.password_value = base::SysNSStringToUTF16(password);
  example.url = GURL(base::SysNSStringToUTF16(origin));
  example.signon_realm = example.url.spec();
  example.notes = {password_manager::PasswordNote(
      base::SysNSStringToUTF16(note), base::Time::Now())};
  return SaveToPasswordProfileStore(example);
}

+ (BOOL)saveCompromisedPasswordToProfileStore:(NSString*)password
                                     username:(NSString*)username
                                       origin:(NSString*)origin {
  PasswordForm example;
  example.username_value = base::SysNSStringToUTF16(username);
  example.password_value = base::SysNSStringToUTF16(password);
  example.url = GURL(base::SysNSStringToUTF16(origin));
  example.signon_realm = example.url.spec();
  example.password_issues.insert({password_manager::InsecureType::kLeaked,
                                  password_manager::InsecurityMetadata()});
  return SaveToPasswordProfileStore(example);
}

+ (BOOL)saveMutedCompromisedPasswordToProfileStore:(NSString*)password
                                          username:(NSString*)userName
                                            origin:(NSString*)origin {
  PasswordForm example;
  example.username_value = base::SysNSStringToUTF16(userName);
  example.password_value = base::SysNSStringToUTF16(password);
  example.url = GURL(base::SysNSStringToUTF16(origin));
  example.signon_realm = example.url.spec();
  example.password_issues.insert(
      {password_manager::InsecureType::kLeaked,
       password_manager::InsecurityMetadata(
           base::Time::Now(), password_manager::IsMuted(true),
           password_manager::TriggerBackendNotification(false))});
  return SaveToPasswordProfileStore(example);
}

+ (BOOL)saveExampleBlockedOriginToProfileStore:(NSString*)origin {
  PasswordForm example;
  example.url = GURL(base::SysNSStringToUTF16(origin));
  example.blocked_by_user = true;
  example.signon_realm = example.url.spec();
  return SaveToPasswordProfileStore(example);
}

+ (BOOL)saveExampleFederatedOriginToProfileStore:(NSString*)federatedOrigin
                                        username:(NSString*)username
                                          origin:(NSString*)origin {
  PasswordForm federated;
  federated.username_value = base::SysNSStringToUTF16(username);
  federated.url = GURL(base::SysNSStringToUTF16(origin));
  federated.signon_realm = federated.url.spec();
  federated.federation_origin =
      url::SchemeHostPort(GURL(base::SysNSStringToUTF16(federatedOrigin)));
  return SaveToPasswordProfileStore(federated);
}

+ (void)saveExamplePasskeyToStore:(NSString*)rpId
                           userId:(NSString*)userId
                         username:(NSString*)username
                  userDisplayName:(NSString*)userDisplayName {
  sync_pb::WebauthnCredentialSpecifics passkey;
  passkey.set_sync_id(base::RandBytesAsString(16));
  passkey.set_credential_id(base::RandBytesAsString(16));
  passkey.set_rp_id(base::SysNSStringToUTF8(rpId));
  passkey.set_user_id(base::SysNSStringToUTF8(userId));
  passkey.set_user_name(base::SysNSStringToUTF8(username));
  passkey.set_user_display_name(base::SysNSStringToUTF8(userDisplayName));
  passkey.set_encrypted(kEncrypted);
  GetPasskeyStore()->AddNewPasskeyForTesting(passkey);
}

+ (NSInteger)passwordProfileStoreResultsCount {
  FakeStoreConsumer consumer;
  if (!consumer.FetchProfileStoreResults()) {
    return -1;
  }
  return consumer.GetStoreResults().size();
}

+ (NSInteger)passwordAccountStoreResultsCount {
  FakeStoreConsumer consumer;
  if (!consumer.FetchAccountStoreResults()) {
    return -1;
  }
  return consumer.GetStoreResults().size();
}

+ (BOOL)clearProfilePasswordStore {
  return ClearProfilePasswordStore();
}

+ (BOOL)clearAccountPasswordStore {
  return ClearAccountPasswordStore();
}

+ (BOOL)clearPasswordStores {
  return ClearPasswordStores();
}

+ (BOOL)isCredentialsServiceEnabled {
  ChromeBrowserState* browserState =
      chrome_test_util::GetOriginalBrowserState();
  return browserState->GetPrefs()->GetBoolean(
      password_manager::prefs::kCredentialsEnableService);
}

+ (void)setFakeBulkLeakCheckBufferedState:
    (password_manager::BulkLeakCheckServiceInterface::State)state {
  FakeBulkLeakCheckService* fakeBulkLeakCheckService =
      static_cast<FakeBulkLeakCheckService*>(
          IOSChromeBulkLeakCheckServiceFactory::GetForBrowserState(
              chrome_test_util::GetOriginalBrowserState()));
  fakeBulkLeakCheckService->SetBufferedState(state);
}

+ (BOOL)isPasscodeSettingsAvailable {
  return ios::provider::SupportsPasscodeSettings();
}

@end