I´m developing a couple of iOS applications and i need to share a element between them, that i want to store in the keychain.
This element is used in a complex login process with 3 or 4 steps, in each one i need to read the value from the keychain, to do this i used the code bellow:
- (NSString *)installationToken
{
KeychainItemWrapper *kw = [[KeychainItemWrapper alloc] initWithIdentifier:#"uuid" accessGroup:#"yyyyy.xxxxxxxxxxx"];
if (![kw objectForKey:(NSString*)kSecAttrAccount] || [[kw objectForKey:(NSString*)kSecAttrAccount] isEqualToString:#""]) {
NSString *result;
CFUUIDRef uuid;
CFStringRef uuidStr;
uuid = CFUUIDCreate(NULL);
assert(uuid != NULL);
uuidStr = CFUUIDCreateString(NULL, uuid);
assert(uuidStr != NULL);
result = [NSString stringWithFormat:#"%#", uuidStr];
assert(result != nil);
CFRelease(uuidStr);
CFRelease(uuid);
[kw setObject:result forKey:(NSString*)kSecAttrAccount];
return result;
} else {
return [kw objectForKey:(NSString*)kSecAttrAccount];
}
}
This all works well in almost every device but in some, users are complaining. So, i checked what my server is receiving, and saw that different values are being sent.
I checked the code and in no other place i'm acessing/emptying this keychain element, what can be wrong with this? For the majority of devices this works like a charm but for some reason, in some devices, they aren't storing or retrieving well from the keychain.
The problem happens in different invocation in the same application.
If you are using Apples' sample code for KeyChainWrapper, then main problem is sometimes randomly, SecItemCopyMatching fails and then the sample code has resetKeychainItem which will basically reset your keychain.
if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)
{
// Stick these default values into Keychain if nothing found.
[self resetKeychainItem];
}
In our app, we noticed similar problems, and so now we are using
https://github.com/carlbrown/PDKeychainBindingsController to do all keyChain related functionality. Now it works very well.
Related
How can I access the Bundle Seed ID/Team ID/App Identifier Prefix string programmatically? (These are all the same thing as far as I can tell).
I am using the UICKeychainStore keychain wrapper to persist data across several applications. Each of these applications has a shared keychain access group in their entitlement plists, and share the same provisioning profile. By default, the keychain services use the first access group in the plist as the access group to save data to. This looks like "AS234SDG.com.myCompany.SpecificApp" when I debug UICKeychainStore. I would like to set the access group to "AS234SDG.com.myCompany.SharedStuff", but I can't seem to locate how to get the "AS234SDG" string of the access group programmatically, and would like to avoid hard-coding it if possible.
Info.plist can have your own information and if you write a value with $(AppIdentifierPrefix), it is replaced to the real app identifier prefix at building phase.
So, try this:
In your Info.plist, add an info about app identifier prefix.
<key>AppIdentifierPrefix</key>
<string>$(AppIdentifierPrefix)</string>
You can then retrieve it programmatically with Objective-C:
NSString *appIdentifierPrefix =
[[NSBundle mainBundle] objectForInfoDictionaryKey:#"AppIdentifierPrefix"];
and with Swift:
let appIdentifierPrefix =
Bundle.main.infoDictionary!["AppIdentifierPrefix"] as! String
Note that appIdentifierPrefix ends with a period; e.g. AS234SDG.
You can programmatically retrieve the Bundle Seed ID by looking at the access group attribute (i.e. kSecAttrAccessGroup) of an existing KeyChain item. In the code below, I look up for an existing KeyChain entry and create one if it doesn't not exist. Once I have a KeyChain entry, I extract the access group information from it and return the access group's first component separated by "." (period) as the Bundle Seed ID.
+ (NSString *)bundleSeedID {
NSString *tempAccountName = #"bundleSeedID";
NSDictionary *query = #{
(__bridge NSString *)kSecClass : (__bridge NSString *)kSecClassGenericPassword,
(__bridge NSString *)kSecAttrAccount : tempAccountName,
(__bridge NSString *)kSecAttrService : #"",
(__bridge NSString *)kSecReturnAttributes: (__bridge NSNumber *)kCFBooleanTrue,
};
CFDictionaryRef result = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
if (status == errSecItemNotFound)
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
if (status != errSecSuccess) {
return nil;
}
status = SecItemDelete((__bridge CFDictionaryRef)query); // remove temp item
NSDictionary *dict = (__bridge_transfer NSDictionary *)result;
NSString *accessGroup = dict[(__bridge NSString *)kSecAttrAccessGroup];
NSArray *components = [accessGroup componentsSeparatedByString:#"."];
NSString *bundleSeedID = [[components objectEnumerator] nextObject];
return bundleSeedID;
}
Here is the Swift version of #David H answer:
static func bundleSeedID() -> String? {
let queryLoad: [String: AnyObject] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "bundleSeedID" as AnyObject,
kSecAttrService as String: "" as AnyObject,
kSecReturnAttributes as String: kCFBooleanTrue
]
var result : AnyObject?
var status = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(queryLoad as CFDictionary, UnsafeMutablePointer($0))
}
if status == errSecItemNotFound {
status = withUnsafeMutablePointer(to: &result) {
SecItemAdd(queryLoad as CFDictionary, UnsafeMutablePointer($0))
}
}
if status == noErr {
if let resultDict = result as? [String: Any], let accessGroup = resultDict[kSecAttrAccessGroup as String] as? String {
let components = accessGroup.components(separatedBy: ".")
return components.first
}else {
return nil
}
} else {
print("Error getting bundleSeedID to Keychain")
return nil
}
}
This is a good question but to achieve what you were intended to do, there could have been a solution
that does not require to retrieve the Bundle Seed ID.
From this article, about the same keychain wrapper you're using:
By default it will pick the first access-group specified in your
Entitlements.plist when writing and will search across all
access-groups when none is specified.
The key will then be search in all groups where access is granted.
So to solve your problem, you could add access group of all your bundle apps into your entitlements.plist instead of using a "shared stuff" group, put $(CFBundleIdentifier) as your first keychain group (your keychain wrapper will then write in this group) and you're all set
If you search in Xcode on your team's ID then you will see that this value is hosted in the build settings under the key DEVELOPMENT_TEAM.
You can retrieve this key by putting in your Info.plist file:
<key>DEVELOPMENT_TEAM</key>
<string>$(DEVELOPMENT_TEAM)</string>
Make sure to put this in every target's Info.plist file where you want to retrieve it using this code:
let teamID = Bundle.main.infoDictionary!["DEVELOPMENT_TEAM"] as! String
This solution will give you the team ID without the dot suffix.
The solution in https://stackoverflow.com/a/28714850/2743633 worked for me only to get the team ID from the main app target. It would not retrieve the team ID when doing the same for a Share Extension target.
The documentation on SecPKCS12Import states the following:
[…] You can then use the Keychain Services API (see Keychain Services
Reference) to put the identities and associated certificates in the
keychain.
This means that the items returned in the “items” argument (3rd argument of that function) should not be automatically added to the keychain. However, I have found that those items are automatically added to the keychain when using that function. If I try to add them using SecItemAdd, I get errSecDuplicateItem.
Is this a bug or should it be this way? Why are the items automatically added?
Here is some sample code:
NSDictionary *options = [[NSDictionary alloc] initWithObjectsAndKeys:#"password", (id)kSecImportExportPassphrase, nil];
CFArrayRef items_ = NULL;
OSStatus ret = SecPKCS12Import((CFDataRef)pkcs12data /* get this from somewhere … */, (CFDictionaryRef)options, &items_);
If you use that code and then open Keychain Access, you’ll see that the certificate and the private key have been added to the keychain.
Regards,
David.
It seems like Apple's documentation may be out of date for that link (SecPKCS12Import), because this link https://developer.apple.com/library/ios/qa/qa1745/_index.html mentions that "reading in a PKCS#12-formatted blob and then importing the contents of the blob into the app's keychain using the function SecPKCS12Import..."
Going by the document revision dates, QA1745 is more recent than the Certificate, Key, and Trust Services Reference.
Is this a bug or should it be this way?
It isn't a bug, the documentation is only incorrect. Well, it is correct for iOS, it is just incorrect for macOS.
Why are the items automatically added?
This is caused by the way how this function is implemented in macOS. The comments in the implementation reveal the cause:
// SecPKCS12Import is implemented on Mac OS X in terms of the existing
// SecKeychainItemImport API, which supports importing items into a
// specified keychain with initial access control settings for keys.
SecPKCS12Import in macOS is just a wrapper around SecKeychainItemImport and as the function name implies, this function imports into a keychain. This also explains the following code:
if (!importKeychain) {
// SecKeychainItemImport requires a keychain, so use default
status = SecKeychainCopyDefault(&importKeychain);
}
On iOS the function is implemented standalone and not as a wrapper since SecKeychainItemImport isn't even available on iOS.
But there is a way round that if that is a problem for you. Many people work around it by creating a temporary keychain, which will never be visible to the system or the user (and thus also not be visible to apps or in Keychain Access), of course, this will work too, but is a bit of an ugly hack.
Better: Use SecItemImport and as import format use kSecFormatPKCS12, then you also get the parsed identities but nothing is imported anywhere unless you request that.
SecPKCS12Import does NOT add items to the keychain. However, it WILL look in the keychain to see if imported items are already there. If it finds existing items, they will be returned for the SecIdentityRef (SecCertificateRef and SecKeyRef). This is why you can get errSecDuplicateItem when calling SecItemAdd after calling SecPKCS12Import.
When debugging, you might want to remove everything in your keychain using code like this:
void _EraseKeychain()
{
NSMutableArray *toDelete = [NSMutableArray array];
NSArray *classes = #[(__bridge id)kSecClassCertificate,
(__bridge id)kSecClassKey,
(__bridge id)kSecClassIdentity,
(__bridge id)kSecClassInternetPassword,
(__bridge id)kSecClassGenericPassword];
NSMutableDictionary *query = [NSMutableDictionary dictionary];
query[(id)kSecClass] = (__bridge id)kSecClassIdentity;
query[(id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
query[(id)kSecReturnPersistentRef] = #YES;
id class;
for( class in classes )
{
query[(__bridge id)kSecClass] = class;
CFTypeRef items = nil;
OSStatus result = SecItemCopyMatching((__bridge CFDictionaryRef)query, &items);
if( result == errSecSuccess )
{
[toDelete addObjectsFromArray:(__bridge NSArray*)items];
CFRelease(items);
}
}
id deleteRef;
for( deleteRef in toDelete )
{
NSString *objectKind = #"unknown";
if( CFGetTypeID(deleteRef) == CFDataGetTypeID() )
{
objectKind = [[NSString alloc] initWithUTF8String:(char *)[(__bridge NSData*)deleteRef bytes]];
}
NSDictionary *delRequest = #{(id)kSecValuePersistentRef:deleteRef};
OSStatus deleteResult = SecItemDelete((__bridge CFDictionaryRef)delRequest);
if( deleteResult == errSecSuccess )
NSLog(#"Deleted item(%#) with persistent ref %#", objectKind, deleteRef);
else if( deleteResult == errSecItemNotFound )
NSLog(#"Already deleted item(%#) with persistent ref %#", objectKind, deleteRef);
else
NSLog(#"Can't delete keychain item(%#) with persistent ref %#", objectKind, deleteRef);
}
}
I want to add some data in the Keychain in iOS. I am not worried about the security just wanted to store some string permanently some where(Keychain) which can be consistent even if user uninstalled the application. I am not storing any password, all the example in the Web are only showing how to store the password. I am planning to use the kSecClassKey attribute to store the string. Please guide me in the correct direction. Any sample code will be very helpful.
I've used SFHF Keychain lib ( https://github.com/kamiro/SFHFKeychainUtils ). It's really easy to use and works fine.
Here an example to use
NSString* username = #"myValue1";
NSString* service = #"com.organization.appname.myValue1";
NSString* valueToStore = #"....";
// Add/Update valute to keychain
NSError* anyStoringError = NULL;
BOOL stored = [SFHFKeychainUtils storeUsername:username andPassword:valueToStore forServiceName:service updateExisting:YES error:&anyStoringError];
// Get value from keychain
NSString *storedValue = [SFHFKeychainUtils getPasswordForUsername:username andServiceName:service error:&anyStoringError];
// Remove value from keychain
BOOL valueRemoved = [SFHFKeychainUtils deleteItemForUsername:username andServiceName:service error:&anyStoringError];
Hope it helps
I tried to use SSKeychain to reserve UUID on iOS
and the sample code is below
NSString *retrieveuuid = [SSKeychain passwordForService:#"tempApp" account:#"tempUser"];
if (retrieveuuid == nil) {
//Generate UUID
CFUUIDRef cfuud = CFUUIDCreate(kCFAllocatorDefault);
NSString *uuid = CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));
//save in keychain
[SSKeychain setPassword:uuid forService:#"tempApp" account:#"tempUser"];
return uuid;
} else {
return retrieveuuid;
}
My question is that I run the first app and then generate a UUID, and then run the second app which
has the same parameters, so that the retrieveduuid should not be null, then why the second App returns a different UUID? I think that will return the same
UUID as first app because I have saved the UUID in the keychain in App1 and try to retrieve it by the same parameters in App2.
Thanks for help
UUID is a Universally Unique IdenTifier. Would be kind of stupid if you got the same in two different apps, wouldn't it? ;)
If you want to share keychains between apps, see this question: How to share keychain data between iOS applications
My app was rejected cause it must follow the iOS Data Storage Guidelines. I have already read some answer here on stackoverflow, and i have already read some blogs... I know my problem, at first application launch i copy 1 sqlite db and unzip some images in documents folder. The problem it's icloud that automaticcaly backup any files in documents directory. I don't need to use icloud in my app but my files must remain in document folder because they are the base data of my application and must be persit (cache folder or temp folder aren't correct solutions). So i've read about a flag that i can set file by file to forbid the backup :
[URL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
I have also read that this method works only in ios higher than 5.0.1, in the other it will be simply ignored (i have setted my ios deployment target to 4.3)... If i use this method my app will be rejected again because the older ios devices backup aren't managed? If yes there is a cross-iosversion method to set the NSURLIsExcludedFromBackupKey?
EDIT
I'm sorry icloud isn't present in ios earlier 5.0 so i think that the problem regards only differences beetween version 5.0 and 5.0.1, I'm wrong?
I have passed all files that will stored in documents folder through this method.
Try this
-(BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
const char* filePath = [[URL path] fileSystemRepresentation];
const char* attrName = "com.apple.MobileBackup";
if (&NSURLIsExcludedFromBackupKey == nil) {
// iOS 5.0.1 and lower
u_int8_t attrValue = 1;
int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
return result == 0;
}
else {
// First try and remove the extended attribute if it is present
int result = getxattr(filePath, attrName, NULL, sizeof(u_int8_t), 0, 0);
if (result != -1) {
// The attribute exists, we need to remove it
int removeResult = removexattr(filePath, attrName, 0);
if (removeResult == 0) {
NSLog(#"Removed extended attribute on file %#", URL);
}
}
// Set the new key
NSError *error = nil;
[URL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error];
return error == nil;
}
}
I've found this method here in Stack Overflow:
Use NSURLIsExcludedFromBackupKey without crashing on iOS 5.0
NOTE:
do not use
NSString *mediaDir = [NSString stringWithFormat:#"%#/Library/Caches/%#", NSHomeDirectory(), MEDIA_DIRECTORY];
since you are making assumptions about the name.
rather, use:
NSArray* lCachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString* lCacheDirectory = [lCachePaths objectAtIndex:0];
In my app I solved it by adding the files (images in my case) to the bundle,
and check in the method where I load the images if they exist in the document folder where the user generated data / images will be placed.
If it isn't there, I know it's one of my prepopulated images and I load it from the bundle.
For the sqlite file:
Not a really satisfying solution, but I create the data in code now, instead of a prepopulated sqlite file.
For media files, we store them in the Caches/ directory. During application startup, we check if they are there. Otherwise the images are either loaded from the bundle (in your case unzipped) or in our case re-downloaded from the Server.
The exact location is
NSString *mediaDir = [NSString stringWithFormat:#"%#/Library/Caches/%#", NSHomeDirectory(), MEDIA_DIRECTORY];
which is the place where you SHOULD store your temporary files. The files in the cache are generally persistent, much more so than in Temp, but can be deleted at some point.