Независимо от того, создаете ли вы мобильное приложение или веб-службу, безопасность конфиденциальных данных важна, а безопасность стала неотъемлемой частью каждого программного продукта. В этом руководстве я покажу вам, как безопасно хранить учетные данные пользователя, используя цепочку ключей приложения, и мы рассмотрим шифрование и дешифрование данных пользователя с помощью сторонней библиотеки.
Вступление
В этом руководстве я научу вас, как защитить конфиденциальные данные на платформе iOS. Конфиденциальными данными могут быть учетные данные пользователя или данные кредитной карты. Тип данных не так важен. В этом руководстве мы будем использовать связку ключей iOS и симметричное шифрование для безопасного хранения данных пользователя. Прежде чем мы перейдем к мельчайшим деталям, я хотел бы дать вам обзор того, что мы собираемся сделать в этом уроке.
брелок для iOS
В iOS и OS X цепочка для ключей — это зашифрованный контейнер для хранения паролей и других данных, которые необходимо защитить. На OS X возможно ограничить доступ цепочки для ключей определенным пользователям или приложениям. В iOS, однако, у каждого приложения есть своя цепочка для ключей, к которой имеет доступ только приложение. Это гарантирует, что данные, хранящиеся в цепочке для ключей, являются безопасными и недоступными для третьих лиц.
Имейте в виду, что цепочку для ключей следует использовать только для хранения небольших фрагментов данных, таких как пароли. В этой статье я надеюсь убедить вас в ценности использования цепочки для ключей на iOS и OS X вместо, например, пользовательской базы данных по умолчанию, которая хранит свои данные в виде простого текста без какой-либо защиты.
На iOS приложение может использовать связку ключей через API Связки ключей . API предоставляет ряд функций для управления данными, хранящимися в цепочке для ключей приложения. Посмотрите на функции, доступные на iOS.
-
SecItemAdd
Эта функция используется для добавления элемента вSecItemAdd
для ключей приложения. -
SecItemCopyMatching
Эта функция используется для поиска элемента цепочки для ключей, принадлежащего приложению. -
SecItemDelete
Как следует из названия, эту функцию можно использовать для удаления элемента из цепочки для ключей приложения. -
SecItemUpdate
Используйте эту функцию, если вам нужно обновить элемент вSecItemUpdate
для ключей приложения.
Keychain Services API — это C API, но я надеюсь, что это не помешает вам его использовать. Каждая из вышеперечисленных функций принимает словарь ( CFDictionaryRef
), который содержит пару ключ-значение класса элемента и необязательные пары ключ-значение атрибута . Точное значение и цель каждого из них станут понятны, как только мы начнем использовать API в качестве примера.
Шифрование и дешифрование
Обсуждая шифрование, вы обычно слышите о двух типах шифрования: симметричном и асимметричном шифровании. Симметричное шифрование, с одной стороны, использует один общий ключ для шифрования и дешифрования данных. Асимметричное шифрование, с другой стороны, использует один ключ для шифрования данных, а другой — отдельный, но связанный, ключ для дешифрования данных.
В этом руководстве мы будем использовать платформу безопасности, доступную на iOS, для шифрования и дешифрования данных. Этот процесс происходит под капотом, поэтому мы не будем напрямую взаимодействовать с этой структурой. Мы будем использовать симметричное шифрование в нашем примере приложения.
Платформа безопасности предлагает ряд других служб, таких как службы рандомизации для генерации криптографически безопасных случайных чисел, службы сертификации, ключа и доверия для управления сертификатами, открытыми и закрытыми ключами и политиками доверия. Каркас безопасности — это низкоуровневый каркас, доступный как на iOS, так и на OS X с API на основе C.
Обзор приложения
В этом руководстве я покажу вам, как вы можете использовать API-интерфейс Keychain Services и симметричное шифрование в приложении iOS. Мы создадим небольшое приложение, которое надежно хранит фотографии, сделанные пользователем.
В этом проекте мы будем использовать Sam Soffes SSKeychain , оболочку Objective-C для взаимодействия с API-интерфейсом Keychain Services. Для шифрования и дешифрования мы будем использовать RNCryptor , стороннюю библиотеку шифрования.
Шифрование данных с помощью RNCryptor
Библиотека RNCryptor — хороший выбор для шифрования и дешифрования данных. Проект используется многими разработчиками и активно поддерживается его создателями. Библиотека предлагает простой в использовании Objective-C API. Если вы знакомы с Cocoa и Objective-C, вам будет легко пользоваться. Основные функции библиотеки перечислены ниже.
- Шифрование AES-256
- Режим CBC
- Растяжение пароля с помощью PBKDF2
- Сольта пароля
- Случайный IV
- HMAC шифрования, затем хэша
Поток приложений
Прежде чем мы начнем создавать приложение, позвольте мне показать вам, как будет выглядеть типичный поток приложения.
- Когда пользователь запускает приложение, ему предоставляется возможность войти в систему.
- Если она еще не создала учетную запись, ее учетные данные добавляются в цепочку для ключей, и она выполняет вход.
- Если она имеет учетную запись, но вводит неверный пароль, отображается сообщение об ошибке.
- После входа в систему у нее появляется доступ к фотографиям, которые она сделала с помощью приложения. Фотографии надежно хранятся приложением.
- Всякий раз, когда она делает фотографию с помощью камеры устройства или выбирает фотографию из своей библиотеки фотографий, фотография шифруется и сохраняется в каталоге документов приложения.
- Всякий раз, когда она переключается на другое приложение или устройство блокируется, она автоматически выходит из системы.
Сборка приложения
Шаг 1: Настройка проекта
Запустите Xcode и создайте новый проект, выбрав шаблон приложения Single View Application из списка шаблонов.
Назовите проект Secure Photos и установите для семейства устройств iPhone. Сообщите Xcode, где вы хотите сохранить проект, и нажмите « Создать» .
Шаг 2: Рамки
Следующий шаг — связать проект с платформами Security и Mobile Core Services . Выберите проект в Навигаторе проектов слева, выберите первую цель с именем « Безопасные фотографии» и откройте вкладку « Фазы сборки » вверху. Разверните секцию « Связать двоичные файлы с библиотеками» и свяжите проект со средами Security и Mobile Core Services .
Шаг 3: Зависимости
Как я упоминал ранее, мы будем использовать библиотеку SSKeychain и библиотеку RNCryptor . Загрузите эти зависимости и добавьте их в проект. Обязательно скопируйте файлы в свой проект и добавьте их к цели « Безопасные фотографии», как показано на снимке экрана ниже.
Шаг 4: Создание классов
Мы будем отображать фотографии пользователя в виде коллекции, что означает, что нам нужно UICollectionViewController
подкласс UICollectionViewController
а также UICollectionViewCell
. Выберите New> File … в меню File , создайте подкласс UICollectionViewController
и назовите его MTPhotosViewController
. Повторите этот шаг еще раз для MTPhotoCollectionViewCell
, который является подклассом UICollectionViewCell
.
Шаг 5: Создание пользовательского интерфейса
Откройте основную раскадровку проекта и обновите раскадровку, как показано на скриншоте ниже. Раскадровка содержит два контроллера представления, экземпляр MTViewController
, который содержит два текстовых поля и кнопку, и экземпляр MTPhotosViewController
. Экземпляр MTViewController
встроен в контроллер навигации.
Нам также необходимо создать переход от экземпляра MTPhotosViewController
экземпляру MTPhotosViewController
. Установите идентификатор перехода в photosViewController
. Экземпляр MTPhotosViewController
также должен содержать элемент панели кнопок, как показано на MTPhotosViewController
экрана ниже.
Чтобы все это работало, нам нужно обновить интерфейс MTViewController
как показано ниже. Мы объявляем выход для каждого текстового поля и действие, которое вызывается кнопкой. Сделайте необходимые подключения в основной раскадровке проекта.
01
02
03
04
05
06
07
08
09
10
|
#import <UIKit/UIKit.h>
@interface MTViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextField *usernameTextField;
@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;
— (IBAction)login:(id)sender;
@end
|
В классе MTPhotosViewController
объявите свойство с именем username
для хранения имени username
MTPhotosViewController
в данный момент в систему, и объявите действие для элемента панели кнопок. Не забудьте связать действие с элементом кнопки панели в основной раскадровке.
1
2
3
4
5
6
7
8
9
|
#import <UIKit/UIKit.h>
@interface MTPhotosViewController : UICollectionViewController
@property (copy, nonatomic) NSString *username;
— (IBAction)photos:(id)sender;
@end
|
Шаг 6: Реализация MTViewController
В MTViewController.m
добавьте оператор импорта для класса SSKeychain
класса SSKeychain
и класса MTAppDelegate
. Мы также соответствуем классу MTViewController
протоколу UIAlertViewDelegate
.
1
2
3
4
5
6
7
8
9
|
#import «MTViewController.h»
#import «SSKeychain.h»
#import «MTAppDelegate.h»
#import «MTPhotosViewController.h»
@interface MTViewController () <UIAlertViewDelegate>
@end
|
Следующим шагом является реализация login:
действие, которое мы объявили ранее. Сначала мы проверяем, создал ли пользователь учетную запись, выбирая пароль для нее. Если это так, мы используем цепочку для ключей приложения, чтобы увидеть, совпадает ли пароль, введенный пользователем, с паролем, который хранится в цепочке для ключей. Методы, предоставляемые библиотекой SSKeychain, позволяют легко читать и манипулировать данными, хранящимися в цепочке для ключей приложения.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
— (IBAction)login:(id)sender {
if (self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0) {
NSString *password = [SSKeychain passwordForService:@»MyPhotos» account:self.usernameTextField.text];
if (password.length > 0) {
if ([self.passwordTextField.text isEqualToString:password]) {
[self performSegueWithIdentifier:@»photosViewController» sender:nil];
} else {
UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@»Error Login» message:@»Invalid username/password combination.»
[alertView show];
}
} else {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@»New Account» message:@»Do you want to create an account?»
[alertView show];
}
} else {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@»Error Input» message:@»Username and/or password cannot be empty.»
[alertView show];
}
}
|
Мы установили контроллер представления в качестве делегата представления оповещения, что означает, что нам нужно реализовать протокол UIAlertViewDelegate
. Посмотрите на реализацию alertView:clickedButtonAtIndex:
показано ниже.
01
02
03
04
05
06
07
08
09
10
11
|
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
switch (buttonIndex) {
case 0:
break;
case 1:
[self createAccount];
break;
default:
break;
}
}
|
В createAccount
мы используем класс SSKeychain
для безопасного хранения имени пользователя и пароля, выбранных пользователем. Затем мы вызываем performSegueWithIdentifier:sender:
1
2
3
4
5
6
7
|
— (void)createAccount {
BOOL result = [SSKeychain setPassword:self.passwordTextField.text forService:@»MyPhotos» account:self.usernameTextField.text];
if (result) {
[self performSegueWithIdentifier:@»photosViewController» sender:nil];
}
}
|
В prepareForSegue:sender:
мы получаем ссылку на экземпляр MTPhotosViewController
, устанавливаем его свойство username
со значением usernameTextField
и сбрасываем passwordTextField
.
1
2
3
4
5
|
— (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
MTPhotosViewController *photosViewController = segue.destinationViewController;
photosViewController.username = self.usernameTextField.text;
self.passwordTextField.text = nil;
}
|
Шаг 7: Реализация MTPhotosCollectionViewCell
Откройте MTPhotosCollectionViewCell.h и объявите выход с именем imageView
типа UIImageView
.
1
2
3
4
5
6
7
|
#import <UIKit/UIKit.h>
@interface MTPhotoCollectionViewCell : UICollectionViewCell
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
|
Откройте основную раскадровку и добавьте экземпляр UIImageView
в ячейку MTPhotosViewController
экземпляра MTPhotosViewController
. Выберите ячейку прототипа (не представление изображения) и установите для ее класса значение MTPhotosCollectionViewCell
в Инспекторе удостоверений справа. С выбранной ячейкой прототипа откройте инспектор атрибутов и установите для идентификатора PhotoCell
.
Шаг 8: Реализация MTPhotosViewController
Начните с импорта необходимых заголовочных файлов в MTPhotosViewController.m, как показано ниже. Нам также нужно объявить два свойства: photos
для хранения массива фотографий, которые будет отображать представление коллекции, и filePath
для сохранения ссылки на путь к файлу. Возможно, вы заметили, что класс MTPhotosViewController
соответствует протоколам UIImagePickerControllerDelegate
, UINavigationControllerDelegate
и UIImagePickerControllerDelegate
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
#import «MTPhotosViewController.h»
#import <MobileCoreServices/MobileCoreServices.h>
#import «RNDecryptor.h»
#import «RNEncryptor.h»
#import «MTPhotoCollectionViewCell.h»
@interface MTPhotosViewController () <UIActionSheetDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
@property (strong, nonatomic) NSMutableArray *photos;
@property (copy, nonatomic) NSString *filePath;
@end
|
Я также реализовал удобный или вспомогательный метод setupUserDirectory
для создания и настройки необходимых каталогов, в которых мы будем хранить данные пользователя. В prepareData
приложение расшифровывает изображения, которые хранятся в защищенном каталоге пользователя. Посмотрите на их реализации ниже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
— (void)setupUserDirectory {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documents = [paths objectAtIndex:0];
self.filePath = [documents stringByAppendingPathComponent:self.username];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:self.filePath]) {
NSLog(@»Directory already present.»);
} else {
NSError *error = nil;
[fileManager createDirectoryAtPath:self.filePath withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@»Unable to create directory for user.»);
}
}
}
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
— (void)prepareData {
self.photos = [[NSMutableArray alloc] init];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *contents = [fileManager contentsOfDirectoryAtPath:self.filePath error:&error];
if ([contents count] && !error){
NSLog(@»Contents of the user’s directory. %@», contents);
for (NSString *fileName in contents) {
if ([fileName rangeOfString:@».securedData»].length > 0) {
NSData *data = [NSData dataWithContentsOfFile:[self.filePath stringByAppendingPathComponent:fileName]];
NSData *decryptedData = [RNDecryptor decryptData:data withSettings:kRNCryptorAES256Settings password:@»A_SECRET_PASSWORD» error:nil];
UIImage *image = [UIImage imageWithData:decryptedData];
[self.photos addObject:image];
} else {
NSLog(@»This file is not secured.»);
}
}
} else if (![contents count]) {
if (error) {
NSLog(@»Unable to read the contents of the user’s directory.»);
} else {
NSLog(@»The user’s directory is empty.»);
}
}
}
|
Вызовите оба метода в методе viewDidLoad
контроллера представления, как показано ниже.
1
2
3
4
5
6
|
— (void)viewDidLoad {
[super viewDidLoad];
[self setupUserDirectory];
[self prepareData];
}
|
Элемент панели кнопок на панели навигации контроллера отображает лист действий, позволяющий пользователю выбирать между камерой устройства и библиотекой фотографий.
1
2
3
4
|
— (IBAction)photos:(id)sender {
UIActionSheet *actionSheet = [[UIActionSheet alloc]initWithTitle:@»Select Source» delegate:self cancelButtonTitle:@»Cancel» destructiveButtonTitle:nil otherButtonTitles:@»Camera», @»Photo Library», nil];
[actionSheet showFromBarButtonItem:sender animated:YES];
}
|
Давайте реализуем actionSheet:clickedButtonAtIndex:
протокола actionSheet:clickedButtonAtIndex:
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
— (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{
if (buttonIndex < 2) {
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc] init];
imagePickerController.mediaTypes = @[(__bridge NSString *)kUTTypeImage];
imagePickerController.allowsEditing = YES;
imagePickerController.delegate = self;
if (buttonIndex == 0) {
#if TARGET_IPHONE_SIMULATOR
#else
imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
#endif
} else if ( buttonIndex == 1) {
imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
}
[self.navigationController presentViewController:imagePickerController animated:YES completion:nil];
}
}
|
Чтобы обработать выбор пользователя в контроллере средства выбора изображений, нам нужно реализовать imagePickerController:didFinishPickingMediaWithInfo:
протокола UIImagePickerControllerDelegate
, как показано ниже. Изображение зашифровано с использованием RNEncryptor
библиотеки RNEncryptor
. Изображение также добавляется в массив photos
а представление коллекции перезагружается.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
— (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
UIImage *image = [info objectForKey: UIImagePickerControllerEditedImage];
if (!image) {
[info objectForKey: UIImagePickerControllerOriginalImage];
}
NSData *imageData = UIImagePNGRepresentation(image);
NSString *imageName = [NSString stringWithFormat:@»image-%d.securedData», self.photos.count + 1];
NSData *encryptedImage = [RNEncryptor encryptData:imageData withSettings:kRNCryptorAES256Settings password:@»A_SECRET_PASSWORD» error:nil];
[encryptedImage writeToFile:[self.filePath stringByAppendingPathComponent:imageName] atomically:YES];
[self.photos addObject:image];
[self.collectionView reloadData];
[picker dismissViewControllerAnimated:YES completion:nil];
}
|
Прежде чем вы сможете собрать и запустить приложение, нам нужно реализовать протокол UICollectionViewDataSource
, как показано ниже.
1
2
3
|
— (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.photos ?
}
|
1
2
3
4
5
|
— (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MTPhotoCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@»PhotoCell» forIndexPath:indexPath];
cell.imageView.image = [self.photos objectAtIndex:indexPath.row];
return cell;
}
|
Шаг 9: Обработка состояний приложений
Если приложение переходит в фоновый режим, пользователь должен выйти из системы. Это важно с точки зрения безопасности. Для этого делегат приложения должен иметь ссылку на контроллер навигации, чтобы он мог получить доступ к корневому контроллеру представления стека навигации. Начните с объявления свойства с именем navigationController
в MTAppDelegate.h .
1
2
3
4
5
6
7
8
|
#import <UIKit/UIKit.h>
@interface MTAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) UINavigationController *navigationController;
@end
|
В методе viewDidLoad
контроллера viewDidLoad
мы устанавливаем свойство navigationController
делегата приложения, как показано ниже. Имейте в виду, что это только один из способов справиться с этим.
Я установил вышеуказанное свойство в ViewController's
viewDidLoad
как показано ниже.
1
2
3
4
5
6
|
— (void)viewDidLoad {
[super viewDidLoad];
MTAppDelegate *applicationDeleagte = (MTAppDelegate *)[[UIApplication sharedApplication] delegate];
[applicationDeleagte setNavigationController:self.navigationController];
}
|
В делегате приложения нам нужно обновить applicationWillResignActive:
как показано ниже. Это так просто. В результате пользователь выходит из системы всякий раз, когда приложение теряет фокус. Это защитит изображения пользователя, хранящиеся в приложении, от посторонних глаз. Недостатком является то, что пользователь должен войти в систему, когда приложение снова станет активным.
1
2
3
|
— (void)applicationWillResignActive:(UIApplication *)application {
[self.navigationController popToRootViewControllerAnimated:NO];
}
|
Шаг 10: Сборка и запуск
Создайте проект и запустите приложение, чтобы испытать его.
Вывод
В этом руководстве вы узнали, как использовать API-интерфейс Keychain Services для хранения конфиденциальных данных, а также как шифровать данные изображений на iOS. Оставьте комментарий в комментариях ниже, если у вас есть какие-либо вопросы или отзывы.