Transforming and Encrypting Data - Pro iOS Persistence: Using Core Data (2014)

Pro iOS Persistence: Using Core Data (2014)

Chapter 7. Transforming and Encrypting Data

Although Core Data offers a reasonable array of supported data types, not all data fit neatly into strings, Booleans, numbers, and so on. Core Data offers a catch-all data type, Binary Data, that can store any type of binary data. We used a Binary Data type in Chapter 5, for example, to store images. Core Data marshals any data stored in a Binary Data type to and from an NSData instance. While this means that Core Data can store any kind of arbitrary data in a Binary Data column, it also means that your applications must transform NSData instances into representations useful to your application, and vice versa. While sometimes this may be trivial—with images, for example, creating an image from an NSData instance is as easy as calling UIImage imageWithData: and UIImagePNGRepresentation—in other cases, you may find your code sprinkled with logic to transform raw data to usable data in various places. Wouldn’t it be nice to have Core Data do this transformation work for you?

In this chapter, we explore the Transformable Core Data type, which allows you to hand the transformation reins to Core Data. We also explore how you can use Transformable in a specific, topical use case: encryption. Using the techniques in this chapter, you can keep user data safe in this social media age.

Using Transformable

In this section, we build an application that displays a dot on the screen, in a random color, for each time the user taps the screen. Figure 7-1 shows the finished application, with many dots.

image

Figure 7-1. The TapOut application

For each tap, Core Data stores three pieces of data.

· The x value of the tap, using Float

· The y value of the tap, using Float

· A random color, using Transformable

Creating the Project and Adding Core Data

Create an Xcode project using the Single View Application template. Set the Product Name to TapOut and leave the User Core Data checkbox unchecked. After creating the project, add Core Data support, as we have before, using the following steps:

1. Add the TapOut data model, TapOut.xcdatamodeld.

2. Add the Persistence class, which encapsulates TapOut’s Core Data interaction.

Listing 7-1 shows the Objective-C header file for the Persistence class., and Listing 7-2 shows the Objective-C implementation file. Listing 7-3 shows the Swift version of Persistence.

Listing 7-1. Persistence.h

#import <Foundation/Foundation.h>
@import CoreData;

@interface Persistence : NSObject

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;

@end

Listing 7-2. Persistence.m

#import "Persistence.h"

@implementation Persistence

- (id)init {
self = [super init];
if (self != nil) {
// Initialize the managed object model
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"TapOut" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

// Initialize the persistent store coordinator
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"TapOut.sqlite"];

NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}

// Initialize the managed object context
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return self;
}

#pragma mark - Helper Methods

- (void)saveContext {
NSError *error = nil;
if ([self.managedObjectContext hasChanges] && ![self.managedObjectContext save:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}

- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

@end

Listing 7-3. Persistence.swift

import Foundation
import CoreData

class Persistence {
// MARK: - Core Data stack

lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "book.persistence.TapOutSwift" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1] as NSURL
}()

lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource("TapOutSwift", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("TapOutSwift.sqlite")
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
coordinator = nil
// Report any error we got.
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}

return coordinator
}()

lazy var managedObjectContext: NSManagedObjectContext? = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
if coordinator == nil {
return nil
}
var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()

// MARK: - Core Data Saving support

func saveContext () {
if let moc = self.managedObjectContext {
var error: NSError? = nil
if moc.hasChanges && !moc.save(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}
}
}

}

Then, add a property for a Persistence instance to AppDelegate, as shown in Listing 7-4 (Objective-C) and Listing 7-5 (Swift), and initialize it when the application launches, as shown in Listing 7-6 (Objective-C) and Listing 7-7 (Swift).

Listing 7-4. Adding a Persistence Property to AppDelegate.h

@class Persistence;
...
@property (strong, nonatomic) Persistence *persistence;

Listing 7-5. Adding a Persistence Property to AppDelegate.swift

var persistence : Persistence?

Listing 7-6. Initializing the Core Data Stack in AppDelegate.m

#import "Persistence.h"

...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.persistence = [[Persistence alloc] init];
return YES;
}

Listing 7-7. Initializing the Core Data Stack in AppDelegate.swift

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
persistence = Persistence()
return true
}

To wrap up adding Core Data, open your Core Data model, TapOut.xcdatamodeld, and add an entity called Tap. Give it three attributes.

· x, of type Float

· y, of type Float

· color, of type Transformable

Generate a class for it in Xcode, making sure to check the checkbox to use scalar properties for primitive types. If you look in the Objective-C header file, Tap.h, or in the Swift file, Tap.swift, you’ll notice that Core Data has set the type of color to id, if you’re using Objective-C, or AnyObject, if you’re using Swift. This is the default, as Core Data has no idea what type you plan to transform from and to. You can safely update this type to the type you plan to use, UIColor * (Objective-C) or UIColor (Swift), so that Tap.h now matches Listing 7-8 andTap.swift matches Listing 7-9.

Listing 7-8. Tap.h

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import <UIKit/UIKit.h>

@interface Tap : NSManagedObject

@property (nonatomic) float x;
@property (nonatomic) float y;
@property (nonatomic, retain) UIColor *color;

@end

Listing 7-9. Tap.swift

import Foundation
import CoreData
import UIKit

@objc(Tap)
class Tap: NSManagedObject {

@NSManaged var color: UIColor
@NSManaged var x: Float
@NSManaged var y: Float

}

Creating Taps

Anytime the user taps the screen, we create a Tap object in the Core Data store. We’ll handle the tap in the view controller, ViewController, and add a method to Persistence to actually add the Tap. We start in Persistence. Open Persistence.h, if you’re doing this in Objective-C, and add a method declaration for adding a Tap, as shown in Listing 7-10.

Listing 7-10. Declaration of a Method in Persistence.h to Add a Tap

#import <UIKit/UIKit.h>

...

- (void)addTap:(CGPoint)point;

