iOS/Swift + Objective-c

[Objective-c] KeyChain example

안경 쓴 귀니 2017. 3. 28. 10:59
반응형




[Objective-c] KeyChain example






Keychain

If you’re a developer, you know you will, you know you will have to store sensitive data in your app sooner or later. Storing sensitive data in NSUserDefaults is a big no-no, so you just can’t store sensitive data there. Instead, just like OS X, iOS provides a keychain your app can use to store all sorts of sensitive data. The keychain is they key place to store all those user passwords or bank information your app may need to store in the app. If you are just starting with iOS development, or just mid-level, the idea of having to store sensitive data is scary. The good news is, iOS has a very easy API to use with the keychain. Interacting with the keychain on iOS is very simple as the API is composed of just four essential functions.

The Keychain Essentials.

The iOS Keychain Is Much Different Than The OS X Keychain

The OS X keychain is much, much more complex than the iOS keychain. This tutorial is specifically for the iOS keychain.

First, in OS X, any app can request access to any info currently stored in the keychain. If you’re an OS X user, you may recall those prompts notifying you that a given app would like to access data in your keychain. They look like this:

mac-os-x-105-leopard-keychain-access-password-dialogue-box

 

Once you put in your password, the keychain will open and grab the data it needs, and hand it over to the app requesting access to it.

Second, the OS X Keychain has one more feature you cannot find in iOS, and this is one of the reasons the iOS Keychain is so simple: In OS X, the user can create more than one keychain for different things. One for certificates, one for passwords, one for anything else that may be sensitive (not many people do this, but this keychain feature is there for those who need it).

In iOS, each app has their own keychain and this keychain cannot be accessed by any other apps, unless  both apps that want to access this keychain are made and maintained by you, have the Keychain Sharing entitlement enabled, and they are both in the same Keychain Access Group(s). This is a completely different paradigm than the OS X Keychain, because now other apps cannot tamper with your private keychain. Also, each app in iOS has one keychain only, where you can add one or more keychain items.

Also, unlike OS X, the user may not even be aware the keychain exists in iOS. When you’re dealing with the iOS Keychain, the user never gets any prompts and nothing gets in the way of your user for interacting with your app. The keychain is completely hidden for the end user, and you are responsible to make the user interaction details as abstract as possible.

iOS Keychain Concepts And Structure

The keychain is the storage, secure and encrypted your app can use to store sensitive data. It is important that your app and all subsequent versions of it are all signed by the same mobile provision profile. If they aren’t, you will have many troubles later on.

Keychain Item is the unit of sensitive data stored in your app. If you visualize a keychain, like the one your grandma has for all her keys, a keychain item is an ‘key’. Keychain items have the sensitive data you want to store, and are accompanied by one or more attributes. The attributes describe the keychain item, and what attributes you can use depend on the item class of the keychain item. The item class is “what kind” of data you are going to store. This can be a user/password combination, a certificate, a generic password, and things like that.

Keychain Search Dictionaries.

Search dictionaries specify the attributes for the keychain item you want to update or create.

All dictionary items have a very similar structure. First, they contain the key-value class pair, to specify “what” you’re going to store (again, this can be a user/password combination, a generic password, a certificate, etc).

Second, they contain one or more key-value pairs that set the attributes for the item. This can be a label to give the item a name, the creation time, and anything other that may describe the item.

Third, they specify search attributes and return attributes, to tell your app how it should retrieve this item from the keychain.

Search dictionaries are also known as queries.

Getting Our Hands Dirty

Once you have absorbed all that theory, let’s build a small app that is going to store a few user/password pairs in the keychain and retrieve them.

1. Create a new XCode project for iPhone only, and make it a single-view application. Make sure you are using Storyboards and ARC.
2. The Keychain Services API is part of Security.framework. Go ahead and add it to your project (yourAppBlueprint > Build Phases > Link Binary with Libraries).
3. Add text fields and buttons like this:

Screenshot 2014-06-10 19.39.53

(The “Password” field would normally be set to hide the characters you type with circles. I am not using this field as a password field so you can see what you type on it – Naturally don’t do this in a real world app).

This app will be very simple. It will allow users to save and modify their user/password combination. The bottom part (everything after the “Fetch Data” label) will be used to get the password, and to delete the keychain item.

Create outlets for all the text fields, actions for all the buttons, and an extra outlet for the “Password” label at the bottom. Your header should look like this (nwUsernameTxtFld refers to the top most username text field):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <UIKit/UIKit.h>
 
@interface TKViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UITextField *nwUsernameTxtFld;
@property (weak, nonatomic) IBOutlet UITextField *passwordTxtFld;
@property (weak, nonatomic) IBOutlet UITextField *usernameTxtFld;
 
@property (weak, nonatomic) IBOutlet UILabel *passwordLbl;
 
