There’re few documents I can find on RubyCocoa Keychain coding. The best one for me is this post: RubyCocoa and Keychain access。
Before start, generate metadata for the Security Framework:
mkdir ~/Library/BridgeSupport gen_bridge_metadata -f Security -o ~/Library/BridgeSupport/Security.bridgesupport
The above post suggests that put the bridgesupport file in /Library/BridgeSupport, but ~/Library/BridgeSupport will also work.
So we need the Security.bridgesupport file to write RubyCocoa code with Keychain, how do we deploy the software? I guess we can do one of the following:
- Ask the users to generate the metadata, which is kinda rude and silly;
- Copy the file to the proper location when users install the app;
- Embed the file in app bundle.
The last one is my favorite option and I’ve found it not very hard to implement. Here’s the code to load the Security Framework and the bridge file:
require 'osx/cocoa' OSX.require_framework 'Security' OSX.load_bridge_support_file(NSBundle.mainBundle.pathForResource_ofType("Security", "bridgesupport"))
Don’t forget to add the Security.bridgesupport file to Xcode project.
Basically, we only need Keychain to store and retrieve password. To do so, we just need to learn three Cocoa methods:
Use the first one to add a password to Keychain:
SERVICE = "MyKeychainApp" ... error = SecKeychainAddGenericPassword( nil, SERVICE.length, SERVICE, username.length, username, password.length, password, nil)
The first nil parameter specifies that we’re going to use the default Keychain (why not?). SERVICE is a const string I use to represent the application. The last nil parameter tells the method that I don’t need it to return a SecKeychainItemRef object.
Chances are your users might want to change the password later. Calling the method again with a different password, the password stored in the Keychain will not be updated! (password can be showed in the Keychain Access application)
This is because when a Keychain item already exists for a username (Keychain item is identified by service name and username), the method call will fail. The sweet method returns errSecDuplicateItem in such case, then we can use the second method to update password:
error = SecKeychainAddGenericPassword( nil, SERVICE.length, SERVICE, username.length, username, password.length, password, nil) if error == OSX::ErrSecDuplicateItem status, *data = SecKeychainFindGenericPassword( nil, SERVICE.length, SERVICE, username.length, username) password_length = data.shift password_date = data.shift #password data item = data.shift #SecKeychainItemRef SecKeychainItemModifyContent(item, nil, password.length, password) end
SecKeychainFindGenericPassword returns a status code and an array of data, which contains the password length, password data, and a pointer to the object of the password.
Pass this pointer and the new password to the SecKeychainItemModifyContent method to update the password. The nil parameter tells the method we don’t have any attribute to set for the item.
When we use SecKeychainFindGenericPassword to get the password, it returns the password length and the password data (which is an ObjcPtr). Get the string value of the password is easy now:
password = password_data.bytestr(password_length)
Happy RubyCocoa programming!