Встраиваем Touch ID в iOS приложение

в 4:18, , рубрики: iOS, mobile development, Touch ID, разработка под iOS, метки: ,

Встраиваем Touch ID в iOS приложение

Вступление

Начиная с iOS 8 Apple открывает доступ к возможности использования технологии Touch ID (аутентификации с помощью сканера отпечатков пальцев, встроенного в iPhone 5s) в сторонних приложениях. В связи с этим я хотел бы поделиться с вами подробной информацией о том, что же именно стало доступно разработчикам, как это встроить в свое приложение, каким поведением это обладает, а также поделиться удобной «оберткой», которая реализует наиболее, на мой взгляд, вероятный сценарий использования Touch ID.

Необходимый API представлен в новом фреймворке LocalAuthentication. На данный момент его функционал ограничивается взаимодействием со сканером отпечатков пальцев, но судя по более общему названию его набор возможностей, вероятно, в будущем расширится. Фреймворк не предоставляет никаких данных о пользователе (что в общем-то логично), а только позволяет предложить пользователю выполнить аутентификацию с помощью средств биометрии (на данный момент это встроенный сканер отпечатков пальцев; но конкретно о сканере во фреймворке речи не идет, используется более общее слово Biometrics). На выходе мы получаем статус: либо аутентификация прошла успешно, либо что-то пошло не так. По сути, почти в любой момент времени можно определить действительно ли тот, кто пользуется устройством, является его владельцем.

Это наводит на мысль об использовании Touch ID в качестве дополнительной защиты при выполнении каких-либо важных операций. Например, при подтверждении перевода денежных средств, изменении каких-либо важных настроек, инициализации защищенного чата и т.д., то есть там, где приложение должно быть максимально уверено, что смартфон не оказался в руках злоумышленника.

Для того, чтобы пост был не только читабельным, но и реюзабельным, я решил описать интеграцию с Touch ID в виде «обертки», которая реализует выше описанный сценарий, что в будущем может вам сэкономить несколько часов рабочего времени. Описание представлено в виде «задача-решение», чтобы было ясно, что делается и для чего. И так, приступим.

Задача

При выполнении важных операций в приложении необходимо иметь возможность запрашивать аутентификацию пользователя с помощью встроенных средств биометрии. Необходимость запроса такой аутентификации должна быть настраиваемой самим пользователем. Также нужно учитывать, что приложение может работать на более ранних версиях операцинной системы и на устройствах, в которых отсутствуют средства биометрии.

Решение

Решение будет представлено в классе BiometricAuthenticationFacade.
Прежде всего рассмотрим самое главное — взаимодействие с фреймворком LocalAuthentication. Эта часть скрыта от пользователя и не доступна из интерфейса класса.

В расширении класса объявим свойство для хранения контекста:

@interface BiometricAuthenticationFacade ()

@property (nonatomic, strong) LAContext *authenticationContext;

@end

Выполним инициализацию свойства с учетом доступности API:

- (instancetype)init {
    self = [super init];
    if (self) {
        if (self.isIOS8AndLater) {
            self.authenticationContext = [[LAContext alloc] init];
        }
    }
    return self;
}

Далее определим метод, который будет возвращать доступность использования локальной аутентификации:

- (BOOL)isPassByBiometricsAvailable {
    return [self.authenticationContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
                                                   error:NULL];
}

В качестве параметра метод canEvaluatePolicy:error: принимает тип локальной аутентификации. На данный момент объявлен только один тип LAPolicyDeviceOwnerAuthenticationWithBiometrics, который говорит сам за себя. Использование биометрии может быть недоступно в случае, если устройство физически не поддерживает такую возможность либо, если пользователь не включил эту возможность в настройках смартфона.

Запрос на выполнение сканирования отпечатка пальца пользователя опишем следующим образом:

- (void)passByBiometricsWithReason:(NSString *)reason
                       succesBlock:(void(^)())successBlock
                      failureBlock:(void(^)(NSError *error))failureBlock {
    [self.authenticationContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:reason reply:^(BOOL success, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (success) {
                successBlock();
            } else {
                failureBlock(error);
            }
        });
    }];
}