- (IBAction)saveButtonPressed:(id)sender;
- (IBAction)updateButtonPressed:(id)sender;
- (IBAction)getPasswordPressed:(id)sender;
- (IBAction)deleteItemPressed:(id)sender;
 
 
@end

4. Now go to the view controller implementation file (.m) and import Security.framework.

1
#import <Security.framework>

5. Add the following code to your saveButtonPressed method. I will explain everything below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (IBAction)saveButtonPressed:(id)sender
{
    //Let's create an empty mutable dictionary:
    NSMutableDictionary *keychainItem = [NSMutableDictionary dictionary];
     
    NSString *username = self.nwUsernameTxtFld.text;
    NSString *password = self.passwordTxtFld.text;
    NSString *website = @"http://www.myawesomeservice.com";
     
    //Populate it with the data and the attributes we want to use.
     
    keychainItem[(__bridge id)kSecClass] = (__bridge id)kSecClassInternetPassword; // We specify what kind of keychain item this is.
    keychainItem[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleWhenUnlocked; // This item can only be accessed when the user unlocks the device.
    keychainItem[(__bridge id)kSecAttrServer] = website;
    keychainItem[(__bridge id)kSecAttrAccount] = username;
     
    //Check if this keychain item already exists.
     
    if(SecItemCopyMatching((__bridge CFDictionaryRef)keychainItem, NULL) == noErr)
    {
         
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"The Item Already Exists", nil)
                                                        message:NSLocalizedString(@"Please update it instead.", )
                                                       delegate:nil
                                              cancelButtonTitle:NSLocalizedString(@"OK", nil)
                                              otherButtonTitles:nil];
        [alert show];
         
    }else
    {
        keychainItem[(__bridge id)kSecValueData] = [password dataUsingEncoding:NSUTF8StringEncoding]; //Our password
         
        OSStatus sts = SecItemAdd((__bridge CFDictionaryRef)keychainItem, NULL);
        NSLog(@"Error Code: %d", (int)sts);
    }
}

The first thing we need to do is to create a search dictionary. We are creating a mutable dictionary to which we will add all our keychain item related data. Then we grab all our data, such as username and the server the password is for, and put it in variables to make our life easier.

