Last active
November 28, 2016 10:26
-
-
Save superwills/6936380 to your computer and use it in GitHub Desktop.
Call `testKeychain()` from application `didFinishLaunchingWithOptions` or something to run this test code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bool SecCheck( OSStatus res, const char* msg ) | |
{ | |
if( res==errSecSuccess ) | |
{ | |
printf( "< %s okie dokie >\n", msg ) ; // COMMENT THIS OUT TO SILENCE OK's | |
} | |
else | |
{ | |
printf( "< NOT OK!! >: %s FAILED:\n >> ", msg ) ; | |
switch( res ) | |
{ | |
case errSecUnimplemented: | |
puts( "errSecUnimplemented: Function or operation not implemented." ) ; break; | |
case errSecParam: | |
puts( "errSecParam: One or more parameters passed to a function where not valid." ) ; break; | |
case errSecAllocate: | |
puts( "errSecAllocate: Failed to allocate memory." ) ; break; | |
case errSecNotAvailable: | |
puts( "errSecNotAvailable: No keychain is available. You may need to restart your computer." ) ; break; | |
case errSecDuplicateItem: | |
puts( "errSecDuplicateItem: The specified item already exists in the keychain." ) ; break; | |
case errSecItemNotFound: | |
puts( "errSecItemNotFound: The specified item could not be found in the keychain." ) ; break; | |
case errSecInteractionNotAllowed: | |
puts( "errSecInteractionNotAllowed: User interaction is not allowed." ) ; break; | |
case errSecDecode: | |
puts( "errSecDecode: Unable to decode the provided data." ) ; break; | |
case errSecAuthFailed: | |
puts( "errSecAuthFailed: The user name or passphrase you entered is not correct." ) ; break; | |
default: | |
puts( "UNDEFINED ERROR" ) ; break; | |
} | |
} | |
return res == errSecSuccess ; | |
} | |
// Sample binary data structure, just for serialization example usage | |
struct BinaryData { | |
int v1,v2,v3 ; | |
double r1,r2,r3 ; | |
char data[80]; // NOTICE I ONLY USE PRIMITIVE TYPES | |
// FOR EASY SERIALIZATION (ie no std::string for the string data) | |
// just fill with sample data | |
BinaryData() { | |
v1=1,v2=2,v3=3,r1=70.5,r2=70.6,r3=2000.7 ; | |
sprintf( data, "Hi! I am sample data." ) ; | |
} | |
// unserialize by copying binary data directly | |
BinaryData( const UInt8* dat ) | |
{ | |
memcpy( this, dat, sizeof( BinaryData ) ) ; | |
} | |
void print() const | |
{ | |
printf( "%d,%d,%d,%f,%f,%f,%s\n", v1,v2,v3, r1,r2,r3, data ) ; | |
} | |
} ; | |
// The base ctor is too large. | |
CFMutableDictionaryRef CFMutableDictionaryCreateEmpty() | |
{ | |
return CFDictionaryCreateMutable( 0, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ) ; | |
} | |
void testKeychain() | |
{ | |
puts( "Testing keychain.." ) ; | |
// The important thing to note is that everything you | |
// store in keychain, __GETS TREATED AS__ A CRYPTOGRAPHIC KEY OR PASSWORD (ie | |
// it will be encrypted securely). Don't be thrown off by the names, you can store | |
// any data you want in a "generic password" entry in keychain -- Keychain doesn't care, | |
// hence the term "generic". | |
// | |
// In this example we will serialize a binary data structure for encryption into Keychain. | |
// | |
// HOW KEYCHAIN WORKS & "QUERIES": | |
// ------------------------------ | |
// Keychain is an odd sort of encrypted ORM. You set up dictionaries with key-value pairs | |
// describing the key-value pair you want to retrieve (or put) into Keychain, then | |
// you call 1 of 4 simple functions: 1) `SecItemAdd`, 2) `SecItemCopyMatching` 3) SecItemUpdate | |
// 4) SecItemDelete. These correspond to the SQL simple CRUD operations (Create, Read, Update, Delete). | |
// Super simple eh? | |
// | |
// So ORM stands for object-relational mapping. | |
// ORM is a relational database you query not using SQL, but a kind of object-oriented sort of interface. | |
// Actually I'd call keychain FRM (functional-relational mapping?) because you setup DICTIONARIES | |
// full of properties, (which are objects), but then you call one of the 4 | |
// global functions listed above on those dictionaries to retrieve or put values into Keychain. | |
// The BEST SOURCE for documentation on SecKey* is | |
// https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/Reference/reference.html | |
// The `GenericKeychain` sample and the `SecItemWrapper` class included with it are _not good_ | |
// because vital details are missing from the sample. There are some very important properties | |
// to know about with SecKey* -- that sample does not describe. | |
// So, here we'll talk about storing GENERIC "PASSWORD" DATA into keychain. | |
// remember, "PASSWORD" here doesn't mean you're storing a "PASSWORD" perse, | |
// it just means the data will be treated as securely AS IF it were a password | |
// (ie it will be encrypted). Encryption is a good thing to keep your data | |
// from being hacked or read without authorization. | |
// You can't put everything in keychain, but its nice to be able to store vital info securely in keychain. | |
// Let us start with a simple, inline example. A class exists in `struct Keychain` in Keychain.h | |
// that wraps this up for you, but an inline example here for the basics. | |
// First, open https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/Reference/reference.html | |
// in your browser. That page is your friend. | |
// Remember, YOU ARE WRITING SQL QUERIES WITH OBJECTS HERE | |
// (by populating a dictionary and passing it to a SecKey* function). | |
// So, you need to populate the dictionary WITH ENOUGH INFORMATION | |
// that the SQL query will return an unambiguous result. | |
// | |
// One important property of SQL queries is the concept of PRIMARY KEY. | |
// KEY columns (or a combination of columns marked as KEYS) in a relational database uniquely | |
// identify rows of data. So, in the 2 rows of data: | |
// | |
// | First | Last | Birthdate | SSN | | |
// | Fazwall | Mehrdad | 10/22/1999 | 871 999 999 | | |
// | Wazfall | Sehrbad | 11/23/1992 | 999 811 815 | | |
// | |
// if you use `First` as the KEY, then insertion of another row | |
// with `First`name "Fazwall" WILL COLLIDE with the existing entry. | |
// Calls to SecItemAdd WILL FAIL with "errSecDuplicateItem: The specified item already exists in the keychain." | |
// | |
// So, this is important. The different TYPES of data stored in keychain | |
// USE DIFFERENT KEY ATTRIBUTES PAIRS TO UNIQUELY IDENTIFY ROWS OF DATA | |
// IN THE UNDERLYING SQLLITE DATABASE. | |
// | |
// If you are confused, that's OK you can still use Keychain, but it's better | |
// if you understand the above. | |
// | |
// So now let's store some general data for our app inside a GENERIC PASSWORD | |
// type entry in Keychain. | |
// First we create an empty dictionary: | |
CFMutableDictionaryRef keyLookup = CFMutableDictionaryCreateEmpty() ; | |
// `keyLookup` IS GOING TO BE OUR SQL QUERY, pretty much. | |
// Now we tell Keychain that the `kSecClass`, or TYPE OF DATA we want to talk about | |
// inside keychain is just a `kSecClassGenericPassword`: | |
CFDictionaryAddValue( keyLookup, kSecClass, kSecClassGenericPassword ) ; // KEYCHAIN! We are talking about | |
// just some GENERIC DATA we want to store in keychain. | |
// | |
// DOCS: "The class key ("Item Class Key Constant") and a class value constant ("Item Class Value Constants"), | |
// which specifies the class of items for which to search." | |
// There are 5 types of allowable data inside keychain, including kSecClassKey (cryptographic keys) | |
// kSecClassCertificate (cryptographic certificates), and a couple of others. See docs if interested in more on that. | |
// | |
// The TYPE however, IS IMPORTANT, because different TYPES of kSecClass | |
// ALLOW DIFFERENT PROPERTIES TO BE GET OR SET. kSecClass is like a static type, | |
// and the "attributes" are like the member variables defined for that type. For example, the | |
// `kSecAttrGeneric` attribute is ONLY available for `kSecClassGenericPassword` | |
// types. | |
// | |
// The kSecClass is PROBABLY a table name, and other properties are PROBABLY | |
// column names in the underlying database. | |
// | |
// | |
// Now, SINCE WE SET kSecClassGenericPassword the KEY columns are going to be: | |
// kSecAttrAccount and kSecAttrService. I just saved you about oh 4 hours. You're welcome. | |
// See http://useyourloaf.com/blog/2010/04/28/keychain-duplicate-item-when-adding-password.html | |
// http://stackoverflow.com/questions/11614047/what-makes-a-keychain-item-unique-in-ios/11672200 | |
// But really, kSecAttrAccount and kSecAttrService ARE GOING TO ALLOW YOU TO | |
// UNIQUELY IDENTIFY different entries you put into KEYCHAIN. | |
// If you DO NOT SET ONE OF `kSecAttrAccount` OR `kSecAttrService`, | |
// YOU WILL GET AN ERROR. The error will be of type | |
// | |
// > errSecDuplicateItem: The specified item already exists in the keychain. | |
// | |
// THIS IS ACTUALLY THE WRONG ERROR -- the error should say "ERROR, KEY COLUMN NOT SET", | |
// but the SecKey API has no such error flag. The real deal is YOU DID NOT SPECIFY | |
// A KEY -- AND DATABASES REQUIRE THE KEY COLUMNS TO BE SET. | |
// The error means the database could not uniquely identify the row. | |
// This situation can be totally confusing if you don't know about it, | |
// because you will neither be able to FIND (errSecItemNotFound) nor | |
// insert (errSecDuplicateItem) your data IF YOU DON'T SET THE KEY COLUMNS. | |
// I create a UNIQUE string for the "account name". Truthfully these look to be | |
// misnomers to me or you feel like you're using a system intended for one thing, | |
// repurposed for something else. | |
CFStringRef cfAccount = CFStringCreateWithCString( NULL, "superwills-x770", kCFStringEncodingMacRoman ) ; | |
// kSecAttrAccount or kSecAttrService are REQUIRED to make the thing unique. | |
CFDictionaryAddValue( keyLookup, kSecAttrAccount, cfAccount ) ; // uniquely identify the row. MANDATORY to set either this kSecAttrAccount key or kSecAttrService. | |
CFRelease( cfAccount ) ; | |
// So first, here's how you DELETE, since it's the easiest. | |
// Since we just specified the kSecAttrAccount, we have uniquely identified the row | |
// (leaving kSecAttrService empty). Now call SecItemDelete, which'll | |
// delete the Sec item corresponding with this cfAccount. | |
// I did this here to avoid duplicate insert below for multiple runs of this program. | |
SecCheck( SecItemDelete( keyLookup ), "SecItemDelete" ) ; | |
// Now I'll add a couple of other properties to my `keyLookup` query, | |
CFStringRef cfDescr = CFStringCreateWithCString( NULL, "Description of the account.", kCFStringEncodingMacRoman ) ; | |
CFDictionaryAddValue( keyLookup, kSecAttrDescription, cfDescr ) ; // Set the description. OPTIONAL | |
CFRelease( cfDescr ) ; | |
// The docs call what we discussed above *** Attribute Item Keys and Values ***. | |
// DOCS: "You use keys in a search dictionary to specify the keychain items for which to search. | |
// You can specify a combination of item attributes and search attributes (see "Search Keys") | |
// when looking for matching items with the SecItemCopyMatching function." | |
// | |
// ALL THE OTHER DATA FIELDS ARE OPTIONAL!! YOU DON'T HAVE TO SET THEM!!! THEY JUST STORE YOUR DATA!! | |
// ALLOWABLE COLUMNS (KEY-VALUE PAIRS) AND THEIR TYPES FOR kSecClassGenericPassword ARE | |
// kSecAttrAccessible (CFTypeRef), kSecAttrAccessGroup (CFStringRef), | |
// kSecAttrCreationDate (CFDateRef) (READ ONLY), kSecAttrModificationDate (CFDateRef) (READ ONLY), | |
// kSecAttrDescription (CFStringRef), kSecAttrComment (CFStringRef), | |
// kSecAttrCreator (CFNumberRef), kSecAttrType (CFNumberRef), kSecAttrLabel (CFStringRef), | |
// kSecAttrIsInvisible (CFBooleanRef), kSecAttrIsNegative (CFBooleanRef), | |
// kSecAttrAccount (*KEY) (CFStringRef), kSecAttrService (*KEY) (CFStringRef), | |
// kSecAttrGeneric (CFDataRef), | |
// | |
// *YOU MUST SET AT LEAST ONE KEY WITH A UNIQUE VALUE THAT DOESN'T OCCUR | |
// MORE THAN ONCE IN KEYCHAIN FOR YOUR APP! | |
// kSecAttrGeneric is just a BINARY BLOB of information you want to store. | |
// ^^ THIS IS ALL YOU NEED TO KNOW! REALLY! ^^ | |
// YOU MAY ALSO SET: kSecValueData (CFDataRef) __I BELIEVE__ for any | |
// kSecClass type. | |
// Before searching, WE MUST TELL keychain HOW we want our data. | |
// there are a couple of options here, but kSecReturnAttributes | |
// just gives you a dictionary (in the same form that you pass | |
// to SecItemCopyMatching() ) and seems to work easiest | |
// DOCS: "A return-type key-value pair ("Search Results Constants"), specifying the type of results you desire." | |
CFDictionaryAddValue( keyLookup, kSecReturnAttributes, kCFBooleanTrue ) ; // makes it return a DICTIONARY of | |
// key value pairs (same format as when you set them to make your db query) | |
CFMutableDictionaryRef dataFromKeychain ; | |
OSStatus res = SecItemCopyMatching( keyLookup, (CFTypeRef *)&dataFromKeychain ) ; | |
// EXPECTED: res==`errSecItemNotFound` for times when you query for a key that doesn't exist yet | |
if( res == noErr ) | |
{ | |
puts( "I FOUND WHAT YOU WERE LOOKING FOR!" ) ; | |
// You get a DICTIONARY back from keychain (because we set kSecReturnAttributes above) | |
CFStringRef cfStr = (CFStringRef)CFDictionaryGetValue( dataFromKeychain, kSecAttrAccount ) ; | |
if( cfStr ) { | |
const char* chrs = CFStringGetCStringPtr( cfStr, kCFStringEncodingMacRoman ) ; | |
printf( "kSecAttrAccount: %s\n", chrs ) ; | |
CFRelease( cfStr ) ; | |
} else puts( "ERR: NO cfStr kSecAttrAccount" ) ; | |
cfStr = (CFStringRef)CFDictionaryGetValue( dataFromKeychain, kSecAttrDescription ) ; | |
if( cfStr ) { | |
const char* chrs = CFStringGetCStringPtr( cfStr, kCFStringEncodingMacRoman ) ; | |
printf( "kSecAttrDescription: %s\n", chrs ) ; | |
CFRelease( cfStr ) ; | |
} else puts( "ERR: NO cfStr kSecAttrDescription" ) ; | |
CFDataRef cfData = (CFDataRef)CFDictionaryGetValue( dataFromKeychain, kSecAttrGeneric ) ; | |
const UInt8* dat = CFDataGetBytePtr( cfData ) ; // take the raw byte pointer, | |
// and unserialize by copying the binary data into a c struct | |
// NOTE THIS IS ONLY GUARANTEED TO WORK FOR PRIMITIVE TYPES INSIDE THE STRUCT | |
// RESULTS UNPREDICTABLE IF OBJECT HAS VTABLE OR YOU USE NON-PRIMITIVE TYPES INSIDE THE STRUCT. | |
if( cfData ) { | |
BinaryData bd( dat ) ; | |
puts( "Reconstituted data from kSecAttrGeneric" ) ; | |
bd.print() ; | |
CFRelease( cfData ) ; | |
} else puts( "ERR: NO CFDATA" ) ; | |
} | |
else if( res == errSecItemNotFound ) | |
{ | |
puts( "Key didn't exist yet, creating," ) ; | |
BinaryData bd ; // ctor inits w/ sample data | |
CFDataRef cfData = CFDataCreate( 0, (const UInt8*)&bd, sizeof(BinaryData) ) ; | |
// store binary data (any CFDataRef) in kSecAttrGeneric | |
// for a kSecClassGenericPassword type keychain entry | |
CFDictionaryAddValue( keyLookup, kSecAttrGeneric, cfData ) ;// the actual data we want to store. | |
// ********* IMPORTANT: TURN OFF RETRIEVAL BEFORE INSERT ********** | |
// SecItemAdd will crash if you try to RETRIEVE+ADD at the same time & pass NULL | |
// as the 2nd arg to SecItemAdd() ( http://stackoverflow.com/questions/10038885/ ). | |
CFDictionarySetValue( keyLookup, kSecReturnAttributes, kCFBooleanFalse ) ; // DO NOT | |
// RETURN ME ANYTHING WHILE I AM TRYING TO SecItemAdd(), because I chose not to catch the result. | |
SecCheck( SecItemAdd( keyLookup, NULL ), "SecItemAdd" ) ; | |
//SecCheck( SecItemAdd( keyLookup, NULL ), "SecItemAdd" ) ; // uncomment to see DUPLICATE INSERT ERROR | |
CFRelease( cfData ) ; | |
} | |
else | |
{ | |
// OTHER problem querying for key that we cannot handle, so print the error | |
SecCheck( res, "Query for key" ) ; | |
} | |
CFRelease( keyLookup ) ; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment