blob: 1f32d1f27fb04297a1c0498b2b5713be5e3fdf01 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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>
#include "base/check.h"
#include "ios/chrome/common/app_group/app_group_constants.h"
#include "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/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/password_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/stale_credentials_view_controller.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface CredentialProviderViewController () <ConfirmationAlertActionHandler,
SuccessfulReauthTimeAccessor>
// 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;
@end
@implementation CredentialProviderViewController
+ (void)initialize {
if (self == [CredentialProviderViewController self]) {
if (crash_helper::common::CanCrashpadStart()) {
crash_helper::common::StartCrashpad();
}
}
}
#pragma mark - ASCredentialProviderViewController
- (void)prepareCredentialListForServiceIdentifiers:
(NSArray<ASCredentialServiceIdentifier*>*)serviceIdentifiers {
__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];
}
}];
}];
}
- (void)provideCredentialWithoutUserInteractionForIdentity:
(ASPasswordCredentialIdentity*)credentialIdentity {
__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
// -provideCredentialWithoutUserInteractionForIdentity:. Not using
// reauthenticationModule here to avoid a double authentication request.
[weakSelf provideCredentialForIdentity:credentialIdentity];
}];
}
- (void)prepareInterfaceToProvideCredentialForIdentity:
(ASPasswordCredentialIdentity*)credentialIdentity {
__weak __typeof__(self) weakSelf = self;
[self validateUserWithCompletion:^(BOOL userIsValid) {
if (!userIsValid) {
[weakSelf showStaleCredentials];
return;
}
[weakSelf reauthenticateIfNeededWithCompletionHandler:^(
ReauthenticationResult result) {
if (result != ReauthenticationResult::kFailure) {
[weakSelf provideCredentialForIdentity:credentialIdentity];
} else {
[weakSelf exitWithErrorCode:ASExtensionErrorCodeUserCanceled];
}
}];
}];
}
- (void)prepareInterfaceForExtensionConfiguration {
// Reset the consent if the extension was disabled and reenabled.
NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults];
[user_defaults
removeObjectForKey:kUserDefaultsCredentialProviderConsentVerified];
self.consentCoordinator = [[ConsentCoordinator alloc]
initWithBaseViewController:self
context:self.extensionContext
reauthenticationHandler:self.reauthenticationHandler
isInitialConfigurationRequest:YES];
[self.consentCoordinator start];
}
#pragma mark - Properties
- (id<CredentialStore>)credentialStore {
if (!_credentialStore) {
_credentialStore = [[ArchivableCredentialStore alloc]
initWithFileURL:CredentialProviderSharedArchivableStoreURL()];
}
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];
}
// Completes the extension request providing |ASPasswordCredential| that matches
// the |credentialIdentity| or an error if not found.
- (void)provideCredentialForIdentity:
(ASPasswordCredentialIdentity*)credentialIdentity {
NSString* identifier = credentialIdentity.recordIdentifier;
id<Credential> credential =
[self.credentialStore credentialWithRecordIdentifier:identifier];
if (credential) {
NSString* password =
PasswordWithKeychainIdentifier(credential.keychainIdentifier);
if (password) {
UpdateUMACountForKey(
app_group::kCredentialExtensionQuickPasswordUseCount);
ASPasswordCredential* ASCredential =
[ASPasswordCredential credentialWithUser:credential.user
password:password];
[self.extensionContext completeRequestWithSelectedCredential:ASCredential
completionHandler:nil];
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:AppGroupUserDefaultsCredentialProviderManagedUserID()];
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 {
self.listCoordinator = [[CredentialListCoordinator alloc]
initWithBaseViewController:self
credentialStore:self.credentialStore
context:self.extensionContext
serviceIdentifiers:serviceIdentifiers
reauthenticationHandler:self.reauthenticationHandler];
[self.listCoordinator start];
UpdateUMACountForKey(app_group::kCredentialExtensionDisplayCount);
}
// Convenience wrapper for -cancelRequestWithError.
- (void)exitWithErrorCode:(ASExtensionErrorCode)errorCode {
NSError* error = [[NSError alloc] initWithDomain:ASExtensionErrorDomain
code:errorCode
userInfo:nil];
[self.extensionContext cancelRequestWithError:error];
}
#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.
}
- (void)confirmationAlertSecondaryAction {
// No-op.
}
- (void)confirmationAlertLearnMoreAction {
// No-op.
}
@end