We are creating a keychain item with the item class (kSecClasskSecClassInternetPassword. We need to do a bridge cast to the keys and all constants in ARC, otherwise the compiler is going to complain. The Keychain Services API works with Core Foundation objects, so throughout this tutorial you will see bridge casts and direct casts of Objective-C objects to their CF counterparts.

When we specify the class we are going to use, we need to check what keys can we give to the search dictionary. To do that, we head over to Apple’s Keychain Item Class Keys and Values reference and we look for the key we are using. We are using kSecClassInternetPassword, so we search for kSecClassInternetPassword and then we find that this dictionary can have the following keys:

kSecAttrAccessible
kSecAttrAccessGroup
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrSecurityDomain
kSecAttrServer
kSecAttrProtocol
kSecAttrAuthenticationType
kSecAttrPort
kSecAttrPath

You do not need to add them all. What kind of data these keys expect can also be found on the same page I linked you to.

One important key is kSecAttrAccessible. We are giving it kSecAttrAccessibleWhenUnlocked. This keys specify when should the data be accessible. The key we gave it specifies that the data is only accessible when the user unlocks the device. You can also make the keychain be accessible only on the specific device the user is using, make it always accessible, and a few other options. Check Apple’s Keychain Accessibility Constants to learn what other constants you can use with this key.

The kSecAttrServer and kSecAttrAccount keys can be pretty much anything you want, but it’s better to keep them relevant. They are strings, and it makes sense to make them the website a user login is used for and the username for it.

You may have noticed that we are not using the password yet. Before we store the password, it is better to make sure it doesn’t exist yet. To do that, we use the SecItemCopyMatching() function. This function takes, as the first parameter, a search dictionary, or query, to look for an existing keychain item. We have specified the search criteria, and you can add a few more keys if you want, such as kSecAttrProtocol and kSecAttrAuthenticationType. The second parameter is a pointer that will store the returned data, where the keychain will populate the search results. This parameter can be NULL, meaning that we don’t need to have the result handed back to us. In this case, we only want to know if an item with the specified criteria already exists and we don’t care about returning it.

This function searches our app’s keychain for items that match the criteria. You can optionally give it a Search Key to refine the search more. Note that not all these keys will work for the Internet Password class. The search keys also allow you to specify wether the function should return more than one matching keychain item or all of them. This is useful if you’re building an app for a web service, where the user can have various different usernames and passwords, and you want to fetch them all.

You can also give the dictionary a Return Type Keys, specifying how the return dictionary should be formed. You can make a dictionary return the value stored, all it’s attributes, whether it should be persistent, and so on. You can find the documentation for this in the last few links I posted.

All keychain-related functions (a grand total of 4) have OSStatus as their return type. It’s very important you check for this value. In the example above, we are checking if SecItemCopyMatching()’s return code is noErr. For this tutorial, we are going to assume that noErr means no error at all. But in a real-world scenario, you may want to check for Keychain Services Result Codes to properly debug your program. It is very common to get a errSecParam return code. It usually means the search dictionary you’re providing to your function is malformed. If you get this code, check that your search dictionary only has acceptable keys for its item class.

Now you know that after creating the initial parts of the dictionary, you check whether you have a keychain item with that criteria or not. If a keychain item exists with this criteria, we will show a alert view asking the user to update the data instead. If there isn’t, we will attempt to insert the keychain item into our keychain. First we add the last search dictionary item, whose key is kSecValueData (depending on how you need to store data, the key may change – check the last part of the Keychain Services Programming Guide). This is the actual password, and you need to convert it to NSData (failing to do this step results in the Sec functions to return errSecParam).

Finally, we create an OSStatus and assign the return value of the SecItemAdd() function. This function takes the search dictionary we want to add into the keychain, and additionally (yep, can be NULL), a second nil dictionary that will contain the resulting dictionary of the operation.

After this line is done, we will print into the console the result code of SecItemAdd(). It should print 0. If it doesn’t, compare your status result code with the table I linked to above and debug accordingly (errSecParam is -50 – If you have this code, check your dictionary).

Add a few dictionary items and make sure the console logs 0. If it does, you can now move on to implement the update method.

5.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (IBAction)updateButtonPressed:(id)sender
{
    //Let's create an empty mutable dictionary:
    NSMutableDictionary *keychainItem = [NSMutableDictionary dictionary];
     
    NSString *username = self.nwUsernameTxtFld.text;
    NSString *password = self.passwordTxtFld.text;
    NSString *website = @"http://www.myawesomeservice.com";
     
    //Populate it with the data and the attributes we want to use.
     
    keychainItem[(__bridge id)kSecClass] = (__bridge id)kSecClassInternetPassword; // We specify what kind of keychain item this is.
    keychainItem[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleWhenUnlocked; // This item can only be accessed when the user unlocks the device.
    keychainItem[(__bridge id)kSecAttrServer] = website;
    keychainItem[(__bridge id)kSecAttrAccount] = username;
     
    //Check if this keychain item already exists.
     
    if(SecItemCopyMatching((__bridge CFDictionaryRef)keychainItem, NULL) == noErr)
    {
        //The item was found.
         
        //We can update the keychain item.
         
        NSMutableDictionary *attributesToUpdate = [NSMutableDictionary dictionary];
        attributesToUpdate[(__bridge id)kSecValueData] = [password dataUsingEncoding:NSUTF8StringEncoding];
         
        OSStatus sts = SecItemUpdate((__bridge CFDictionaryRef)keychainItem, (__bridge CFDictionaryRef)attributesToUpdate);
        NSLog(@"Error Code: %d", (int)sts);
    }else
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Item Doesn't Exist.", nil)
                                                        message:NSLocalizedString(@"The item you want to update doesn't exist.", nil)
                                                       delegate:nil
                                              cancelButtonTitle:NSLocalizedString(@"OK", nil)
                                                           otherButtonTitles:nil];
        [alert show];
    }
}

You should be able to understand everything up to the if. In the if we check for the existence of the keychain item, giving the user an error if the keychain item they want to update doesn’t exist.

Afterwards we need to create a dictionary with all the attributes we want to update. In this case, we only want to update the password for a given user, so we pass in the value data key to update it.

The SecItemUpdate() function takes two parameters: The search query that brings the records you want to update, and a dictionary with the attributes you want to use to replace the ones already stored. Like all other Sec functions, it returns an status code, which you can check to see if the operation was successful or if it found any problems.

Add some new user/password combinations and edit them. The keychain should update the data correctly. After that you can move on to the fetch data method.

