Skip to content

Instantly share code, notes, and snippets.

@superwills
Last active November 28, 2016 10:26
Show Gist options
  • Save superwills/6936380 to your computer and use it in GitHub Desktop.
Save superwills/6936380 to your computer and use it in GitHub Desktop.
Call `testKeychain()` from application `didFinishLaunchingWithOptions` or something to run this test code.
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