chromium/ios/chrome/credential_provider_extension/ui/new_password_mediator.mm

// 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.

#import "ios/chrome/credential_provider_extension/ui/new_password_mediator.h"

#import <AuthenticationServices/AuthenticationServices.h>

#import "base/strings/sys_string_conversions.h"
#import "components/autofill/core/browser/proto/password_requirements.pb.h"
#import "components/password_manager/core/browser/generation/password_generator.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/app_group/app_group_metrics.h"
#import "ios/chrome/common/credential_provider/archivable_credential.h"
#import "ios/chrome/common/credential_provider/archivable_credential_util.h"
#import "ios/chrome/common/credential_provider/constants.h"
#import "ios/chrome/common/credential_provider/credential_store.h"
#import "ios/chrome/common/credential_provider/user_defaults_credential_store.h"
#import "ios/chrome/credential_provider_extension/metrics_util.h"
#import "ios/chrome/credential_provider_extension/password_spec_fetcher_buildflags.h"
#import "ios/chrome/credential_provider_extension/password_util.h"
#import "ios/chrome/credential_provider_extension/ui/credential_response_handler.h"
#import "ios/chrome/credential_provider_extension/ui/new_password_ui_handler.h"
#import "ios/chrome/credential_provider_extension/ui/ui_util.h"
#import "ios/components/credential_provider_extension/password_spec_fetcher.h"

using autofill::GeneratePassword;
using autofill::PasswordRequirementsSpec;
using base::SysUTF16ToNSString;

@interface NewPasswordMediator ()

// The service identifier the password is being created for,
@property(nonatomic, strong) ASCredentialServiceIdentifier* serviceIdentifier;

// The NSUserDefaults new credentials should be stored to.
@property(nonatomic, strong) NSUserDefaults* userDefaults;

// Fetcher for password specs.
@property(nonatomic, strong) PasswordSpecFetcher* fetcher;

@end

@implementation NewPasswordMediator

- (instancetype)initWithUserDefaults:(NSUserDefaults*)userDefaults
                   serviceIdentifier:
                       (ASCredentialServiceIdentifier*)serviceIdentifier {
  self = [super init];
  if (self) {
    _userDefaults = userDefaults;
    _serviceIdentifier = serviceIdentifier;
    NSString* host = HostForServiceIdentifier(serviceIdentifier);
    _fetcher =
        [[PasswordSpecFetcher alloc] initWithHost:host
                                           APIKey:BUILDFLAG(GOOGLE_API_KEY)];
    [_fetcher fetchSpecWithCompletion:nil];
  }
  return self;
}

#pragma mark - NewCredentialHandler

- (void)userDidRequestGeneratedPassword {
  if (self.fetcher.didFetchSpec) {
    PasswordRequirementsSpec spec = self.fetcher.spec;
    [self.uiHandler setPassword:SysUTF16ToNSString(GeneratePassword(spec))];
    return;
  }
  __weak __typeof__(self) weakSelf = self;
  [self.fetcher fetchSpecWithCompletion:^(PasswordRequirementsSpec spec) {
    [weakSelf.uiHandler setPassword:SysUTF16ToNSString(GeneratePassword(spec))];
  }];
}

- (void)saveCredentialWithUsername:(NSString*)username
                          password:(NSString*)password
                              note:(NSString*)note
                              gaia:(NSString*)gaia
                     shouldReplace:(BOOL)shouldReplace {
  if (!shouldReplace && [self credentialExistsForUsername:username]) {
    [self.uiHandler alertUserCredentialExists];
    return;
  }

  ArchivableCredential* credential =
      [self createNewCredentialWithUsername:username
                                   password:password
                                       note:note
                                       gaia:gaia];

  if (!credential) {
    [self.uiHandler alertSavePasswordFailed];
    return;
  }

  [self
      saveNewCredential:credential
             completion:^(NSError* error) {
               if (error) {
                 UpdateUMACountForKey(
                     app_group::kCredentialExtensionSaveCredentialFailureCount);
                 [self.uiHandler alertSavePasswordFailed];
                 return;
               }
               [self.uiHandler credentialSaved:credential];
               [self userSelectedCredential:credential];
             }];
}

#pragma mark - Private

// Checks whether a credential already exists with the given username.
- (BOOL)credentialExistsForUsername:(NSString*)username {
  NSURL* url = [NSURL URLWithString:[self currentIdentifier]];
  NSString* recordIdentifier = RecordIdentifierForData(url, username);

  return [self.existingCredentials
      credentialWithRecordIdentifier:recordIdentifier];
}

// Creates a new credential but doesn't add it to any stores.
- (ArchivableCredential*)createNewCredentialWithUsername:(NSString*)username
                                                password:(NSString*)password
                                                    note:(NSString*)note
                                                    gaia:(NSString*)gaia {
  NSString* identifier = [self currentIdentifier];
  NSURL* url = [NSURL URLWithString:identifier];
  NSString* recordIdentifier = RecordIdentifierForData(url, username);

  return [[ArchivableCredential alloc] initWithFavicon:nil
                                                  gaia:gaia
                                              password:password
                                                  rank:1
                                      recordIdentifier:recordIdentifier
                                     serviceIdentifier:identifier
                                           serviceName:url.host ?: identifier
                                              username:username
                                                  note:note];
}

// Saves the given credential to disk and calls `completion` once the operation
// is finished.
- (void)saveNewCredential:(ArchivableCredential*)credential
               completion:(void (^)(NSError* error))completion {
  NSString* key = AppGroupUserDefaultsCredentialProviderNewCredentials();
  UserDefaultsCredentialStore* store = [[UserDefaultsCredentialStore alloc]
      initWithUserDefaults:self.userDefaults
                       key:key];

  if ([store credentialWithRecordIdentifier:credential.recordIdentifier]) {
    [store updateCredential:credential];
  } else {
    [store addCredential:credential];
  }

  [store saveDataWithCompletion:completion];
}

// Alerts the host app that the user selected a credential.
- (void)userSelectedCredential:(id<Credential>)credential {
  NSString* password = credential.password;
  ASPasswordCredential* passwordCredential =
      [ASPasswordCredential credentialWithUser:credential.username
                                      password:password];
  [self.credentialResponseHandler userSelectedPassword:passwordCredential];
}

- (NSString*)currentIdentifier {
  NSString* identifier = self.serviceIdentifier.identifier;

  // According to Apple
  // (https://developer.apple.com/documentation/xcode/supporting-associated-domains).
  // associated domains must have an https:// scheme, and to autofill passwords
  // an associated domain is needed
  // (https://developer.apple.com/documentation/security/password_autofill/).
  // Also iOS strips https:// from passed identifier, Chrome restores it here to
  // save a valid URL.
  if (self.serviceIdentifier.type == ASCredentialServiceIdentifierTypeDomain &&
      ![identifier hasPrefix:@"https://"]) {
    identifier = [@"https://" stringByAppendingString:identifier];
  }
  return identifier;
}

@end