6.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
- (IBAction)getPasswordPressed:(id)sender
{
    //Let's create an empty mutable dictionary:
    NSMutableDictionary *keychainItem = [NSMutableDictionary dictionary];
     
    NSString *username = self.usernameTxtFld.text;
    NSString *website = @"http://www.myawesomeservice.com";
     
    //Populate it with the data and the attributes we want to use.
     
    keychainItem[(__bridge id)kSecClass] = (__bridge id)kSecClassInternetPassword; // We specify what kind of keychain item this is.
    keychainItem[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleWhenUnlocked; // This item can only be accessed when the user unlocks the device.
    keychainItem[(__bridge id)kSecAttrServer] = website;
    keychainItem[(__bridge id)kSecAttrAccount] = username;
     
    //Check if this keychain item already exists.
     
    keychainItem[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue;
    keychainItem[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue;
     
    CFDictionaryRef result = nil;
     
    OSStatus sts = SecItemCopyMatching((__bridge CFDictionaryRef)keychainItem, (CFTypeRef *)&result);
     
    NSLog(@"Error Code: %d", (int)sts);
     
    if(sts == noErr)
    {
        NSDictionary *resultDict = (__bridge_transfer NSDictionary *)result;
        NSData *pswd = resultDict[(__bridge id)kSecValueData];
        NSString *password = [[NSString alloc] initWithData:pswd encoding:NSUTF8StringEncoding];
         
        self.passwordLbl.text = password;
    }else
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"The Item Doesn't Exist", nil)
                                                        message:NSLocalizedString(@"No keychain item found for this user.", )
                                                       delegate:nil
                                              cancelButtonTitle:NSLocalizedString(@"OK", nil)
                                              otherButtonTitles:nil];
        [alert show];
    }
}

Fetching data is arguably the hardest thing to do when it comes to keychain items. One of the few things you will notice is that this search dictionary has two new keys, kSecReturnData and kSecReturnAttributes, and that they accept core foundation booleans. The first key will specify that it wants to return the securely stored data only, whereas the second key tells the API that it wants to fetch all the attributes. This is very important: If you only have kSecReturnData as true but not kSecReturnAttributes, the second parameter of the SecItemCopyMatching() function will only return the encrypted data and store it in a CFDataRef object. Whereas, if you also set kSecReturnAttributes to true, then the second parameter of the function will return a dictionary, that is, a CFDictionaryRef. They are different types and if you accidentally give it the wrong pointer to the second parameter, your app may crash eventually.

If the if evaluates to true, then it’s all nice and dandy. We create a new NSDictionary to which we transfer ownership of our old CFDictionaryRef while casting it as a NSDicitonary, and then we can just use it like a normal Objective-C object. We grab the NSData with the kSecValueData key, and then we convert it into a string, that later we give it the password label.

When you type in a username in the second username textfield, the user’s password should show up in the password label below. Try it and play with it. When you are certain it works, let’s move to the last method.

7.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (IBAction)deleteItemPressed:(id)sender
{
    //Let's create an empty mutable dictionary:
    NSMutableDictionary *keychainItem = [NSMutableDictionary dictionary];
     
    NSString *username = self.usernameTxtFld.text;
    NSString *website = @"http://www.myawesomeservice.com";
     
    //Populate it with the data and the attributes we want to use.
     
    keychainItem[(__bridge id)kSecClass] = (__bridge id)kSecClassInternetPassword; // We specify what kind of keychain item this is.
    keychainItem[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleWhenUnlocked; // This item can only be accessed when the user unlocks the device.
    keychainItem[(__bridge id)kSecAttrServer] = website;
    keychainItem[(__bridge id)kSecAttrAccount] = username;
     
    //Check if this keychain item already exists.
     
    if(SecItemCopyMatching((__bridge CFDictionaryRef)keychainItem, NULL) == noErr)
    {
        OSStatus sts = SecItemDelete((__bridge CFDictionaryRef)keychainItem);
        NSLog(@"Error Code: %d", (int)sts);
    }else
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"The Item Doesn't Exist.", nil)
                                                        message:NSLocalizedString(@"The item doesn't exist. It may have already been deleted.", nil)
                                                       delegate:nil
                                              cancelButtonTitle:NSLocalizedString(@"OK", nil)
                                              otherButtonTitles:nil];
        [alert show];
    }
}

This is the easiest method to implement. We do what we have been doing all along: Create a search dictionary and then check if we have items with that criteria. If it finds a dictionary, we simply call SecItemDelete(), which only takes one parameter that is the search dictionary, and like all the previous 3 Sec functions you have seen, it returns an OS Status.

Play with the whole app now and see if it behaves like you would expect it to behave.

Wrapping Up

You should know have a firm understanding of how the iOS Keychain works.

Keep in mind that the keychain can be used to store other data, even private keys and certificates. The iOS keychain, while keeping the API simple, is really powerful with all its features. The way search dictionaries work makes a lot of sense with the whole API.

You have seen the 4 functions that come with the Keychain Services API:

  • SecItemCopyMatching
  • SecItemAdd
  • SecItemUpdate
  • SecItemDelete

And you also learned how to use them.

As usual, any comments and feedback are very welcome. You may also download this project’s source code from here.

How to tell what each error code means:

If you get an error code (like -50), and you can’t tell what it means on the internet, you can open up the Terminal in your Mac and type:

1
security error -50

That will tell you what the error is so you can go about fixing it. In the case of error -50, the Terminal will tell you:

Error: 0xFFFFFFCE -50 One or more parameters passed to a function were not valid




[출처 : https://www.andyibanez.com/using-ios-keychain/]



반응형