In Persistence.m or Persistence.swift, add the definition of addTap:. This method creates a Tap instance in the managed object context, sets its x and y values to the corresponding values in the specified point, sets color to a random color using a helper function, and then saves the managed object context. Listing 7-11 shows the Objective-C version of this method and the random-color-generating helper, and Listing 7-12 shows the Swift version.

Listing 7-11. Adding a Tap in Persistence.m

#import "Tap.h"

...

- (void)addTap:(CGPoint)point {
Tap *tap = [NSEntityDescription insertNewObjectForEntityForName:@"Tap" inManagedObjectContext:self.managedObjectContext];
tap.x = point.x;
tap.y = point.y;
tap.color = [self randomColor];
[self saveContext];
}

- (UIColor *)randomColor {
float red = (arc4random() % 256) / 255.0f;
float green = (arc4random() % 256) / 255.0f;
float blue = (arc4random() % 256) / 255.0f;

return [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
}

Listing 7-12. Adding a Tap in Persistence.swift

func addTap(point: CGPoint) {
var tap = NSEntityDescription.insertNewObjectForEntityForName("Tap", inManagedObjectContext: self.managedObjectContext!) as? Tap

tap?.x = Float(point.x)
tap?.y = Float(point.y)
tap?.color = self.randomColor()
self.saveContext()
}

func randomColor() -> UIColor {
let red = CGFloat(arc4random() % UInt32(256)) / 255.0
let green = CGFloat(arc4random() % UInt32(256)) / 255.0
let blue = CGFloat(arc4random() % UInt32(256)) / 255.0

return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
}

Now add a tap gesture recognizer to the view controller that ultimately calls the addTap: method when you tap the view. Listing 7-13 shows the updated ViewController.m file, and Listing 7-14 shows the updated ViewController.swift file.

Listing 7-13. ViewController.m with a Tap Gesture Recognizer

#import "ViewController.h"
#import "AppDelegate.h"
#import "Persistence.h"

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(viewWasTapped:)];
[self.view addGestureRecognizer:tapGestureRecognizer];
}

- (void)viewWasTapped:(UIGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateEnded) {
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
Persistence *persistence = appDelegate.persistence;
CGPoint point = [sender locationInView:sender.view];
[persistence addTap:point];
[self.view setNeedsDisplay];
}
}

@end

Listing 7-14. ViewController.swift with a Tap Gesture Recognizer

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "viewWasTapped:")
self.view.addGestureRecognizer(tapGestureRecognizer)
}

func viewWasTapped(sender: UIGestureRecognizer) {
if sender.state == .Ended {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let point = sender.locationInView(sender.view)
appDelegate.persistence?.addTap(point)
self.view.setNeedsDisplay()
}
}
}

You should be able to build and run TapOut now and tap the screen to create Tap instances. You can verify that the Tap instances are created by looking in the SQLite database, but nothing displays yet on the screen. In the next section, we add code to actually display the Tap instances that Core Data is storing.

Creating the View and Displaying the Taps

To display the Tap instances, we first fetch them from Core Data. Then, we iterate through them, drawing them on the view using standard drawing primitives. Add a method called taps to Persistence that fetches all the Tap instances from the Core Data store. This code should be familiar, as it’s standard code for fetching objects from Core Data. Listing 7-15 shows the method declaration in Persistence.h, Listing 7-16 shows the Objective-C definition in Persistence.m, and Listing 7-17 shows the Swift definition in Persistence.swift.

Listing 7-15. Declaring the taps Method in Persistence.h

- (NSArray *)taps;

Listing 7-16. Fetching All the Tap Instances in Persistence.m

- (NSArray *)taps {
NSError *error = nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Tap"];
NSArray *taps = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (taps == nil) {
NSLog(@"Error fetching taps %@, %@", error, [error userInfo]);
}
return taps;
}

Listing 7-17. Fetching All the Tap Instances in Persistence.swift

func taps() -> [Tap] {
var error: NSError? = nil
let fetchRequest = NSFetchRequest(entityName: "Tap")
let taps = self.managedObjectContext?.executeFetchRequest(fetchRequest, error: &error)

return taps as [Tap]
}

Now create a class called TapView that extends UIView. In the drawRect: method, fetch the Tap instances using the method you just created, and then iterate through them to draw them on the screen as circles, centered on their x and y values, in the color stored in Core Data. Listing 7-18 shows TapView.h, Listing 7-19 shows the Objective-C TapView.m, and Listing 7-20 shows the Swift TapView.swift.

Listing 7-18. TapView.h

@interface TapView : UIView

@end

Listing 7-19. TapView.m

#import "TapView.h"
#import "AppDelegate.h"
#import "Persistence.h"
#import "Tap.h"

@implementation TapView

- (void)drawRect:(CGRect)rect {
static float DIAMETER = 10.0f;

CGContextRef context = UIGraphicsGetCurrentContext();

AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSArray *taps = [appDelegate.persistence taps];
for (Tap *tap in taps) {
const CGFloat *rgb = CGColorGetComponents(tap.color.CGColor);
CGContextSetRGBFillColor(context, rgb[0], rgb[1], rgb[2], 1.0f);
CGContextFillEllipseInRect(context, CGRectMake(tap.x – DIAMETER/2, tap.y – DIAMETER/2, DIAMETER, DIAMETER));
}
}

@end

Listing 7-20. TapView.swift

import Foundation
import UIKit

class TapView : UIView {
override func drawRect(rect: CGRect) {
let DIAMETER = CGFloat(10.0)

let context = UIGraphicsGetCurrentContext()

let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
let taps = appDelegate?.persistence?.taps()
if let taps = taps {
for tap : Tap in taps {
let rgb = CGColorGetComponents(tap.color.CGColor)
CGContextSetRGBFillColor(context, rgb[0], rgb[1], rgb[2], 1.0)
CGContextFillEllipseInRect(context, CGRectMake(CGFloat(tap.x - Float(DIAMETER/2)), CGFloat(tap.y - Float(DIAMETER/2)), DIAMETER, DIAMETER))
}
}
}
}

Update the application’s view to use this new view class.

1. Open Main.storyboard.

2. Select the View.

3. In the Identity Inspector, change the Class dropdown to TapView.

Now, run the application, and all the Tap instances you’ve added should be displayed. You can also tap the screen to add more dots. You can tap until your iPhone looks like it’s contracted measles!

Hey, Wait a Minute! How Did My Color Transform?

You might notice that we didn’t write any transformation code to transform our UIColor instance into or out of the Core Data store. How did Core Data know how to transform it?

In pre-iOS 5 days, we would have had to write a custom transformer class to perform transformations for UIColor instances. As of iOS 5, however, UIColor adheres to the NSCoding protocol, so it’s automatically encoded and decoded properly. It’s so easy, it almost feels like cheating.

We won’t be so lucky with our encryption transformation, however, and will have to write a custom transformation class to encrypt and decrypt our data. We do this in the next section.

Encrypting Data

In this age of social media, identity theft, and NSA (National Security Agency) snooping, people increasingly value privacy. Keeping your device physically secure can protect your data—until you leave your iPhone in a bar or a thief steals it. In the event that someone else gains control of your iPhone or iPad, that person can siphon off the SQLite databases that Core Data uses in various applications and, using tools like the ones we’re using in this book, read all your data—including the private, sensitive bits that can make you blush, or can even destroy your life.

One way to safeguard that data is to encrypt them using known, industry-strength encryption algorithms. In the rest of this chapter, we create a secure note-taking application called SecureNote that stores all notes in encrypted format. We use Core Data’s Transformable data type and a custom transformation class to manage the encryption and decryption of the data. The finished application will resemble Figure 7-2.

image

Figure 7-2. The SecureNote application

Creating the Project and Setting Up the Core Data Model

Create a new project in Xcode using the Master-Detail Application. Call it SecureNote and make sure the Use Core Data check box is checked. Once Xcode creates your project, open the Core Data model, SecureNote.xcdatamodeld, and delete the Event entity that Xcode created for you. Then, add an entity called Note and give it three attributes.

· timeStamp (Date)

· title (Transformable)

· body (Transformable)

We leave timeStamp unencrypted, but we will encrypt title and body using a custom transformer class. The next section discusses encryption and decryption and then builds a custom transformer class that Core Data will use for encrypting and decrypting title and body.

Before we move on to encryption and decryption, however, update the NSFetchedResultsController that Xcode generated for you to fetch Note instances. You’ll find this code in MasterViewController.m or MasterViewController.swift in thefetchedResultsController accessor. Change it to match Listing 7-21.

Listing 7-21. Fetching Note Instances

NSEntityDescription *entity = [NSEntityDescription entityForName:@"Note" inManagedObjectContext:self.managedObjectContext]; // Objective-C

let entity = NSEntityDescription.entityForName("Note", inManagedObjectContext: self.managedObjectContext!) // Swift

Generate an NSManagedObject subclass for the Note entity. The generated class will set the type of body and title to id (Objective-C) or AnyObject (Swift), because it doesn’t know what else to set it to, but you can change the type for both to NSString * (Objective-C) or String (Swift). Listing 7-22 shows the Note.h header file, Listing 7-23 shows the Note.m Objective-C implementation file, and Listing 7-24 shows the Note.swift Swift implementation file.

Listing 7-22. Note.h

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface Note : NSManagedObject

@property (nonatomic, retain) NSString *body;
@property (nonatomic, retain) NSDate *timeStamp;
@property (nonatomic, retain) NSString *title;

@end

Listing 7-23. Note.m

#import "Note.h"

@implementation Note

@dynamic body;
@dynamic timeStamp;
@dynamic title;

@end

Listing 7-24. Note.swift

import Foundation
import CoreData

@objc(Note)
class Note: NSManagedObject {

@NSManaged var timeStamp: NSDate
@NSManaged var title: String
@NSManaged var body: String

}

We want the master view to show the note’s title, with the timestamp displayed below it, so open the storyboard (Main.storyboard), select the prototype cell in the master view, and change its style in the Attributes inspector from Basic to Subtitle, as shown in Figure 7-3.

image

Figure 7-3. Setting the table view cell to Subtitle

Now, update the configureCell:atIndexPath: method in MasterViewController.m or MasterViewController.swift to show the title of the note in the main text label, with the timestamp in the detail text label, as shown in Listing 7-25 (Objective-C) and Listing 7-26(Swift).

Listing 7-25. Showing the Note Title and the Timestamp in the Cell (Objective-C)

#import "Note.h"

...

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Note *note = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = note.title;
cell.detailTextLabel.text = [note.timeStamp description];
}

Listing 7-26. Showing the Note Title and the Timestamp in the Cell (Swift)

func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) {
let note = self.fetchedResultsController.objectAtIndexPath(indexPath) as Note
cell.textLabel.text = note.title
cell.detailTextLabel?.text = note.timeStamp.description
}

You should update the insertNewObject: method in MasterViewController.m or MasterViewController.swift as well to use the new Note class, as shown in Listing 7-27 (Objective-C) and Listing 7-28 (Swift). In that method, we create a new Note instance, and we set the timestamp to the current date and time, the title to “New Note,” and the body to an empty string. We also select the new note and transition to its detail view, so that, once we update the detail view, we’ll be able to edit the new note.

Listing 7-27. Using the Note Class When Creating a New Object (Objective-C)

- (void)insertNewObject:(id)sender {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
Note *note = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];

note.timeStamp = [NSDate date];
note.title = @"New Note";
note.body = @"";

// Save the context.
NSError *error = nil;
if (![context save:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}

// Select the new note and segue to its detail view
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
[self performSegueWithIdentifier:@"showDetail" sender:self];
}

Listing 7-28. Using the Note Class When Creating a New Object (Swift)

func insertNewObject(sender: AnyObject) {
let context = self.fetchedResultsController.managedObjectContext
let entity = self.fetchedResultsController.fetchRequest.entity
let note = NSEntityDescription.insertNewObjectForEntityForName(entity!.name!, inManagedObjectContext: context) as Note

note.timeStamp = NSDate()
note.title = "New Note"
note.body = ""

// Save the context.
var error: NSError? = nil
if !context.save(&error) {
abort()
}

// Select the new note and segue to its detail view
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: .Top)
self.performSegueWithIdentifier("showDetail", sender: self)
}

Performing the Encryption and Decryption

To do the actual encryption and decryption, we use the Common Crypto library supplied by iOS. This isn’t a book on encryption, so we don’t delve into details on how the encryption works. We encourage you to do your own research on encryption if you want to know more. Some iOS cryptography resources include

· Apple’s Cryptographics Services Guide at https://developer.apple.com/library/mac/documentation/security/conceptual/cryptoservices/Introduction/Introduction.html

· Rob Napier’s blog post: “Properly Encrypting With AES With CommonCrypto” at http://robnapier.net/aes-commoncrypto/

For the purposes of this chapter, you should understand the following:

· We use symmetrical encryption using a user-supplied password as the key.

· For each string we encrypt, we create a random salt that we combine with the password and hash using SHA1.

· We use the AES algorithm for encryption.

We use a user-supplied password that we store in a member of the application delegate called, appropriately, password. Add it to AppDelegate.h or AppDelegate.swift, as shown in Listing 7-29 (Objective-C) and Listing 7-30 (Swift).

Listing 7-29. Adding Password to the Application Delegate (Objective-C)

@property (copy, nonatomic) NSString *password;

Listing 7-30. Adding Password to the Application Delegate (Swift)

var password: String?

Before we discuss how to prompt the user for the password and how to verify it’s correct, and how to generate and store the salts for each string, we build the actual value transformer. Before building it, we must understand the value transformer methods and what they do.

Understanding the NSValueTransformer Methods

NSValueTransformer has four interesting methods: two are static, and two are instance methods. Table 7-1 lists these methods and what they do.

Table 7-1. The NSValueTransformer Methods

Method

Description

+ (BOOL)allowsReverseTransformation

Static method that returns YES if this value transformer supports reverse transformations, NO if it doesn’t. The default is YES.

+ (Class)transformedValueClass

The class of the value returned by a forward transformation.

- (id)transformedValue:(id)value

Called to transform value into the new value.

- (id)reverseTransformedValue:(id)value

Called to transform in reverse, transforming value to the new value. Your value transformer must support reverse transformations. Override this method if you can’t reverse a transformation simply by reapplying transformedValue;.

Implementing the Encryption/Decryption Value Transformer

For our encryption/decryption value transformer, we must support both forward and reverse transformations—we encrypt on a forward transformation into the Core Data data store and decrypt on a reverse transformation into memory and onto the screen. Thus, we overridetransformedValue: and reverseTransformedValue: to encrypt and decrypt, respectively. We don’t bother to override allowsReverseTransformation, as the default return value is YES. We return the NSData class for transformedValueClass, since that is the type of the object we return from transformedValue:.

An interesting problem is how to store two pieces of data for each string we encrypt: the salt and the initialization vector (IV), each of which is random data. These bits of data combine with the password to prevent rainbow table attacks across your data. We create these when we encrypt a string, and then use them to decrypt the string. Since we must have the salt and IV for each string to decrypt it, we must store both the salt and the IV for each encrypted string.

You might think we could create attributes in the Note entity in the Core Data model to store the salt and the IV for each managed object, but remember that we’re giving Core Data an NSTransformer-derived class to read and write our Transformable attributes. Core Data code, not our code, will be instantiating and calling our transformers. Further, our transformer class knows nothing about the context in which it’s called—it doesn’t know anything about the managed object that it’s being used for. All it knows is that it receives a value and returns a value. We can’t read from or write to other attributes in the model in our transformer because we don’t know which managed object we’re working with, or even which attribute we’re encrypting or decrypting.

The solution, then, is to mash together the salt, IV, and encrypted string into a single attribute when we write the data, and parse the salt, IV, and encrypted string back from the data when we read so we can perform the decryption. We’ll store the data as follows:

· Salt (8 bytes)

· IV (16 bytes)

· Encrypted string (the rest of the bytes)

For the actual encryption and decryption, we create a parameterized helper method called transform:, which allows us to specify whether to encrypt or decrypt, since the code for doing each operation is so similar. This method takes the password, salt, and IV as parameters. As we said before, we don’t discuss the details of the actual encryption and decryption but have added comments to the code that you can follow.

To add the security framework to the Objective-C version of the project, you simply add this statement to any file that must reference any security code:

@import SystemConfiguration;

Adding frameworks to a Swift project isn’t so easy, at least as of this writing. To add the security framework to your Swift project, do the following:

1. Create a dummy Objective-C file in your project.

2. Xcode will ask you if you want to create a bridge header file. Say yes.

3. Delete the dummy Objective-C file.

4. Open the bridge header, SecureNoteSwift-Bridging-Header.h, and add these imports:

#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonDigest.h>

#import "SSKeychain.h"

Listing 7-31 shows the header file for our value transformer, EncryptionTransformer, Listing 7-32 shows the Objective-C implementation file, and Listing 7-33 shows the Swift implementation file.

Listing 7-31. EncryptionTransformer.h

#import <Foundation/Foundation.h>

@interface EncryptionTransformer : NSValueTransformer

@end

Listing 7-32. EncryptionTransformer.m

@import SystemConfiguration;

#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonDigest.h>
#import "EncryptionTransformer.h"
#import "AppDelegate.h"

#define kSaltLength 8

@implementation EncryptionTransformer

+ (Class)transformedValueClass {
return [NSData class];
}

- (id)transformedValue:(id)value {
// We're passed in a string (NSString) that we're going to encrypt.
// The format of the bytes we return is:
// salt (16 bytes) | IV (16 bytes) | encrypted string
NSData *salt = [self randomDataOfLength:kSaltLength];
NSData *iv = [self randomDataOfLength:kCCBlockSizeAES128];
NSData *data = [(NSString *)value dataUsingEncoding:NSUTF8StringEncoding];

// Do the actual encryption
NSData *result = [self transform:data
password:[self password]
salt:salt
iv:iv
operation:kCCEncrypt];

// Build the response data
NSMutableData *response = [[NSMutableData alloc] init];
[response appendData:salt];
[response appendData:iv];
[response appendData:result];
return response;
}

- (id)reverseTransformedValue:(id)value {
// We're passed in bytes (NSData) from Core Data that we're going to transform
// into a string (NSString) and return to the application.

// The bytes are in the format:
// salt (16 bytes) | IV (16 bytes) | encrypted data
NSData *data = (NSData *)value;
NSData *salt = [data subdataWithRange:NSMakeRange(0, kSaltLength)];
NSData *iv = [data subdataWithRange:NSMakeRange(kSaltLength, kCCBlockSizeAES128)];
NSData *text = [data subdataWithRange:NSMakeRange(kSaltLength + kCCBlockSizeAES128, [data length] - kSaltLength - kCCBlockSizeAES128)];

// Get the decrypted data
NSData *decrypted = [self transform:text password:[self password] salt:salt iv:iv operation:kCCDecrypt];

// Return only the decrypted string
return [[NSString alloc] initWithData:decrypted encoding:NSUTF8StringEncoding];
}

- (NSData *)transform:(NSData *)value
password:(NSString *)password
salt:(NSData *)salt
iv:(NSData *)iv
operation:(CCOperation)operation {
// Get the key by salting the password
NSData *key = [self keyForPassword:password salt:salt];

// Perform the operation (encryption or decryption)
size_t outputSize = 0;
NSMutableData *outputData = [NSMutableData dataWithLength:value.length + kCCBlockSizeAES128];
CCCryptorStatus status = CCCrypt(operation,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
key.bytes,
key.length,
iv.bytes,
value.bytes,
value.length,
outputData.mutableBytes,
outputData.length,
&outputSize);

// On success, set the size and return the data
// On failure, return nil
if (status == kCCSuccess) {
outputData.length = outputSize;
return outputData;
} else {
return nil;
}
}

- (NSData *)keyForPassword:(NSString *)password
salt:(NSData *)salt {
// Append the salt to the password
NSMutableData *passwordAndSalt = [[password dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
[passwordAndSalt appendData:salt];

// Hash it
NSData *hash = [self sha1:passwordAndSalt];

// Create the key by using the hashed password and salt, making
// it the proper size for AES128 (0-padding if necessary)
NSRange range = NSMakeRange(0, MIN(hash.length, kCCBlockSizeAES128));
NSMutableData *key = [NSMutableData dataWithLength:kCCBlockSizeAES128];
[key replaceBytesInRange:range withBytes:hash.bytes];
return key;
}

- (NSData *)sha1:(NSData *)data {
// Get the SHA1 into an array
uint8_t digest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(data.bytes, (CC_LONG) data.length, digest);

// Create a formatted string with the SHA1
NSMutableString* sha1 = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) {
[sha1 appendFormat:@"%02x", digest[i]];
}
return [sha1 dataUsingEncoding:NSUTF8StringEncoding];
}

- (NSString *)password {
return ((AppDelegate *)[[UIApplication sharedApplication] delegate]).password;
}

- (NSData *)randomDataOfLength:(size_t)length {
// SecRandomCopyBytes returns 0 on success, non-zero on failure
// If the call fails, the buffer will be full of zeros left over
// from NSMutableData dataWithLength:, which is less secure!
// A shipping app may choose to fail if this step fails
NSMutableData *randomData = [NSMutableData dataWithLength:length];
SecRandomCopyBytes(kSecRandomDefault, length, randomData.mutableBytes);
return randomData;
}

@end

Listing 7-33. EncryptionTransformer.swift

import Foundation

import UIKit
import Security

@objc(EncryptionTransformer)
class EncryptionTransformer : NSValueTransformer {
let saltLength : UInt = 8

override class func transformedValueClass() -> AnyClass {
return NSData.self
}

override func transformedValue(value: AnyObject?) -> AnyObject? {
// We're passed in a string (NSString) that we're going to encrypt.
// The format of the bytes we return is:
// salt (16 bytes) | IV (16 bytes) | encrypted string

let salt = self.randomDataOfLength(saltLength)
let iv = self.randomDataOfLength(UInt(kCCBlockSizeAES128))

let data = value?.dataUsingEncoding(NSUTF8StringEncoding)

// Do the actual encryption
let result = self.transform(data!, password: self.password()!, salt: salt, iv: iv, operation: UInt32(kCCEncrypt))

// Build the response data
var response = NSMutableData()
response.appendData(salt)
response.appendData(iv)
response.appendData(result!)
return response
}

override func reverseTransformedValue(value: AnyObject?) -> AnyObject? {
// We're passed in bytes (NSData) from Core Data that we're going to transform
// into a string (NSString) and return to the application.
let data = value as NSData?
if let data = data {
let salt = data.subdataWithRange(NSMakeRange(0, Int(saltLength)))
let iv = data.subdataWithRange(NSMakeRange(Int(saltLength), kCCBlockSizeAES128))
let text = data.subdataWithRange(NSMakeRange(Int(saltLength) + kCCBlockSizeAES128, data.length - Int(saltLength) - kCCBlockSizeAES128))

// Get the decrypted data
let decrypted = self.transform(text, password: self.password()!, salt: salt, iv: iv, operation: UInt32(kCCDecrypt))

// Return only the decrypted data
return NSString(data: decrypted!, encoding: NSUTF8StringEncoding)
}
else {
return nil
}
}

func transform(value: NSData, password: String, salt: NSData, iv: NSData, operation: CCOperation) -> NSData? {
// Get the key by salting the password
let key = self.keyForPassword(password, salt: salt)

var outputSize :UInt = 0

// Perform the operation (encryption or decryption)
let outputData = NSMutableData(length: value.length + kCCBlockSizeAES128)
let status = CCCrypt(operation, UInt32(kCCAlgorithmAES128), UInt32(kCCOptionPKCS7Padding), key.bytes, UInt(key.length), iv.bytes, value.bytes, UInt(value.length), outputData!.mutableBytes, UInt(outputData!.length), &outputSize)

// On success, set the size and return the data
// On failure, return nil
if UInt32(status) == UInt32(kCCSuccess) {
outputData!.length = Int(outputSize)
return outputData
}
else {
return nil
}
}

func sha1(data: NSData) -> NSData {
// Get the SHA1 into an array
var digest = [UInt8](count:Int(CC_SHA1_DIGEST_LENGTH), repeatedValue: 0)
CC_SHA1(data.bytes, CC_LONG(data.length), &digest)

// Create a formatted string with the SHA1
let sha1 = NSMutableString(capacity: Int(CC_SHA1_DIGEST_LENGTH))
for byte in digest {
sha1.appendFormat("%02x", byte)
}

return sha1.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
}

func keyForPassword(password: String, salt: NSData) -> NSData {
// Append the salt to the password
let passwordAndSalt = password.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)?.mutableCopy() as? NSMutableData
passwordAndSalt?.appendData(salt)

// Hash it
let hash = self.sha1(passwordAndSalt!)

// Create the key by using the hashed password and salt, making
// it the proper size for AES128 (0-padding if necessary)
let range = NSMakeRange(0, min(hash.length, kCCBlockSizeAES128));
let key = NSMutableData(length: kCCBlockSizeAES128)
key.replaceBytesInRange(range, withBytes: hash.bytes)
return key
}

func randomDataOfLength(length: UInt) -> NSData {
let data = NSMutableData(length: Int(length))
var dataPointer = UnsafeMutablePointer<UInt8>(data.mutableBytes)
SecRandomCopyBytes(kSecRandomDefault, length, dataPointer);
return data
}

func password() -> NSString? {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
return appDelegate.password
}
}

Although you’ll find code to cover the encryption details, you can also see the NSValueTransformer methods as you’d expect: transformedValue: receives a string to encrypt and returns the data structure we discussed earlier—the salt, IV, and encrypted string. ThereverseTransformedValue: method, as expected, receives that same data structure, parses the salt, IV, and encrypted string, and returns the decrypted string.

Open the Core Data model, SecureNote.xcdatamodeld, and set both the title and body attributes of Note to use this transformer. You do this by selecting the attribute, opening the Data Model inspector, and typing the transformer’s class name, EncryptionTransformer, in the Name field below the Attribute Type dropdown, as shown in Figure 7-4.

image

Figure 7-4. Setting title to use EncryptionTransformer

Setting Up the Detail View to Edit a Note

The existing SecureNote detail view shows the timestamp of the selected entity. Instead, we want to show the selected note’s title and body in editable fields. When the user taps the back button, we save the data from the fields into the Note entity and then save the managed object context.

In DetailViewController.h, change the detailItem property to note, of type Note*. Delete the existing UILabel and create the text field and text view for the note’s title and body, respectively. Listing 7-34 shows the updated header file.

Listing 7-34. DetailViewController.h with the Fields for the Note

#import <UIKit/UIKit.h>

@class Note;

@interface SNDetailViewController : UIViewController

@property (strong, nonatomic) Note *note;
@property (weak, nonatomic) IBOutlet UITextField *noteTitle;
@property (weak, nonatomic) IBOutlet UITextView *noteBody;

@end

Update DetailViewController.m to use the note instance and the noteTitle and noteBody controls instead of detailItem and detailDescriptionLabel. Also, in viewWillDisappear:, update note’s body and title and save the managed object context.Listing 7-35 shows the updated implementation file for Objective-C. Listing 7-36 shows the updated DetailViewController.swift that implements the corresponding changes.

Listing 7-35. DetailViewController.m with the Fields for the Note

#import "DetailViewController.h"
#import "Note.h"

@implementation DetailViewController

#pragma mark - Managing the note

- (void)setNote:(Note *)note {
if (_note != note) {
_note = note;
[self configureView];
}
}

- (void)configureView {
if (self.note) {
self.noteTitle.text = self.note.title;
self.noteBody.text = self.note.body;
} else {
self.noteTitle.text = @"";
self.noteBody.text = @"";
}
}

- (void)viewDidLoad {
[super viewDidLoad];
[self configureView];
}

- (void)viewWillDisappear:(BOOL)animated {
self.note.title = self.noteTitle.text;
self.note.body = self.noteBody.text;

NSError *error;
if (![[self.note managedObjectContext] save:&error]) {
NSLog(@"Error saving note %@ -- %@ %@", self.noteTitle.text, error, [error userInfo]);
}
}

@end

Listing 7-36. DetailViewController.swift with the Fields for the Note

import UIKit

class DetailViewController: UIViewController {
@IBOutlet weak var noteTitle: UITextField!
@IBOutlet weak var noteBody: UITextView!

var note: Note? {
didSet {
self.configureView()
}
}

func configureView() {
if self.noteTitle != nil {
if let note = self.note {
self.noteTitle.text = note.title
self.noteBody.text = note.body
}
else {
self.noteTitle.text = ""
self.noteBody.text = ""
}
}
}

override func viewDidLoad() {
super.viewDidLoad()
self.configureView()
}

override func viewWillDisappear(animated: Bool) {
self.note?.title = self.noteTitle.text
self.note?.body = self.noteBody.text

if let managedObjectContext = self.note?.managedObjectContext {
var error: NSError? = nil
if !managedObjectContext.save(&error) {
NSLog("Error saving note \(self.noteTitle.text) -- \(error) \(error!.userInfo)")
}
}
}

func setNote(note: Note) {
if(self.note != note) {
self.note = note
self.configureView()
}
}
}

Update the prepareForSegue:sender: method in MasterViewController.m or MasterViewController.swift to call setNote: instead of setDetailItem:, as shown in Listing 7-37 (Objective-C) or Listing 7-38 (Swift).

Listing 7-37. Calling setNote: Instead of setDetailItem: in MasterViewController.m

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
Note *note = [[self fetchedResultsController] objectAtIndexPath:indexPath];
[[segue destinationViewController] setNote:note];
}
}

Listing 7-38. Calling setNote: Instead of setDetailItem: in MasterViewController.swift

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow() {
let note = self.fetchedResultsController.objectAtIndexPath(indexPath) as Note
(segue.destinationViewController as DetailViewController).note = note
}
}
}

Open the storyboard, Main.storyboard, to update the user interface. In the detail view, delete the existing label. Add a borderless text field to the top of the view with the placeholder text Note Title, bump its font size to 18, and make it span the width of the view. Below that, add a text view that fills the rest of the space. Connect these two controls to noteTitle and noteBody, respectively. Use the Resolve Autolayout Issues popup menu to add missing constraints. Figure 7-5 shows the updated detail view.

image

Figure 7-5. The detail view with the note details

Prompting For and Verifying the Password

We’ve been ignoring the need for the user to enter a password that SecureNote uses to encrypt and decrypt the notes, so let’s address that now. To address password entry, we do the following:

1. Insert a new view and view controller in front of the master view controller that displays a password entry field and a Log In button.

2. When the user attempts to log in, set the password field we created in the application delegate to the entered password.

3. If the user has not yet entered a password, accept any input as the official password, store it in the iOS keychain, and segue to the master view.

4. If the user already has a password in the iOS keychain, compare the entered password to the stored password and, only if they match, segue to the master view.

Since we must interface with the iOS keychain, and the keychain API is a nasty mess of C code, we’re going to cheat a little and download Sam Soffes’s excellent library SSKeychain from https://github.com/soffes/sskeychain. Add this library to your project, either through CocoaPods or by cloning the repository and adding the source files. Since we already added the security framework to the project, you should be ready to use SSKeychain once you add the source files.

Adding the Password View Controller

Create a new UIViewController class (with no XIB file) called PasswordViewController that will control the password view, accept the password input, verify the password entered, and determine whether to segue to the master view. This controller will also be responsible for setting the managed object context in the master view controller to the application’s managed object context. The header file, PasswordViewController.h, should have an outlet for the password text field so we can extract the password that the user enters, as shown in Listing 7-39.

Listing 7-39. PasswordViewController.h with an Outlet for the Password Field

#import <UIKit/UIKit.h>

@interface PasswordViewController : UIViewController

@property (weak, nonatomic) IBOutlet UITextField *passwordField;

@end

The implementation file, either PasswordViewController.m or PasswordViewController.swift, overrides two methods.

· prepareForSegue:sender:, which allows us to do any preparation before the segue is performed.

· shouldPerformSegueWithIdentifier:sender:, which gives us the opportunity to either allow or disallow the segue to be performed.

In prepareForSegue:sender:, we grab the managed object context from the application delegate and set it on the Master View Controller.

In shouldPerformSegueWithIdentifier:sender:, we make sure the user has actually entered something in the password field. We set whatever the user has entered into the application delegate. Then, we grab the SecureNote password stored in the iOS keychain. If the password from the keychain is blank, we store the password the user entered into the keychain and then allow the segue to be performed by returning YES. If we find a SecureNote password in the keychain, however, we verify it against the password that the user entered and allow the segue to be performed only if they match. Listing 7-40 shows PasswordViewController.m and Listing 7-41 shows PasswordViewController.swift.

Listing 7-40. PasswordViewController.m

#import "PasswordViewController.h"
#import "AppDelegate.h"
#import "MasterViewController.h"
#import "SSKeychain.h"

@implementation PasswordViewController

#pragma mark - Navigation

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"logIn"]) {
// Set the managed object context in the master view controller
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
MasterViewController *controller = (MasterViewController *)[segue destinationViewController];
controller.managedObjectContext = appDelegate.managedObjectContext;
}
}

- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
// Make sure they've entered something in the password field
NSString *enteredPassword = self.passwordField.text;
if ([identifier isEqualToString:@"logIn"] && enteredPassword.length > 0) {

// Store whatever they've entered in the application delegate
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
appDelegate.password = enteredPassword;

// Retrieve the password from the keychain
NSString *password = [SSKeychain passwordForService:@"SecureNote" account:@"default"];

// If they've never entered a password, they're creating a new one
if (password == nil) {
// Store the password in the keychain and allow segue to be performed
[SSKeychain setPassword:enteredPassword forService:@"SecureNote" account:@"default"];
return YES;
} else {
// Verify password and allow segue only if verified
return [password isEqualToString:enteredPassword];
}
} else {
return NO;
}
}

@end

Listing 7-41. PasswordViewController.swift

import Foundation
import UIKit

class PasswordViewController : UIViewController {
@IBOutlet weak var passwordField: UITextField!

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "logIn" {
// Set the managed object context in the master view controller
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let controller = segue.destinationViewController as MasterViewController
controller.managedObjectContext = appDelegate.managedObjectContext
}
}

override func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool {
// Make sure they've entered something in the password field
let enteredPassword = self.passwordField.text
if identifier == "logIn" {
// Store whatever they've entered in the application delegate
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
appDelegate.password = enteredPassword

// Retrieve the password from the keychain
let password = SSKeychain.passwordForService("SecureNote", account: "default")

// If they've never entered a password, they're creating a new one
if password == nil {
// Store the password in the keychain and allow segue to be performed
SSKeychain.setPassword(enteredPassword, forService: "SecureNote", account: "default")
return true
} else {
// Verify password and allow segue only if verified
return password == enteredPassword
}
}
else {
return false;
}
}
}

