Whether you're creating a mobile application or a web service, keeping sensitive data secure is important and security has become an essential aspect of every software product. In this tutorial, I will show you how to safely store user credentials using the application's keychain and we'll take a look at encrypting and decrypting user data using a third party library.
Introduction
In this tutorial, I will teach you how to secure sensitive data on the iOS platform. Sensitive data can be a user's account credentials or credit card details. The type of data isn't that important. In this tutorial, we will use iOS's keychain and symmetric encryption to securely store the user's data. Before we get into the nitty-gritty details, I'd like to give you an overview of what we're going to do in this tutorial.
iOS Keychain
On iOS and OS X, a keychain is an encrypted container for storing passwords and other data that needs to be secured. On OS X, it is possible to limit keychain access to particular users or applications. On iOS, however, each application has its own keychain to which only the application has access. This ensures that the data stored in the keychain is safe and unaccessible by third parties.
Keep in mind that the keychain should only be used for storing small pieces of data, such as passwords. With this article, I hope to convince you of the value of using the keychain on iOS and OS X instead of, for example, the application's user defaults database, which stores its data in plain text without any form of security.
On iOS, an application can use the keychain through the Keychain Services API. The API provides a number of functions for manipulating the data stored in the application's keychain. Take a look at the functions available on iOS.
-
SecItemAdd
This function is used for adding an item to the application's keychain. -
SecItemCopyMatching
You use this function to find a keychain item owned by the application. -
SecItemDelete
As its name implies, this function can be used to remove an item from the application's keychain. -
SecItemUpdate
Use this function if you need to update an item in the application's keychain.
The Keychain Services API is a C API, but I hope that doesn't prevent you from using it. Each of the above functions accepts a dictionary (CFDictionaryRef
), which contains an item class key-value pair and optional attribute key-value pairs. The exact meaning and purpose of each will become clear once we start using the API in an example.
Encryption and Decryption
When discussing encryption, you generally hear about two types of encryption, symmetric and asymmetric encryption. Symmetric encryption, on the one hand, uses one shared key for encrypting and decrypting data. Asymmetric encryption, on the other hand, uses one key for encrypting data and another separate, but related, key for decrypting data.
In this tutorial, we'll leverage the Security framework available on iOS to encrypt and decrypt data. This process takes place under the hood so we won't be directly interacting with this framework. We'll use symmetric encryption in our example application.
The Security framework offers a number of other services, such as Randomization services for generating cryptographically secure random numbers, Certificate, Key, and Trust Services for managing certificates, public and private keys, and trust policies. The Security framework is a low-level framework available on both iOS and OS X with C-based APIs.
Application Overview
In this tutorial, I will show you how you can use the Keychain Services API and symmetric encryption in an iOS application. We'll create a small application that securely stores photos taken by the user.
In this project, we'll use Sam Soffes SSKeychain, an Objective-C wrapper for interacting with the Keychain Services API. For encryption and decryption, we'll use RNCryptor, a third party encryption library.
Encrypting Data with RNCryptor
The RNCryptor library is a good choice for encrypting and decrypting data. The project is used by many developers and actively maintained by its creators. The library offers an easy to use Objective-C API. If you're familiar with Cocoa and Objective-C, you'll find it easy to use. The library's main features are listed below.
- AES-256 Encryption
- CBC Mode
- Password Stretching with PBKDF2
- Password Salting
- Random IV
- Encrypt-Then-Hash HMAC
Application Flow
Before we start building the application, let me show you what the typical flow of the application will look like.
- When the user launches the application, she's presented with a view to sign in.
- If she hasn't created an account yet, her credentials are added to the keychain and she's signed in.
- If she has an account, but enters an incorrect password, an error message is shown.
- Once she's signed in, she has access to the photos she's taken with the application. The photos are securely stored by the application.
- Whenever she takes a photo with the device's camera or picks a photo from her photo library, the photo is encrypted and stored in the application's Documents directory.
- Whenever she switches to another application or the device gets locked, she's automatically signed out.
Building the Application
Step 1: Project Setup
Fire up Xcode and create a new project by selecting the Single View Application template from the list of templates.
Name the project Secure Photos and set Device Family to iPhone. Tell Xcode where you want to save the project and hit Create.
Step 2: Frameworks
The next step is to link the project against the Security and Mobile Core Services frameworks. Select the project in the Project Navigator on the left, choose the first target named Secure Photos, and open the Build Phases tab at the top. Expand the Link Binary With Libraries drawer and link the project against the Security and Mobile Core Services frameworks.
Step 3: Dependencies
As I mentioned earlier, we'll be using the SSKeychain library and the RNCryptor library. Download these dependencies and add them to the project. Make sure to copy the files to your project and add them to the Secure Photos target as shown in the screenshot below.
Step 4: Creating Classes
We will display the user's photos in a collection view, which means we need to subclass UICollectionViewController
as well as UICollectionViewCell
. Select New > File... from the File menu, create a subclass of UICollectionViewController
, and name it MTPhotosViewController
. Repeat this step one more time for MTPhotoCollectionViewCell
, which is a subclass of UICollectionViewCell
.
Step 5: Creating the User Interface
Open the project's main storyboard and update the storyboard as shown in the screenshot below. The storyboard contains two view controllers, an instance of MTViewController
, which contains two text fields and a button, and an instance of MTPhotosViewController
. The MTViewController
instance is embedded in a navigation controller.
We also need to create a segue from the MTViewController
instance to the MTPhotosViewController
instance. Set the segue's identifier to photosViewController
. The MTPhotosViewController
instance should also contain a bar button item as shown in the screenshot below.
To make all this work, we need to update the interface of MTViewController
as shown below. We declare an outlet for each text field and an action that is triggered by the button. Make the necessary connections in the project's main storyboard.
#import <UIKit/UIKit.h> @interface MTViewController : UIViewController @property (weak, nonatomic) IBOutlet UITextField *usernameTextField; @property (weak, nonatomic) IBOutlet UITextField *passwordTextField; - (IBAction)login:(id)sender; @end
In the MTPhotosViewController
class, declare a property named username
for storing the username of the currently signed in user and declare an action for the bar button item. Don't forget to connect the action with the bar button item in the main storyboard.
#import <UIKit/UIKit.h> @interface MTPhotosViewController : UICollectionViewController @property (copy, nonatomic) NSString *username; - (IBAction)photos:(id)sender; @end
Step 6: Implementing MTViewController
In MTViewController.m
, add an import statement for the MTPhotosViewController
class, the SSKeychain
class, and the MTAppDelegate
class. We also conform the MTViewController
class to the UIAlertViewDelegate
protocol.
#import "MTViewController.h" #import "SSKeychain.h" #import "MTAppDelegate.h" #import "MTPhotosViewController.h" @interface MTViewController () <UIAlertViewDelegate> @end
The next step is implementing the login:
action we declared earlier. We first check whether the user has already created an account by fetching the password for the account. If this is true, we use the application's keychain to see if the password entered by the user matches the one stored in the keychain. The methods provided by the SSKeychain library make it easy to read and manipulate data stored in the application's keychain.
- (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." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } } else { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"New Account" message:@"Do you want to create an account?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil]; [alertView show]; } } else { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Input" message:@"Username and/or password cannot be empty." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } }
We've set the view controller as the alert view's delegate, which means we need to implement the UIAlertViewDelegate
protocol. Take a look at the implementation of alertView:clickedButtonAtIndex:
shown below.
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ switch (buttonIndex) { case 0: break; case 1: [self createAccount]; break; default: break; } }
In createAccount
, we leverage the SSKeychain
class to securely store the username and password chosen by the user. We then call performSegueWithIdentifier:sender:
.
- (void)createAccount { BOOL result = [SSKeychain setPassword:self.passwordTextField.text forService:@"MyPhotos" account:self.usernameTextField.text]; if (result) { [self performSegueWithIdentifier:@"photosViewController" sender:nil]; } }
In prepareForSegue:sender:
, we get a reference to the MTPhotosViewController
instance, set its username
property with the value of the usernameTextField
, and reset the passwordTextField
.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ MTPhotosViewController *photosViewController = segue.destinationViewController; photosViewController.username = self.usernameTextField.text; self.passwordTextField.text = nil; }
Step 7: Implementing MTPhotosCollectionViewCell
Open MTPhotosCollectionViewCell.h and declare an outlet named imageView
of type UIImageView
.
#import <UIKit/UIKit.h> @interface MTPhotoCollectionViewCell : UICollectionViewCell @property (weak, nonatomic) IBOutlet UIImageView *imageView; @end
Open the main storyboard and add an UIImageView
instance to the prototype cell of the MTPhotosViewController
instance. Select the prototype cell (not the image view) and set its class to MTPhotosCollectionViewCell
in the Identity Inspector on the right. With the prototype cell still selected, open the Attributes Inspector and set the identifier to PhotoCell
.
Step 8: Implementing MTPhotosViewController
Start by importing the necessary header files in MTPhotosViewController.m as shown below. We also need to declare two properties, photos
for storing the array of photos the collection view will display and filePath
to keep a reference to the file path. You may have noticed that the MTPhotosViewController
class conforms to the UIActionSheetDelegate
, UINavigationControllerDelegate
, and UIImagePickerControllerDelegate
protocols.
#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
I've also implemented a convenience or helper method, setupUserDirectory
, for creating and setting up the necessary directories in which we'll store the user's data. In prepareData
, the application decrypts the images that are stored in the user's secure directory. Take a look at their implementations below.
- (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."); } } }
- (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."); } } }
Invoke both methods in the view controller's viewDidLoad
method as shown below.
- (void)viewDidLoad { [super viewDidLoad]; [self setupUserDirectory]; [self prepareData]; }
The bar button item in the view controller's navigation bar shows an action sheet allowing the user to choose between the device's camera and the photo library.
- (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]; }
Let's implement actionSheet:clickedButtonAtIndex:
of the UIActionSheetDelegate
protocol.
- (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]; } }
To handle the user's selection in the image picker controller, we need to implement imagePickerController:didFinishPickingMediaWithInfo:
of the UIImagePickerControllerDelegate
protocol as shown below. The image is encrypted using encryptData
of the RNEncryptor
library. The image is also added to the photos
array and the collection view is reloaded.
- (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]; }
Before you can build and run the application, we need to implement the UICollectionViewDataSource
protocol as shown below.
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.photos ? self.photos.count : 0; }
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { MTPhotoCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"PhotoCell" forIndexPath:indexPath]; cell.imageView.image = [self.photos objectAtIndex:indexPath.row]; return cell; }
Step 9: Handling Application States
If the application goes to the background, the user needs to be signed out. This is important from a security perspective. To accomplish this, the application delegate needs to have a reference to the navigation controller so it can pop to the root view controller of the navigation stack. Start by declaring a property named navigationController
in MTAppDelegate.h.
#import <UIKit/UIKit.h> @interface MTAppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) UINavigationController *navigationController; @end
In the view controller's viewDidLoad
method, we set the application delegate's navigationController
property as shown below. Keep in mind that this is just one way to handle this.
I have set the above property in ViewController's
viewDidLoad
method as shown below.
- (void)viewDidLoad { [super viewDidLoad]; MTAppDelegate *applicationDeleagte = (MTAppDelegate *)[[UIApplication sharedApplication] delegate]; [applicationDeleagte setNavigationController:self.navigationController]; }
In the application delegate, we need to update applicationWillResignActive:
as shown below. It's as simple as that. The result is that the user is signed out whenever the application loses focus. It will protect the user's images stored in the application from prying eyes. The downside is that the user needs to sign in when the application becomes active again.
- (void)applicationWillResignActive:(UIApplication *)application { [self.navigationController popToRootViewControllerAnimated:NO]; }
Step 10: Build and Run
Build the project and run the application to put it through its paces.
Conclusion
In this tutorial, you learned how to use the Keychain Services API to store sensitive data and you also learned how to encrypt image data on iOS. Leave a comment in the comments below if you have any questions or feedback.
Comments