...

пятница, 5 сентября 2014 г.

[Из песочницы] Встраиваем 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. Можно было бы оставить как есть, но большинство разработчиков предполагают, что блок, который передается в метод, вызванный на главном потоке, также будет вызван на главном потоке, и не ставят дополнительную проверку. Так уж сложилось исторически.


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




  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 раз подряд, то система предложит ввести пароль, указанный в настройках смартфона:



Для ясности уточню, что невозможно включить сканер в настройках смартфона, при этом не создав пароль.

После того, как пользователь введет верный пароль, ему снова будет предложено сканирование отпечатка пальца. То есть знать пароль недостаточно.


На этом взаимодействие с 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


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий