Protecting resources in iPhone and iPad apps
源码:https://github.com/lingzhao/EncryptedResourceDemo
UPDATE: The example project has been updated to work with iOS5.
If you are distributing an iPhone or iPad app you may be giving away more than you realize. Resources that you embed in your application bundle can be extracted very easily by anyone who has downloaded the app with iTunes or has the app on their iOS device. This is not such a problem if those resources are just parts of your UI or the odd NIB file, but once you realise that your high-value resources, such as embedded PDFs, audio files, proprietary datasets and high-resolution images are just as accessible then you might want to consider how to protect those resources from prying eyes or unauthorised use outside your application.
To see just how accessible your resources are, use a tool such as iPhone Explorer to connect to your device. This tool lets you browse all the app bundles on your device and copy any resource to your Mac or PC. Alternatively, just locate the ipa file that is downloaded when you purchase an app with iTunes, change the extension to ‘.zip’ then unzip it. All your resources are on display.
Protection through encryption
In this article I’m going to walk through one approach to protecting your resources from unauthorised use outside your application. During the build phase, your high-value resources will be encrypted before being embedded in the app bundle. These resources can then be decrypted on-the-fly when required in your app. I had a number of goals in mind when developing the solution presented in this article:
- Adding new resources to be encrypted should be quick and easy.
- The encryption process should be completely automatic during the build phase.
- Encrypted PDFs or HTML files must be useable in an embedded UIWebView
- Decryption must be in-memory and not create temporary unprotected files
I also did not want to complicate the submission process by introducing encryption that might be subject to export restrictions, so we are limiting ourselves to using the CommonCrypto API, which is already distributed as part of iOS. The usual caveats apply when using any sort of encryption approach – given enough time and determination someone will bypass any protection scheme you can come up with. However, make it difficult enough to crack the protection compared to the value of the protected resources and it becomes a question of economics. If you are working with client-supplied resources then it becomes even more important to use a scheme such as the one described here to protectyourself from charges of negligence should those resources turn up on some torrent site.
An encryption command-line tool
So, down to the details. We’re going to create a simple command-line tool that will be run as part of the build process to encrypt resources using the AES256 symmetric cipher. An Xcode project to build the tool can be found at the bottom of this article. The tool takes command-line arguments specifying a 256-bit key, the input file path and an output file path. We will use a custom build step to call the command-line tool for each of our resources that we want to be encrypted.
Setting up our project
Now we have a tool to perform the encryption, we can turn to an example project that makes use of the encryption to protect embedded resources. You can find an example Xcode project at the end of this article. This sample simply displays a UIWebView when run and populates it with protected HTML and image files, which are embedded as encrypted resources in the app.
The first step is to create a sub-folder under the project folder to contain the original resources that we want to protect. In the example we have called this sub-folder ‘EncryptedResources’. One of our goals was to make adding new resources as quick and easy as possible and when we have finished setting up the project we will be able to add a new protected resource simply by dragging it into this folder within Finder. As a cosmetic convenience I also added the folder to the Xcode project as a folder reference (by checking the ‘Create Folder References for any added folders’ checkbox) but please remember to deselect any target membership on the ‘Targets’ tab of the Info dialog for this folder or the unencrypted resources will be added to the app bundle by the regular ‘Copy Bundle Resources’ build step.
Adding a custom build step
To process the files in the EncryptedResources folder, we add a custom build step by selecting the ‘Project > New Build Phase > New Run Script Build Phase’ menu item in Xcode. In the example we have moved this build step to the start of the project target (under the Targets > EncryptedResourceDemo item in the ‘Groups and Files’ list) so that it is run as the first step in the build process. The shell script associated with this step can be viewed by selecting the ‘File/Get Info’ menu item when the step is selected:
DIRNAME=EncryptedResources ENC_KEY="abcdefghijklmnopqrstuvwxyz123456" INDIR=$PROJECT_DIR/$DIRNAME OUTDIR=$TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/$DIRNAME if [ ! -d "$OUTDIR" ]; then mkdir -p "$OUTDIR" fi for file in "$INDIR"/* do echo "Encrypting $file" "$PROJECT_DIR/crypt" -e -k $ENC_KEY -i "$file" -o "$OUTDIR/`basename "$file"`" done
The shell script iterates over every file in the ‘EncryptedResources’ folder and calls our ‘crypt’ tool for each file, placing the encrypted output in the application bundle. Note that the script expects the ‘crypt’ tool to be present in the base of the project directory. As an alternative, you could place the ‘crypt’ tool somewhere on your path and modify the script accordingly. Note also, that it is in the build script where we specify the key to use for encryption. The 256-bit key is specified as 32 x 8-bit characters – you should change it from the default given here.
Using protected resources in applications
So now we have a built app bundle containing our protected resources. The resources can still be viewed with tools such as iPhone Explorer or extracted from an iTunes ipa file but are useless without the correct decryption key. We now turn to how these resources can be used legitimately by your app. In creating a decryption framework, I wanted a scheme that would be as flexible as possible and not place an unnecessary burden on the developer every time she wanted to use a protected resource. I also needed a scheme that decrypted in memory and did not create temporary files (which could be viewed with iTunes Explorer while an app was running). I chose to create a custom URL protocol and extend the URL loading system. This allows us to use encrypted resources anywhere that a URL can be specified and, thanks to the widespread use of URLs in the iOS frameworks, we get a lot of bang for our buck with relatively little new code including:
- Loading encrypted resources into memory with
[NSData dataWithContentsOfURL:]
- Viewing encrypted HTML files and images in UIWebView
Custom URL protocols
If you haven’t come across implementing custom URL protocols before and you are interested in finding out more then check out the iPhone Reference Library. By subclassing NSURLProtocol
and implementing a few required methods we can grant the URL loading system the ability to load other types of resources beyond the standard built-in schemes (http:
, https:
, ftp:
and file:
).
Our NSURLProtocol
subclass is called EncryptedFileURLProtocol
and the implementation can be found under the EncryptedFileURLProtocol group in the example project. It implements a new URL scheme (encrypted-file:
) that works like the standardfile:
scheme. A URL using this scheme specifies a file on the local system just like file:
, however, the files will be decrypted on-the-fly when the resource is loaded.
A custom NSURLProtocol subclass must override the canInitWithRequest:
. The URL loading system uses this method to determine which protocol can handle a particular request. The loading system asks each registered protocol in turn whether it can handle the specified request. The first protocol to return YES
gets to handle the request. The implementation of our canInitWithRequest:
method is shown below and will return YES
if the requested URL scheme is ‘encrypted-file:’.
33 34 35 36 |
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { return ([[[request URL] scheme] isEqualToString:ENCRYPTED_FILE_SCHEME_NAME]); } |
A custom NSURLProtocol must also override two additional methods, startLoading
and stopLoading
. These are called by the URL loading system at appropriate points when handling a request. Our startLoading
method initializes the cryptographic engine and opens an NSInputStream to load the resource. The decryption key is also specified at this point. It is shared between all instances of our custom NSURLProtocol and needs to match the key used for encryption during the build process. The default key value is specified on line 20 of EncryptedFileURLProtocol.m and can be modfied using the class-level key
property.
58 59 60 61 62 63 64 65 66 67 68 |
- (void)startLoading { inBuffer = malloc(BUFFER_LENGTH); outBuffer = malloc(BUFFER_LENGTH); CCCryptorCreate(kCCDecrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding, [sharedKey cStringUsingEncoding:NSISOLatin1StringEncoding], kCCKeySizeAES256, NULL, &cryptoRef); inStream = [[NSInputStream alloc] initWithFileAtPath:[[self.request URL] path]]; [inStream setDelegate:self]; [inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [inStream open]; } |
The stopLoading
method is called when the request ends, either due to normal completion or an error condition. In our implementation we clean up the input stream, cryptographic engine and buffers:
71 72 73 74 75 76 77 78 79 80 |
- (void)stopLoading { [inStream close]; [inStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [inStream release]; inStream = nil; CCCryptorRelease(cryptoRef); free(inBuffer); free(outBuffer); } |
Because we have scheduled our input stream on the current run loop and specified our custom instance of NSURLProtocol as the stream delegate, we will be called periodically with chunks of data to process. Below is an extract from the stream event handler where a chunk of data read from the input stream is decrypted and passed on to the URL loading system:
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
switch(streamEvent) { case NSStreamEventHasBytesAvailable: { size_t len = 0; len = [(NSInputStream *)inStream read:inBuffer maxLength:BUFFER_LENGTH]; if (len) { // Decrypt read bytes if (kCCSuccess != CCCryptorUpdate(cryptoRef, inBuffer, len, outBuffer, BUFFER_LENGTH, &len)) { [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:ERROR_DOMAIN code:DECRYPTION_ERROR_CODE userInfo:nil]]; return; } // Pass decrypted bytes on to URL loading system NSData *data = [NSData dataWithBytesNoCopy:outBuffer length:len freeWhenDone:NO]; [self.client URLProtocol:self didLoadData:data]; } break; } ... |
Implementing these four methods is nearly all we need to do to leverage the power of the URL loading system. The final piece of the puzzle is to register our custom NSURLProtocol subclass with the system. This is done in the application:didFinishLaunchingWithOptions:
method of our application delegate (in EncryptedResourceDemoAppDelegate.m):
30 31 |
// Register the custom URL protocol with the URL loading system [NSURLProtocol registerClass:[EncryptedFileURLProtocol class]]; |
Using the encrypted-file:
URL scheme
Now that we have implemented our custom NSURLProtocol subclass and registered it with the URL loading system we can start using URLs that use the encrypted-file:
scheme. The example application demonstrates one way to do this – that is to use the scheme to load protected resources into a UIWebView instance. This is actually not very different from using a regular file:
URL to load and view an embedded HTML resource. The code from the viewDidLoad
method of our main view controller is shown below (from EncryptedResourceDemoViewController.m):
21 22 23 24 25 26 27 28 29 30 31 |
- (void)viewDidLoad { [super viewDidLoad]; webView.scalesPageToFit = YES; NSString *indexPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"EncryptedResources"]; NSURL *url = [NSURL encryptedFileURLWithPath:indexPath]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [webView loadRequest:request]; } |
The app bundle is queried for the path to ‘index.html’ in the EncryptedResources sub-folder. This is then used to construct an NSURL. An NSURLRequest is created from the URL and a loadRequest:
message is sent to the web view. If you are familiar with NSURL and its class methods then you may have spotted the unfamiliar encryptedFileURLWithPath:
method. I have extended NSURL using a category to add this method as a convenience. It works just like fileURLWithPath:
but creates a URL using the encrypted-file:
scheme rather than the regular file:
scheme. One cool benefit of extending the URL loading system is that any relative URLs referenced in the HTML, such as the src
parameter of IMG elements, will also use the encrypted-file:
protocol and will be decrypted on-the-fly.
As mentioned above, URLs are used in many places in the iOS frameworks. To load an encrypted resource into memory you can do the following:
NSString *indexPath = [[NSBundle mainBundle] pathForResource:@"..." ofType:@"..." inDirectory:@"EncryptedResources"]; NSURL *url = [NSURL encryptedFileURLWithPath:indexPath]; NSData *data = [NSData dataWithContentsOfURL:url];
This could used to create a UIImage from an encrypted image file using [UIImage initWithData:]
or you could go one step further by extending UIImage using categories to implement a initWithContentsOfEncryptedFile:
method.
Next steps
In this article I have presented a scheme for protecting the resources that you embed in your iPhone and iPad applications. Along the way we have also learned about how to use the CommonCrypto API and how to implement a custom URL protocol. The example project demonstrates how to use the scheme and you are free to use the ‘crypt’ command-line tool and EncryptedFileURLProtocol source in your own projects. Something you might want to think about (depending on the value of the resources you are trying to protect and your level of paranoia) is a mechanism for obfuscating the decryption key. With the current scheme the key is compiled into the application binary as a plain string and could be extracted by anyone with a hex editor, a little patience and a little knowledge. Of course, this assumes that they have worked out that the resources are encrypted using AES256.
We’d love to hear if you have found this article useful or even used it in your own projects.