Adding the Password View to the Storyboard

After some fiddling with the storyboard and a tweak to the application delegate code, we’ll finally be ready to run SecureNote.

1. First, edit the application:didFinishLaunchingWithOptions: method in AppDelegate.m or AppDelegate.swift to do nothing but return YES or true, as shown in Listing 7-42 (Objective-C) or Listing 7-43 (Swift).

Listing 7-42. Removing the Code That Sets the Managed Object Context (Objective-C)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}

Listing 7-43. Removing the Code That Sets the Managed Object Context (Swift)

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
return true
}

2. Now open the storyboard, Main.storyboard, and drag a new View Controller between the Navigation Controller and the Master View Controller. You may have to move the Master View Controller and the Detail View Controller to the right to make room.

3. Set the Custom Class in the Identity inspector of your new View Controller to PasswordViewController.

4. In the Attributes inspector, set the Title field to Password. Drag a label onto the view and set its text to Password:. Drag a Text Field below the label, size it to the width of the view, and connect it to the passwordField outlet. Also, in the Attributes inspector, check the Secure Text Entry checkbox so that the password is masked as the user types it. Finally, drag a button onto the view, below the password field, and set its text to Log In. Add missing constraints from the popup menu.

5. Control+drag from the button to the Master View Controller and, in the pop-up that appears, select show, so that tapping this button will initiate the segue to the Master View Controller. Also, with the arrow representing the segue selected, open the Attributes inspector and set the Identifier to logIn. Figure 7-6 shows the Password View Controller.

image

Figure 7-6. The Password View Controller

6. Your last task is to change the segue from the Navigation Controller to point to the Password View Controller. Select the Navigation Controller, open the Connections inspector, and break the connection to the Master View Controller. Then, drag from the circle at the end of the root view controller line to the Password View Controller, and the segue will update properly. Your finished storyboard should resemble Figure 7-7.

image

Figure 7-7. The finished storyboard

Running SecureNote

You should now be able to build and run SecureNote. When you run it, you should see the password view. Enter anything to set your password, and then click Log In. You should then be able to tap the + button to add notes or tap existing notes to edit them.

If you rerun the application, you should see the password view again. Entering an incorrect password and tapping Log In should do nothing. Entering the correct password and tapping Log In should take you to the master view with your list of notes.

If you open your SQLite database and look at the data, you should find that the data in the ZBODY and ZTITLE columns in the ZNOTE table contain incomprehensible gobbledygook—the fruits of our encryption transformer.

An Alternative to Transformable: Data Protection

In the previous section, we encrypted data on a field-by-field basis using value transformers and the Common Crypto library. Apple offers another way to encrypt data on an iPhone or iPad using what’s called data protection. If you enable data protection on your device, iOS encrypts a portion of its data storage when the device is locked and decrypts it when it’s unlocked. To enable data protection, set your device to require a passcode in Settings image Touch ID & Passcode or Settings image Passcode, depending on your device and iOS version. At the very bottom of that settings screen, you should see the following message: “Data protection is enabled.” Note that this is available on a device only and not on the iOS Simulator.

Turning on data protection doesn’t automatically encrypt all the storage on the device. If you want to encrypt a Core Data data store, you must turn it on by setting an attribute on the persistent store. You do this by passing a dictionary with the value NSFileProtectionComplete for the key NSFileProtectionKey to the setAttributes:ofItemAtPath:error method of NSFileManager. This would look something like Listing 7-44 (Objective-C) or Listing 7-45 (Swift).

Listing 7-44. Setting the Core Data Persistent Store to Use Data Protection (Objective-C)

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}

NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"SecureNote.sqlite"];

NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}

// Encrypt the persistent store
NSDictionary *fileAttributes = @{ NSFileProtectionKey : NSFileProtectionComplete };
if (![[NSFileManager defaultManager] setAttributes: fileAttributes
ofItemAtPath:[storeURL path]
error:&error]) {
NSLog(@"Unresolved error with persistent store encryption %@, %@", error, [error userInfo]);
abort();
}

return _persistentStoreCoordinator;
}

Listing 7-45. Setting the Core Data Persistent Store to Use Data Protection (Swift)

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SecureNoteSwift.sqlite")
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
coordinator = nil
// Report any error we got.
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError.errorWithDomain("YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}

// Encrypt the persistent store
let fileAttributes = [NSFileProtectionKey : NSFileProtectionComplete]
if !NSFileManager.defaultManager().setAttributes(fileAttributes, ofItemAtPath: url.path!, error: &error) {
NSLog("Unresolved error with persistent store encryption \(error), \(error!.userInfo)")
abort()
}

return coordinator
}()

When you run your application on a device that has data protection enabled, the persistent store’s database file will be automatically encrypted when the device is locked. This method is cheap to implement, but understand that once the device is unlocked, the file is automatically decrypted so the database is as secure as the device. If someone can guess the passcode, the database will be accessible, especially if the device is jail broken (i.e., root access is available). Another inconvenience with this encryption method is that for very large databases, the time required to unlock and start your application might be rather large since a huge file will need to be decrypted before it can be used.

Summary

In this chapter we saw value transformers and how to use them to extend Core Data to be able to store any data type. If our data type supports the NSCoding protocol, as UIColor does, we saw that we don’t even need to provide a customer value transformer class. Even when our data type doesn’t support the NSCoding protocol, we see how simple adding a custom value transformer can be to allow us to simply read and write any data type into and out of a Core Data store.

We also saw how to use the data protection built in to iOS by setting attributes on our Core Data store file.

With this knowledge, we can protect our applications’ data from prying eyes, and perhaps even the NSA!