В качестве параметров метод evaluatePolicy:localizedReason:reply: принимает выше описанный тип локальной аутентификации, сообщение, которое должно кратко описывать причину запроса и блок, который асинхронно выполнится после завершения всей процедуры.

Обратите внимание, что выполнение блока reply на главном потоке не гарантировано (по факту вызывается не на главном), поэтому добавлен вызов dispatch_async. Можно было бы оставить как есть, но большинство разработчиков предполагают, что блок, который передается в метод, вызванный на главном потоке, также будет вызван на главном потоке, и не ставят дополнительную проверку. Так уж сложилось исторически.

При вызове выше описанного метода система отобразит диалог:
Встраиваем Touch ID в iOS приложение

  1. В заголовке используется название приложения (CFBundleDisplayName);
  2. Строка, указанная в качестве параметра localizedReason;
  3. С этим полем не все так просто. При его нажатии диалог для ввода пароля не появится, как вы могли подумать, а вместо этого вызовется блок reply с ошибкой. Код ошибки задокументирован:

    LAErrorUserFallback
    Authentication was canceled because the user tapped the fallback button (Enter Password).

    То есть так и было задумано. Честно говоря, эту логику я так и не понял;

  4. Кнопка для отмены запроса. В результате вызовется блок reply с соответствующей ошибкой LAErrorUserCancel.

Если сканирование прошло успешно, то вызовется блок reply с положительным результатом.
Необходимо отметить, что диалог для сканирования отображается не при каждом вызове метода evaluatePolicy:localizedReason:reply:. То есть успешность последнего сканирования обладает некоторым временем жизни. Повторная попытка аутентификации в течение нескольких минут приведет к мгновенному вызову блока reply с положительным результатом.

Если же воспользоваться не тем пальцем и попытаться его отсканировать 5 раз подряд, то система предложит ввести пароль, указанный в настройках смартфона:
Встраиваем Touch ID в iOS приложение
Для ясности уточню, что невозможно включить сканер в настройках смартфона, при этом не создав пароль.
После того, как пользователь введет верный пароль, ему снова будет предложено сканирование отпечатка пальца. То есть знать пароль недостаточно.

На этом взаимодействие с LocalAuthentication завершено.
Перейдем к реализации интерфейса нашего фасада.

Метод, позволяющий узнать доступность аутентификации. Результат определяется доступностью API и сканера:

- (BOOL)isAuthenticationAvailable {
    return self.isIOS8AndLater && self.isPassByBiometricsAvailable;
}

Метод, позволяющий определить включена ли аутентификация для той или иной операции:

- (BOOL)isAuthenticationEnabledForFeature:(NSString *)featureName {
    return self.isAuthenticationAvailable && [self loadIsAuthenticationEnabledForFeature:featureName];
}

Примером операции может быть доступ к настройкам, выполнение денежной транзакции и т.д.
Состояние включения хранится в NSUserDefaults. Ниже будет представлена реализация метода loadIsAuthenticationEnabledForFeature:.

Метод включения аутентификации для определенной операции:

- (void)enableAuthenticationForFeature:(NSString *)featureName
                           succesBlock:(void(^)())successBlock
                          failureBlock:(void(^)(NSError *error))failureBlock {
    if (self.isAuthenticationAvailable) {
        if ([self isAuthenticationEnabledForFeature:featureName]) {
            successBlock();
        } else {
            [self saveIsAuthenticationEnabled:YES forFeature:featureName];
            successBlock();
        }
    } else {
        failureBlock(self.authenticationUnavailabilityError);
    }
}

Метод необходим для того, чтобы пользователь приложения имел возможность самостоятельно определять операции, для которых необходима дополнительная проверка.
Состояние включения сохраняется в NSUserDefaults. Ниже будет представлена реализация метода saveIsAuthenticationEnabled:forFeature

Метод выключения аутентификации для определенной операции:

- (void)disableAuthenticationForFeature:(NSString *)featureName
                             withReason:(NSString *)reason
                            succesBlock:(void(^)())successBlock
                           failureBlock:(void(^)(NSError *error))failureBlock {
    if (self.isAuthenticationAvailable) {
        if ([self isAuthenticationEnabledForFeature:featureName]) {
            [self passByBiometricsWithReason:reason succesBlock:^{
                [self saveIsAuthenticationEnabled:NO forFeature:featureName];
                successBlock();
            } failureBlock:failureBlock];
        } else {
            successBlock();
        }
    } else {
        failureBlock(self.authenticationUnavailabilityError);
    }
}

Как видите, для выключения необходимо убедиться, что мы имеем дело с владельцем смартфона, а не злоумышленником.

Метод запроса аутентификации пользователя для доступа к операции:

- (void)authenticateForAccessToFeature:(NSString *)featureName
                            withReason:(NSString *)reason
                           succesBlock:(void(^)())successBlock
                          failureBlock:(void(^)(NSError *error))failureBlock {
    if (self.isAuthenticationAvailable) {
        if ([self isAuthenticationEnabledForFeature:featureName]) {
            [self passByBiometricsWithReason:reason
                              succesBlock:successBlock
                             failureBlock:failureBlock];
        } else {
            successBlock();
        }
    } else {
        failureBlock(self.authenticationUnavailabilityError);
    }
}

Методы для сохранения и получения информации о необходимости аутентификации пользователя для доступа к операции (не доступны из интерфейса класса):

- (void)saveIsAuthenticationEnabled:(BOOL)isAuthenticationEnabled forFeature:(NSString *)featureName {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    
    NSMutableDictionary *featuresDictionary = nil;
    NSDictionary *currentDictionary = [userDefaults valueForKey:kFeaturesDictionaryKey];
    if (currentDictionary == nil) {
        featuresDictionary = [NSMutableDictionary dictionary];
    } else {
        featuresDictionary = [NSMutableDictionary dictionaryWithDictionary:currentDictionary];
    }
    
    [featuresDictionary setValue:@(isAuthenticationEnabled) forKey:featureName];
    [userDefaults setValue:featuresDictionary forKey:kFeaturesDictionaryKey];
    [userDefaults synchronize];
}

- (BOOL)loadIsAuthenticationEnabledForFeature:(NSString *)featureName {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *featuresDictionary = [userDefaults valueForKey:kFeaturesDictionaryKey];
    return [[featuresDictionary valueForKey:featureName] boolValue];
}

В качестве хранилища используется NSUserDefaults. Все данные хранятся в отдельном словаре, чтобы снизить вероятность появления конфликтов в названиях ключей.
На этом основная реализация фасада завершается.

Концовка

И напоследок, для тех, кто осилил дочитать до конца, несколько интересных фактов о сканере в iPhone 5s:

  • Вероятность ложного пропуска, т.е. того, что отпечаток случайного человека будет распознан как Ваш, равна 1 на 50 000;
  • Система позволяет выполнить 5 попыток сканирования перед тем, как будет затребован пароль пользователя. Таким образом атака типа brute-force не может быть выполнена, а вероятность того, что сканер может быть взломан злоумышленником равна ≈0.0001;
  • Сканер снимает растровое изображение размером в 88x88 пикселей и плотностью 500 ppi. Полученное растровое изображение преобразуется в векторное и подвергается дополнительному анализу;
  • Полученные данные отпечатка хранятся в зашифрованном виде в специальной области (Secure Enclave) на процессоре A7. Данные шифруются приватным ключом, который генерируется и записывается в Secure Enclave во время производства процессора на фабрике. Apple утверждает, что ни зашифрованные даные, ни приватный ключ не покидают мобильное устройство и неизвестны третьим лицам, в том числе и самой компании Apple.

Источник интересных фактов: iOS Security
Полная версия исходного кода доступна на GitHub: BiometricAuthenticationFacade

Автор: visput

Источник

* - обязательные к заполнению поля