changed git call from https to git readonly
[atutor.git] / mods / atsocial_iphone_app / KeychainUtils / SFHFKeychainUtils.m
1 //
2 //  SFHFKeychainUtils.m
3 //
4 //  Created by Buzz Andersen on 10/20/08.
5 //  Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone.
6 //  Copyright 2008 Sci-Fi Hi-Fi. All rights reserved.
7 //
8 //  Permission is hereby granted, free of charge, to any person
9 //  obtaining a copy of this software and associated documentation
10 //  files (the "Software"), to deal in the Software without
11 //  restriction, including without limitation the rights to use,
12 //  copy, modify, merge, publish, distribute, sublicense, and/or sell
13 //  copies of the Software, and to permit persons to whom the
14 //  Software is furnished to do so, subject to the following
15 //  conditions:
16 //
17 //  The above copyright notice and this permission notice shall be
18 //  included in all copies or substantial portions of the Software.
19 //
20 //  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 //  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 //  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 //  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 //  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 //  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 //  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 //  OTHER DEALINGS IN THE SOFTWARE.
28 //
29
30 #import "SFHFKeychainUtils.h"
31 #import <Security/Security.h>
32
33 static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";
34
35 #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
36 @interface SFHFKeychainUtils (PrivateMethods)
37 + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
38 @end
39 #endif
40
41 @implementation SFHFKeychainUtils
42
43 #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
44
45 + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
46         if (!username || !serviceName) {
47                 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
48                 return nil;
49         }
50         
51         SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
52         
53         if (*error || !item) {
54                 return nil;
55         }
56         
57         // from Advanced Mac OS X Programming, ch. 16
58     UInt32 length;
59     char *password;
60     SecKeychainAttribute attributes[8];
61     SecKeychainAttributeList list;
62         
63     attributes[0].tag = kSecAccountItemAttr;
64     attributes[1].tag = kSecDescriptionItemAttr;
65     attributes[2].tag = kSecLabelItemAttr;
66     attributes[3].tag = kSecModDateItemAttr;
67     
68     list.count = 4;
69     list.attr = attributes;
70     
71     OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
72         
73         if (status != noErr) {
74                 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
75                 return nil;
76     }
77     
78         NSString *passwordString = nil;
79         
80         if (password != NULL) {
81                 char passwordBuffer[1024];
82                 
83                 if (length > 1023) {
84                         length = 1023;
85                 }
86                 strncpy(passwordBuffer, password, length);
87                 
88                 passwordBuffer[length] = '\0';
89                 passwordString = [NSString stringWithCString:passwordBuffer];
90         }
91         
92         SecKeychainItemFreeContent(&list, password);
93     
94     CFRelease(item);
95     
96     return passwordString;
97 }
98
99 + (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {     
100         if (!username || !password || !serviceName) {
101                 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
102                 return;
103         }
104         
105         OSStatus status = noErr;
106         
107         SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
108         
109         if (*error && [*error code] != noErr) {
110                 return;
111         }
112         
113         *error = nil;
114         
115         if (item) {
116                 status = SecKeychainItemModifyAttributesAndData(item,
117                                                                                                                 NULL,
118                                                                                                                 strlen([password UTF8String]),
119                                                                                                                 [password UTF8String]);
120                 
121                 CFRelease(item);
122         }
123         else {
124                 status = SecKeychainAddGenericPassword(NULL,                                     
125                                                                                            strlen([serviceName UTF8String]), 
126                                                                                            [serviceName UTF8String],
127                                                                                            strlen([username UTF8String]),                        
128                                                                                            [username UTF8String],
129                                                                                            strlen([password UTF8String]),
130                                                                                            [password UTF8String],
131                                                                                            NULL);
132         }
133         
134         if (status != noErr) {
135                 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
136         }
137 }
138
139 + (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
140         if (!username || !serviceName) {
141                 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil];
142                 return;
143         }
144         
145         *error = nil;
146         
147         SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
148         
149         if (*error && [*error code] != noErr) {
150                 return;
151         }
152         
153         OSStatus status;
154         
155         if (item) {
156                 status = SecKeychainItemDelete(item);
157                 
158                 CFRelease(item);
159         }
160         
161         if (status != noErr) {
162                 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
163         }
164 }
165
166 + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
167         if (!username || !serviceName) {
168                 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
169                 return nil;
170         }
171         
172         *error = nil;
173     
174         SecKeychainItemRef item;
175         
176         OSStatus status = SecKeychainFindGenericPassword(NULL,
177                                                                                                          strlen([serviceName UTF8String]),
178                                                                                                          [serviceName UTF8String],
179                                                                                                          strlen([username UTF8String]),
180                                                                                                          [username UTF8String],
181                                                                                                          NULL,
182                                                                                                          NULL,
183                                                                                                          &item);
184         
185         if (status != noErr) {
186                 if (status != errSecItemNotFound) {
187                         *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
188                 }
189                 
190                 return nil;             
191         }
192         
193         return item;
194 }
195
196 #else
197
198 + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
199         if (!username || !serviceName) {
200         if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
201                 return nil;
202         }
203     
204         if (error) *error = nil;
205     
206         // Set up a query dictionary with the base query attributes: item type (generic), username, and service
207         
208         NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil] autorelease];
209         NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, nil] autorelease];
210         
211         NSMutableDictionary *query = [[[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
212         
213         // First do a query for attributes, in case we already have a Keychain item with no password data set.
214         // One likely way such an incorrect item could have come about is due to the previous (incorrect)
215         // version of this code (which set the password as a generic attribute instead of password data).
216         
217         NSDictionary *attributeResult = NULL;
218         NSMutableDictionary *attributeQuery = [query mutableCopy];
219         [attributeQuery setObject: (id) kCFBooleanTrue forKey:(id) kSecReturnAttributes];
220         OSStatus status = SecItemCopyMatching((CFDictionaryRef) attributeQuery, (CFTypeRef *) &attributeResult);
221         
222         [attributeResult release];
223         [attributeQuery release];
224         
225         if (status != noErr) {
226                 // No existing item found--simply return nil for the password
227                 if (status != errSecItemNotFound) {
228                         //Only return an error if a real exception happened--not simply for "not found."
229                         if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
230                 }
231                 
232                 return nil;
233         }
234         
235         // We have an existing item, now query for the password data associated with it.
236         
237         NSData *resultData = nil;
238         NSMutableDictionary *passwordQuery = [query mutableCopy];
239         [passwordQuery setObject: (id) kCFBooleanTrue forKey: (id) kSecReturnData];
240     
241         status = SecItemCopyMatching((CFDictionaryRef) passwordQuery, (CFTypeRef *) &resultData);
242         
243         [resultData autorelease];
244         [passwordQuery release];
245         
246         if (status != noErr) {
247                 if (status == errSecItemNotFound) {
248                         // We found attributes for the item previously, but no password now, so return a special error.
249                         // Users of this API will probably want to detect this error and prompt the user to
250                         // re-enter their credentials.  When you attempt to store the re-entered credentials
251                         // using storeUsername:andPassword:forServiceName:updateExisting:error
252                         // the old, incorrect entry will be deleted and a new one with a properly encrypted
253                         // password will be added.
254                         if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];                  
255                 }
256                 else {
257                         // Something else went wrong. Simply return the normal Keychain API error code.
258                         if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
259                 }
260                 
261                 return nil;
262         }
263     
264         NSString *password = nil;       
265     
266         if (resultData) {
267                 password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
268         }
269         else {
270                 // There is an existing item, but we weren't able to get password data for it for some reason,
271                 // Possibly as a result of an item being incorrectly entered by the previous code.
272                 // Set the -1999 error so the code above us can prompt the user again.
273                 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];          
274         }
275     
276         return [password autorelease];
277 }
278
279 + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {             
280         if (!username || !password || !serviceName) {
281                 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
282                 return NO;
283         }
284         
285         // See if we already have a password entered for these credentials.
286         NSError *checkPasswordError = nil;
287         NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error: &checkPasswordError];
288     
289         if ([checkPasswordError code] == -1999) {
290                 // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
291                 // Delete the existing item before moving on entering a correct one.
292         
293                 NSError *deleteError = nil;
294                 
295                 [self deleteItemForUsername: username andServiceName: serviceName error: &deleteError];
296         
297                 if ([deleteError code] != noErr) {
298             if (error) *error = deleteError;
299                         return NO;
300                 }
301         }
302         else if ([checkPasswordError code] != noErr) {
303         if (error) *error = checkPasswordError;
304                 return NO;
305         }
306         
307         if (error) *error = nil;
308         
309         OSStatus status = noErr;
310     
311         if (existingPassword) {
312                 // We have an existing, properly entered item with a password.
313                 // Update the existing item.
314                 
315                 if (![existingPassword isEqualToString:password] && updateExisting) {
316                         //Only update if we're allowed to update existing.  If not, simply do nothing.
317                         
318                         NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, 
319                                                           kSecAttrService, 
320                                                           kSecAttrLabel, 
321                                                           kSecAttrAccount, 
322                                                           nil] autorelease];
323                         
324                         NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, 
325                                                                  serviceName,
326                                                                  serviceName,
327                                                                  username,
328                                                                  nil] autorelease];
329                         
330                         NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];                      
331                         
332                         status = SecItemUpdate((CFDictionaryRef) query, (CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (NSString *) kSecValueData]);
333                 }
334         }
335         else {
336                 // No existing entry (or an existing, improperly entered, and therefore now
337                 // deleted, entry).  Create a new entry.
338                 
339                 NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, 
340                                                   kSecAttrService, 
341                                                   kSecAttrLabel, 
342                                                   kSecAttrAccount, 
343                                                   kSecValueData, 
344                                                   nil] autorelease];
345                 
346                 NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, 
347                                                          serviceName,
348                                                          serviceName,
349                                                          username,
350                                                          [password dataUsingEncoding: NSUTF8StringEncoding],
351                                                          nil] autorelease];
352                 
353                 NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];                      
354         
355                 status = SecItemAdd((CFDictionaryRef) query, NULL);
356         }
357         
358         if (status != noErr) {
359                 // Something went wrong with adding the new item. Return the Keychain error code.
360                 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
361         return NO;
362         }
363     return YES;
364 }
365
366 + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
367         if (!username || !serviceName) {
368                 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
369                 return NO;
370         }
371         
372         if (error) *error = nil;
373     
374         NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil] autorelease];
375         NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil] autorelease];
376         
377         NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
378         
379         OSStatus status = SecItemDelete((CFDictionaryRef) query);
380         
381         if (status != noErr) {
382                 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
383         return NO;
384         }
385     return YES;
386 }
387
388 #endif
389
390 @end