Pull to refresh

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

Reading time 8 min
Views 30K


Вступление


Начиная с 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).
    То есть так и было задумано. Как объяснил пользователь Flanker_4, система таким образом предлагает приложению самостоятельно выполнить альтернативную аутентификацию: ввод пароля приложения. То есть реализация диалога и логики для ввода и проверки пароля лежит на плечах разработчика;
  4. Кнопка для отмены запроса. В результате вызовется блок reply с соответствующей ошибкой LAErrorUserCancel.

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

Если же воспользоваться не тем пальцем и попытаться его отсканировать 5 раз подряд, то система предложит ввести пароль, указанный в настройках смартфона:

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

На этом взаимодействие с 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
Tags:
Hubs:
+19
Comments 21
Comments Comments 21

Articles