Вступление
Начиная с 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
. Можно было бы оставить как есть, но большинство разработчиков предполагают, что блок, который передается в метод, вызванный на главном потоке, также будет вызван на главном потоке, и не ставят дополнительную проверку. Так уж сложилось исторически.
При вызове выше описанного метода система отобразит диалог:
- В заголовке используется название приложения (
CFBundleDisplayName
); - Строка, указанная в качестве параметра
localizedReason
; - С этим полем не все так просто. При его нажатии диалог для ввода пароля не появится, как вы могли подумать, а вместо этого вызовется блок
reply
с ошибкой. Код ошибки задокументирован:
LAErrorUserFallback
Authentication was canceled because the user tapped the fallback button (Enter Password).То есть так и было задумано. Честно говоря, эту логику я так и не понял;
- Кнопка для отмены запроса. В результате вызовется блок
reply
с соответствующей ошибкойLAErrorUserCancel
.
Если сканирование прошло успешно, то вызовется блок reply
с положительным результатом.
Необходимо отметить, что диалог для сканирования отображается не при каждом вызове метода evaluatePolicy:localizedReason:reply:
. То есть успешность последнего сканирования обладает некоторым временем жизни. Повторная попытка аутентификации в течение нескольких минут приведет к мгновенному вызову блока reply
с положительным результатом.
Если же воспользоваться не тем пальцем и попытаться его отсканировать 5 раз подряд, то система предложит ввести пароль, указанный в настройках смартфона:
Для ясности уточню, что невозможно включить сканер в настройках смартфона, при этом не создав пароль.
После того, как пользователь введет верный пароль, ему снова будет предложено сканирование отпечатка пальца. То есть знать пароль недостаточно.
На этом взаимодействие с 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