// 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/credential_provider_extension/credential_provider_view_controller.h"
#import <Foundation/Foundation.h>
#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/command_line.h"
#import "components/webauthn/core/browser/passkey_model_utils.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/crash_report/crash_helper.h"
#import "ios/chrome/common/credential_provider/archivable_credential_store.h"
#import "ios/chrome/common/credential_provider/constants.h"
#import "ios/chrome/common/credential_provider/credential.h"
#import "ios/chrome/common/credential_provider/multi_store_credential_store.h"
#import "ios/chrome/common/credential_provider/user_defaults_credential_store.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_action_handler.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/credential_provider_extension/account_verification_provider.h"
#import "ios/chrome/credential_provider_extension/metrics_util.h"
#import "ios/chrome/credential_provider_extension/passkey_util.h"
#import "ios/chrome/credential_provider_extension/reauthentication_handler.h"
#import "ios/chrome/credential_provider_extension/ui/consent_coordinator.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_coordinator.h"
#import "ios/chrome/credential_provider_extension/ui/credential_response_handler.h"
#import "ios/chrome/credential_provider_extension/ui/feature_flags.h"
#import "ios/chrome/credential_provider_extension/ui/saving_enterprise_disabled_view_controller.h"
#import "ios/chrome/credential_provider_extension/ui/stale_credentials_view_controller.h"
namespace {
UIColor* BackgroundColor() {
return [UIColor colorNamed:kGroupedPrimaryBackgroundColor];
}
}
@interface CredentialProviderViewController () <ConfirmationAlertActionHandler,
SuccessfulReauthTimeAccessor,
CredentialResponseHandler>
// Interface for the persistent credential store.
@property(nonatomic, strong) id<CredentialStore> credentialStore;
// List coordinator that shows the list of passwords when started.
@property(nonatomic, strong) CredentialListCoordinator* listCoordinator;
// Consent coordinator that shows a view requesting device auth in order to
// enable the extension.
@property(nonatomic, strong) ConsentCoordinator* consentCoordinator;
// Date kept for ReauthenticationModule.
@property(nonatomic, strong) NSDate* lastSuccessfulReauthTime;
// Reauthentication Module used for reauthentication.
@property(nonatomic, strong) ReauthenticationModule* reauthenticationModule;
// Interface for `reauthenticationModule`, handling mostly the case when no
// hardware for authentication is available.
@property(nonatomic, strong) ReauthenticationHandler* reauthenticationHandler;
// Interface for verified that accounts are still valid.
@property(nonatomic, strong) AccountVerificationProvider* accountVerificator;
// Loading indicator used for user validation, which APIs can take a long time.
@property(nonatomic, strong) UIActivityIndicatorView* activityIndicatorView;
// Identfiers cached in `-prepareCredentialListForServiceIdentifiers:` to show
// the next time this view appears.
@property(nonatomic, strong)
NSArray<ASCredentialServiceIdentifier*>* serviceIdentifiers;
@end
@implementation CredentialProviderViewController {
// Information about a passkey credential request.
ASPasskeyCredentialRequestParameters* _requestParameters
API_AVAILABLE(ios(17.0));
}
+ (void)initialize {
if (self == [CredentialProviderViewController self]) {
crash_helper::common::StartCrashpad();
}
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = BackgroundColor();
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// If identifiers were stored in
// `-prepareCredentialListForServiceIdentifiers:`, handle that now.
if (self.serviceIdentifiers) {
NSArray<ASCredentialServiceIdentifier*>* serviceIdentifiers =
self.serviceIdentifiers;
self.serviceIdentifiers = nil;
__weak __typeof__(self) weakSelf = self;
[self validateUserWithCompletion:^(BOOL userIsValid) {
if (!userIsValid) {
[weakSelf showStaleCredentials];
return;
}
[weakSelf reauthenticateIfNeededWithCompletionHandler:^(
ReauthenticationResult result) {
if (result != ReauthenticationResult::kFailure) {
[weakSelf showCredentialListForServiceIdentifiers:serviceIdentifiers];
} else {
[weakSelf exitWithErrorCode:ASExtensionErrorCodeFailed];
}
}];
}];
}
}
#pragma mark - ASCredentialProviderViewController
- (void)prepareCredentialListForServiceIdentifiers:
(NSArray<ASCredentialServiceIdentifier*>*)serviceIdentifiers {
// Sometimes, this method is called while the authentication framework thinks
// the app is not foregrounded, so authentication fails. Instead of directly
// authenticating and showing the credentials, store the list of
// identifiers and authenticate once the extension is visible.
self.serviceIdentifiers = serviceIdentifiers;
}
// Only available in iOS 17.0+.
// The system calls this method when there’s an active passkey request in the
// app or website.
- (void)prepareCredentialListForServiceIdentifiers:
(NSArray<ASCredentialServiceIdentifier*>*)serviceIdentifiers
requestParameters:
(ASPasskeyCredentialRequestParameters*)
requestParameters
API_AVAILABLE(ios(17.0)) {
self.serviceIdentifiers = serviceIdentifiers;
_requestParameters = requestParameters;
}
// Deprecated in iOS 17.0+.
// Replaced with provideCredentialWithoutUserInteractionForRequest.
- (void)provideCredentialWithoutUserInteractionForIdentity:
(ASPasswordCredentialIdentity*)credentialIdentity {
if (@available(iOS 17.0, *)) {
return;
}
[self provideCredentialWithoutUserInteractionForIdentifier:
credentialIdentity.recordIdentifier];
}
// Only available in iOS 17.0+.
- (void)provideCredentialWithoutUserInteractionForRequest:
(id<ASCredentialRequest>)credentialRequest API_AVAILABLE(ios(17.0)) {
[self provideCredentialWithoutUserInteractionForIdentifier:
credentialRequest.credentialIdentity.recordIdentifier];
}
// Deprecated in iOS 17.0+.
// Replaced with prepareInterfaceToProvideCredentialForRequest.
- (void)prepareInterfaceToProvideCredentialForIdentity:
(ASPasswordCredentialIdentity*)credentialIdentity {
if (@available(iOS 17.0, *)) {
return;
}
[self prepareInterfaceToProvideCredentialForIdentifier:credentialIdentity
.recordIdentifier];
}
// Only available in iOS 17.0+.
- (void)prepareInterfaceToProvideCredentialForRequest:
(id<ASCredentialRequest>)credentialRequest API_AVAILABLE(ios(17.0)) {
[self prepareInterfaceToProvideCredentialForIdentifier:credentialRequest
.credentialIdentity
.recordIdentifier];
}
- (void)prepareInterfaceForExtensionConfiguration {
self.consentCoordinator =
[[ConsentCoordinator alloc] initWithBaseViewController:self
credentialResponseHandler:self];
[self.consentCoordinator start];
}
// Only available in iOS 18.0+.
- (void)performPasskeyRegistrationWithoutUserInteractionIfPossible:
(ASPasskeyCredentialRequest*)registrationRequest API_AVAILABLE(ios(18.0)) {
// This function is called to silently create passkeys.
// We're always allowed to return an error until we support this flow.
[self exitWithErrorCode:ASExtensionErrorCodeFailed];
}
- (void)prepareInterfaceForPasskeyRegistration:
(id<ASCredentialRequest>)registrationRequest {
if (![registrationRequest isKindOfClass:[ASPasskeyCredentialRequest class]]) {
[self exitWithErrorCode:ASExtensionErrorCodeFailed];
return;
}
if (!IsPasswordCreationUserEnabled()) {
[self showSavingDisabledByEnterpriseAlert];
return;
}
ASPasskeyCredentialRequest* passkeyCredentialRequest =
base::apple::ObjCCastStrict<ASPasskeyCredentialRequest>(
registrationRequest);
NSArray<NSNumber*>* supportedAlgorithms =
[passkeyCredentialRequest.supportedAlgorithms
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
NSNumber* algorithm,
NSDictionary* bindings) {
return webauthn::passkey_model_utils::IsSupportedAlgorithm(
algorithm.intValue);
}]];
if (supportedAlgorithms.count == 0) {
[self exitWithErrorCode:ASExtensionErrorCodeFailed];
return;
}
// TODO(crbug.com/330355124): Handle
// passkeyCredentialRequest.userVerificationPreference.
ASPasskeyCredentialIdentity* identity =
base::apple::ObjCCastStrict<ASPasskeyCredentialIdentity>(
passkeyCredentialRequest.credentialIdentity);
__weak __typeof(self) weakSelf = self;
FetchKeyCompletionBlock completion = ^(NSData* securityDomainSecret) {
CredentialProviderViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
ASPasskeyRegistrationCredential* passkeyRegistrationCredential =
PerformPasskeyCreation(passkeyCredentialRequest.clientDataHash,
identity.relyingPartyIdentifier,
identity.userName, identity.userHandle,
securityDomainSecret);
if (passkeyRegistrationCredential) {
[strongSelf completeRegistrationRequestWithSelectedPasskeyCredential:
passkeyRegistrationCredential];
} else {
[strongSelf exitWithErrorCode:ASExtensionErrorCodeFailed];
}
};
FetchSecurityDomainSecret(completion);
}
#pragma mark - Properties
- (id<CredentialStore>)credentialStore {
if (!_credentialStore) {
ArchivableCredentialStore* archivableStore =
[[ArchivableCredentialStore alloc]
initWithFileURL:CredentialProviderSharedArchivableStoreURL()];
NSString* key = AppGroupUserDefaultsCredentialProviderNewCredentials();
UserDefaultsCredentialStore* defaultsStore =
[[UserDefaultsCredentialStore alloc]
initWithUserDefaults:app_group::GetGroupUserDefaults()
key:key];
_credentialStore = [[MultiStoreCredentialStore alloc]
initWithStores:@[ defaultsStore, archivableStore ]];
}
return _credentialStore;
}
- (ReauthenticationHandler*)reauthenticationHandler {
if (!_reauthenticationHandler) {
_reauthenticationHandler = [[ReauthenticationHandler alloc]
initWithReauthenticationModule:self.reauthenticationModule];
}
return _reauthenticationHandler;
}
- (ReauthenticationModule*)reauthenticationModule {
if (!_reauthenticationModule) {
_reauthenticationModule = [[ReauthenticationModule alloc]
initWithSuccessfulReauthTimeAccessor:self];
}
return _reauthenticationModule;
}
- (AccountVerificationProvider*)accountVerificator {
if (!_accountVerificator) {
_accountVerificator = [[AccountVerificationProvider alloc] init];
}
return _accountVerificator;
}
#pragma mark - Private
- (void)reauthenticateIfNeededWithCompletionHandler:
(void (^)(ReauthenticationResult))completionHandler {
[self.reauthenticationHandler
verifyUserWithCompletionHandler:completionHandler
presentReminderOnViewController:self];
}
- (void)provideCredentialWithoutUserInteractionForIdentifier:
(NSString*)identifier {
__weak __typeof__(self) weakSelf = self;
[self validateUserWithCompletion:^(BOOL userIsValid) {
// reauthenticationModule can't attempt reauth when no password is set. This
// means a password shouldn't be retrieved.
if (!weakSelf.reauthenticationModule.canAttemptReauth || !userIsValid) {
[weakSelf exitWithErrorCode:ASExtensionErrorCodeUserInteractionRequired];
return;
}
// iOS already gates the password with device auth for
// -provideCredentialWithoutUserInteractionForRequest:. Not using
// reauthenticationModule here to avoid a double authentication request.
[weakSelf provideCredentialForIdentifier:identifier];
}];
}
- (void)prepareInterfaceToProvideCredentialForIdentifier:(NSString*)identifier {
__weak __typeof__(self) weakSelf = self;
[self validateUserWithCompletion:^(BOOL userIsValid) {
if (!userIsValid) {
[weakSelf showStaleCredentials];
return;
}
[weakSelf reauthenticateIfNeededWithCompletionHandler:^(
ReauthenticationResult result) {
if (result != ReauthenticationResult::kFailure) {
[weakSelf provideCredentialForIdentifier:identifier];
} else {
[weakSelf exitWithErrorCode:ASExtensionErrorCodeUserCanceled];
}
}];
}];
}
// Completes the extension request providing `ASPasswordCredential` that matches
// the `identifier` or an error if not found.
- (void)provideCredentialForIdentifier:(NSString*)identifier {
id<Credential> credential =
[self.credentialStore credentialWithRecordIdentifier:identifier];
if (credential) {
UpdateUMACountForKey(app_group::kCredentialExtensionQuickPasswordUseCount);
ASPasswordCredential* passwordCredential =
[ASPasswordCredential credentialWithUser:credential.username
password:credential.password];
[self completeRequestWithSelectedCredential:passwordCredential];
return;
}
[self exitWithErrorCode:ASExtensionErrorCodeCredentialIdentityNotFound];
}
- (void)provideCredentialForRequest:(id<ASCredentialRequest>)credentialRequest
API_AVAILABLE(ios(17.0)) {
NSString* identifier = credentialRequest.credentialIdentity.recordIdentifier;
if (credentialRequest.type == ASCredentialRequestTypePassword) {
[self provideCredentialForIdentifier:identifier];
return;
}
if (credentialRequest.type == ASCredentialRequestTypePasskeyAssertion) {
id<Credential> credential =
[self.credentialStore credentialWithRecordIdentifier:identifier];
if (credential) {
UpdateUMACountForKey(app_group::kCredentialExtensionQuickPasskeyUseCount);
ASPasskeyCredentialRequest* passkeyCredentialRequest =
base::apple::ObjCCastStrict<ASPasskeyCredentialRequest>(
credentialRequest);
// TODO(crbug.com/330355124): Handle
// passkeyCredentialRequest.userVerificationPreference.
__weak __typeof(self) weakSelf = self;
FetchKeyCompletionBlock completion = ^(NSData* securityDomainSecret) {
CredentialProviderViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
ASPasskeyAssertionCredential* passkeyCredential =
PerformPasskeyAssertion(credential,
passkeyCredentialRequest.clientDataHash,
nil, securityDomainSecret);
[strongSelf userSelectedPasskey:passkeyCredential];
};
FetchSecurityDomainSecret(completion);
return;
}
}
[self exitWithErrorCode:ASExtensionErrorCodeCredentialIdentityNotFound];
}
// Shows a loading indicator,
- (void)showLoadingIndicator {
DCHECK(!self.activityIndicatorView);
self.activityIndicatorView = [[UIActivityIndicatorView alloc] init];
UIActivityIndicatorView* activityView = self.activityIndicatorView;
activityView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:activityView];
[NSLayoutConstraint activateConstraints:@[
[activityView.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[activityView.centerYAnchor
constraintEqualToAnchor:self.view.centerYAnchor],
]];
[activityView startAnimating];
activityView.color = [UIColor colorNamed:kBlueColor];
}
// Hides the loading indicator.
- (void)hideLoadingIndicator {
[self.activityIndicatorView removeFromSuperview];
self.activityIndicatorView = nil;
}
// Verifies that the user is still signed in.
// Return NO in the completion when the user is no longer valid. YES otherwise.
- (void)validateUserWithCompletion:(void (^)(BOOL))completion {
[self showLoadingIndicator];
auto handler = ^(BOOL isValid) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingIndicator];
if (completion) {
completion(isValid);
}
});
};
NSString* validationID = [app_group::GetGroupUserDefaults()
stringForKey:AppGroupUserDefaultsCredentialProviderUserID()];
if (validationID) {
[self.accountVerificator
validateValidationID:validationID
completionHandler:^(BOOL isValid, NSError* error) {
handler(!error && isValid);
}];
} else {
handler(YES);
}
}
// Presents the stale credentials view controller.
- (void)showStaleCredentials {
StaleCredentialsViewController* staleCredentialsViewController =
[[StaleCredentialsViewController alloc] init];
staleCredentialsViewController.modalPresentationStyle =
UIModalPresentationOverCurrentContext;
staleCredentialsViewController.actionHandler = self;
[self presentViewController:staleCredentialsViewController
animated:YES
completion:nil];
}
// Starts the credential list feature.
- (void)showCredentialListForServiceIdentifiers:
(NSArray<ASCredentialServiceIdentifier*>*)serviceIdentifiers {
// Views in the password creation flow (FormInputAccessoryView) use
// base::i18n::IsRTL(), which checks some values from the command line.
// Initialize the command line for the process running this extension here
// before that.
if (!base::CommandLine::InitializedForCurrentProcess()) {
base::CommandLine::Init(0, nullptr);
}
self.listCoordinator = [[CredentialListCoordinator alloc]
initWithBaseViewController:self
credentialStore:self.credentialStore
serviceIdentifiers:serviceIdentifiers
reauthenticationHandler:self.reauthenticationHandler
credentialResponseHandler:self];
if (@available(iOS 17.0, *)) {
self.listCoordinator.requestParameters = _requestParameters;
}
[self.listCoordinator start];
UpdateUMACountForKey(app_group::kCredentialExtensionDisplayCount);
}
// Convenience wrapper for
// -completeRequestWithSelectedCredential:completionHandler:.
- (void)completeRequestWithSelectedCredential:
(ASPasswordCredential*)credential {
[self.listCoordinator stop];
self.listCoordinator = nil;
[self.extensionContext completeRequestWithSelectedCredential:credential
completionHandler:nil];
}
// Convenience wrapper for
// -completeAssertionRequestWithSelectedPasskeyCredential:completionHandler:.
- (void)completeAssertionRequestWithSelectedPasskeyCredential:
(ASPasskeyAssertionCredential*)credential API_AVAILABLE(ios(17.0)) {
[self.listCoordinator stop];
self.listCoordinator = nil;
[self.extensionContext
completeAssertionRequestWithSelectedPasskeyCredential:credential
completionHandler:nil];
}
// Convenience wrapper for
// -completeRegistrationRequestWithSelectedPasskeyCredential:completionHandler:.
- (void)completeRegistrationRequestWithSelectedPasskeyCredential:
(ASPasskeyRegistrationCredential*)credential API_AVAILABLE(ios(17.0)) {
[self.listCoordinator stop];
self.listCoordinator = nil;
[self.extensionContext
completeRegistrationRequestWithSelectedPasskeyCredential:credential
completionHandler:nil];
}
// Convenience wrapper for -cancelRequestWithError.
- (void)exitWithErrorCode:(ASExtensionErrorCode)errorCode {
[self.listCoordinator stop];
self.listCoordinator = nil;
NSError* error = [[NSError alloc] initWithDomain:ASExtensionErrorDomain
code:errorCode
userInfo:nil];
[self.extensionContext cancelRequestWithError:error];
}
// Displays sheet with information that credential saving is disabled by the
// enterprise policy.
- (void)showSavingDisabledByEnterpriseAlert {
// TODO(crbug.com/362719658): Check whether it's possible to make the whole
// VC a half sheet.
SavingEnterpriseDisabledViewController*
savingEnterpriseDisabledViewController =
[[SavingEnterpriseDisabledViewController alloc] init];
savingEnterpriseDisabledViewController.actionHandler = self;
[self presentViewController:savingEnterpriseDisabledViewController
animated:YES
completion:nil];
}
#pragma mark - SuccessfulReauthTimeAccessor
- (void)updateSuccessfulReauthTime {
self.lastSuccessfulReauthTime = [[NSDate alloc] init];
UpdateUMACountForKey(app_group::kCredentialExtensionReauthCount);
}
#pragma mark - ConfirmationAlertActionHandler
- (void)confirmationAlertDismissAction {
// Finish the extension. There is no recovery from the stale credentials
// state.
[self exitWithErrorCode:ASExtensionErrorCodeFailed];
}
- (void)confirmationAlertPrimaryAction {
// No-op.
}
#pragma mark - CredentialResponseHandler
- (void)userSelectedPassword:(ASPasswordCredential*)credential {
[self completeRequestWithSelectedCredential:credential];
}
- (void)userSelectedPasskey:(ASPasskeyAssertionCredential*)credential
API_AVAILABLE(ios(17.0)) {
if (credential) {
[self completeAssertionRequestWithSelectedPasskeyCredential:credential];
} else {
[self exitWithErrorCode:ASExtensionErrorCodeCredentialIdentityNotFound];
}
}
- (void)userCancelledRequestWithErrorCode:(ASExtensionErrorCode)errorCode {
[self exitWithErrorCode:errorCode];
}
- (void)completeExtensionConfigurationRequest {
[self.consentCoordinator stop];
self.consentCoordinator = nil;
[self.extensionContext completeExtensionConfigurationRequest];
}
@end