From 75446cb734c2a6223254f64f290eed4425590d8c Mon Sep 17 00:00:00 2001 From: Joel Arvidsson Date: Fri, 29 May 2015 18:25:56 +0200 Subject: [PATCH] Added generic password support. --- README.md | 33 +++++++-- RNKeychainManager/RNKeychainManager.m | 103 ++++++++++++++++++++++---- index.ios.js | 73 +++++++++++++++++- 3 files changed, 189 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f76b38b..dfdc627 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # react-native-keychain Keychain Access for React Native -Currently functionality is limited to just storing internet passwords. More to come... +Currently functionality is limited to just storing internet and generic passwords. Wider exposure of the underlying API coming. ## Installation @@ -17,25 +17,46 @@ See `KeychainExample` for fully working project example. ```js var Keychain = require('Keychain'); -var server = 'http://facebook.com'; var username = 'zuck'; var password = 'poniesRgr8'; + +// Generic Password, service argument optional +Keychain + .setGenericPassword(username, password) + .then(function() { + console.log('Credentials saved successfully!'); + }); + +Keychain + .getGenericPassword() + .then(function(credentials) { + console.log('Credentials successfully loaded for user ' + credentials.username); + }); + +Keychain + .resetGenericPassword() + .then(function() { + console.log('Credentials successfully deleted'); + }); + +// Internet Password, server argument required +var server = 'http://facebook.com'; Keychain .setInternetCredentials(server, username, password) .then(function() { - console.log('Credentials saved successfully!') + console.log('Credentials saved successfully!'); }); Keychain .getInternetCredentials(server) .then(function(credentials) { - console.log('Credentials successfully loaded', credentials) + console.log('Credentials successfully loaded for user ' + credentials.username); }); Keychain .resetInternetCredentials(server) - .then(function(credentials) { - console.log('Credentials successfully deleted') + .then(function() { + console.log('Credentials successfully deleted'); }); ``` diff --git a/RNKeychainManager/RNKeychainManager.m b/RNKeychainManager/RNKeychainManager.m index e7cd8a5..630c32f 100644 --- a/RNKeychainManager/RNKeychainManager.m +++ b/RNKeychainManager/RNKeychainManager.m @@ -71,17 +71,21 @@ NSDictionary * makeError(NSError *error) } -RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString*)server withUsername:(NSString*)username withPassword:(NSString*)password callback:(RCTResponseSenderBlock)callback){ +RCT_EXPORT_METHOD(setGenericPasswordForService:(NSString*)service withUsername:(NSString*)username withPassword:(NSString*)password callback:(RCTResponseSenderBlock)callback){ + if(service == nil) { + service = [[NSBundle mainBundle] bundleIdentifier]; + } + // Create dictionary of search parameters - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, kCFBooleanTrue, kSecReturnAttributes, nil]; - + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, service, kSecAttrService, kCFBooleanTrue, kSecReturnAttributes, nil]; + // Remove any old values from the keychain OSStatus osStatus = SecItemDelete((__bridge CFDictionaryRef) dict); - + // Create dictionary of parameters to add NSData* passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; - dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, passwordData, kSecValueData, username, kSecAttrAccount, nil]; - + dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, service, kSecAttrService, passwordData, kSecValueData, username, kSecAttrAccount, nil]; + // Try to save to keychain osStatus = SecItemAdd((__bridge CFDictionaryRef) dict, NULL); @@ -89,16 +93,19 @@ RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString*)server withUsername NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; return callback(@[makeError(error)]); } - + callback(@[[NSNull null]]); } -RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString*)server callback:(RCTResponseSenderBlock)callback){ - +RCT_EXPORT_METHOD(getGenericPasswordForService:(NSString*)service callback:(RCTResponseSenderBlock)callback){ + if(service == nil) { + service = [[NSBundle mainBundle] bundleIdentifier]; + } + // Create dictionary of search parameters - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, kCFBooleanTrue, kSecReturnAttributes, kCFBooleanTrue, kSecReturnData, nil]; - + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, service, kSecAttrService, kCFBooleanTrue, kSecReturnAttributes, kCFBooleanTrue, kSecReturnData, nil]; + // Look up server in the keychain NSDictionary* found = nil; CFTypeRef foundTypeRef = NULL; @@ -119,7 +126,77 @@ RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString*)server callback:(RC NSString* password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; callback(@[[NSNull null], username, password]); - + +} + +RCT_EXPORT_METHOD(resetGenericPasswordForService:(NSString*)service callback:(RCTResponseSenderBlock)callback){ + if(service == nil) { + service = [[NSBundle mainBundle] bundleIdentifier]; + } + + // Create dictionary of search parameters + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, service, kSecAttrService, kCFBooleanTrue, kSecReturnAttributes, kCFBooleanTrue, kSecReturnData, nil]; + + // Remove any old values from the keychain + OSStatus osStatus = SecItemDelete((__bridge CFDictionaryRef) dict); + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return callback(@[makeError(error)]); + } + + callback(@[[NSNull null]]); + +} + +RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString*)server withUsername:(NSString*)username withPassword:(NSString*)password callback:(RCTResponseSenderBlock)callback){ + // Create dictionary of search parameters + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, kCFBooleanTrue, kSecReturnAttributes, nil]; + + // Remove any old values from the keychain + OSStatus osStatus = SecItemDelete((__bridge CFDictionaryRef) dict); + + // Create dictionary of parameters to add + NSData* passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; + dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, passwordData, kSecValueData, username, kSecAttrAccount, nil]; + + // Try to save to keychain + osStatus = SecItemAdd((__bridge CFDictionaryRef) dict, NULL); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return callback(@[makeError(error)]); + } + + callback(@[[NSNull null]]); + +} + +RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString*)server callback:(RCTResponseSenderBlock)callback){ + + // Create dictionary of search parameters + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, kCFBooleanTrue, kSecReturnAttributes, kCFBooleanTrue, kSecReturnData, nil]; + + // Look up server in the keychain + NSDictionary* found = nil; + CFTypeRef foundTypeRef = NULL; + OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) dict, (CFTypeRef*)&foundTypeRef); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return callback(@[makeError(error)]); + } + + found = (__bridge NSDictionary*)(foundTypeRef); + if (!found) { + return callback(@[[NSNull null]]); + } + + // Found + NSString* username = (NSString*) [found objectForKey:(__bridge id)(kSecAttrAccount)]; + NSString* password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; + + callback(@[[NSNull null], username, password]); + } RCT_EXPORT_METHOD(resetInternetCredentialsForServer:(NSString*)server callback:(RCTResponseSenderBlock)callback){ @@ -133,7 +210,7 @@ RCT_EXPORT_METHOD(resetInternetCredentialsForServer:(NSString*)server callback:( NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; return callback(@[makeError(error)]); } - + callback(@[[NSNull null]]); } diff --git a/index.ios.js b/index.ios.js index cd8cf1a..efb4361 100644 --- a/index.ios.js +++ b/index.ios.js @@ -56,7 +56,8 @@ var Keychain = { }, /** - * Deletes all keychain entries for `server` and calls `callback` with an `Error` if there is any. + * Deletes all internet password keychain entries for `server` and calls `callback` with an + * `Error` if there is any. * Returns a `Promise` object. */ resetInternetCredentials: function( @@ -75,6 +76,76 @@ var Keychain = { }); }, + /** + * Saves the `username` and `password` combination for `service` (defaults to `bundleId`) + * and calls `callback` with an `Error` if there is any. + * Returns a `Promise` object. + */ + setGenericPassword: function( + username: string, + password: string, + service?: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RNKeychainManager.setGenericPasswordForService(service, username, password, function(err) { + callback && callback((err && convertError(err)) || null); + if (err) { + reject(convertError(err)); + } else { + resolve(); + } + }); + }); + }, + + /** + * Fetches login combination for `service` (defaults to `bundleId`) as an object with the format + * `{ username, password }` and passes the result to `callback`, along with an `Error` if + * there is any. + * Returns a `Promise` object. + */ + getGenericPassword: function( + service?: string, + callback?: ?(error: ?Error, result: ?string) => void + ): Promise { + return new Promise((resolve, reject) => { + RNKeychainManager.getGenericPasswordForService(service, function(err, username, password) { + err = convertError(err); + if(!err && arguments.length === 1) { + err = new Error('No keychain entry found' + (service ? ' for service "' + service + '"' : '')); + } + callback && callback((err && convertError(err)) || null, username, password); + if (err) { + reject(convertError(err)); + } else { + resolve({ username, password }); + } + }); + }); + }, + + /** + * Deletes all generic password keychain entries for `service` (defaults to `bundleId`) and calls + * `callback` with an `Error` if there is any. + * Returns a `Promise` object. + */ + resetGenericPassword: function( + service?: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RNKeychainManager.resetGenericPasswordForService(service, function(err) { + callback && callback((err && convertError(err)) || null); + if (err) { + reject(convertError(err)); + } else { + resolve(); + } + }); + }); + }, + }; function convertError(err) {