Статьи

Защита и шифрование данных на iOS

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


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

Несмотря на то, что это руководство посвящено iOS, концепции и методы также можно использовать в OS X.

В 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 — хороший выбор для шифрования и дешифрования данных. Проект используется многими разработчиками и активно поддерживается его создателями. Библиотека предлагает простой в использовании Objective-C API. Если вы знакомы с Cocoa и Objective-C, вам будет легко пользоваться. Основные функции библиотеки перечислены ниже.

  • Шифрование AES-256
  • Режим CBC
  • Растяжение пароля с помощью PBKDF2
  • Сольта пароля
  • Случайный IV
  • HMAC шифрования, затем хэша

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

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

Запустите Xcode и создайте новый проект, выбрав шаблон приложения Single View Application из списка шаблонов.


Назовите проект Secure Photos и установите для семейства устройств iPhone. Сообщите Xcode, где вы хотите сохранить проект, и нажмите « Создать» .


Следующий шаг — связать проект с платформами Security и Mobile Core Services . Выберите проект в Навигаторе проектов слева, выберите первую цель с именем « Безопасные фотографии» и откройте вкладку « Фазы сборки » вверху. Разверните секцию « Связать двоичные файлы с библиотеками» и свяжите проект со средами Security и Mobile Core Services .


Как я упоминал ранее, мы будем использовать библиотеку SSKeychain и библиотеку RNCryptor . Загрузите эти зависимости и добавьте их в проект. Обязательно скопируйте файлы в свой проект и добавьте их к цели « Безопасные фотографии», как показано на снимке экрана ниже.


Мы будем отображать фотографии пользователя в виде коллекции, что означает, что нам нужно UICollectionViewController подкласс UICollectionViewController а также UICollectionViewCell . Выберите New> File … в меню File , создайте подкласс UICollectionViewController и назовите его MTPhotosViewController . Повторите этот шаг еще раз для MTPhotoCollectionViewCell , который является подклассом UICollectionViewCell .

Откройте основную раскадровку проекта и обновите раскадровку, как показано на скриншоте ниже. Раскадровка содержит два контроллера представления, экземпляр 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

В 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;
}

Откройте 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 .

Начните с импорта необходимых заголовочных файлов в 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;
}

Если приложение переходит в фоновый режим, пользователь должен выйти из системы. Это важно с точки зрения безопасности. Для этого делегат приложения должен иметь ссылку на контроллер навигации, чтобы он мог получить доступ к корневому контроллеру представления стека навигации. Начните с объявления свойства с именем 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];
}

Создайте проект и запустите приложение, чтобы испытать его.


В этом руководстве вы узнали, как использовать API-интерфейс Keychain Services для хранения конфиденциальных данных, а также как шифровать данные изображений на iOS. Оставьте комментарий в комментариях ниже, если у вас есть какие-либо вопросы или отзывы.