diff --git a/README.md b/README.md index 95160b6..45fcaf3 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,9 @@ Keychain Keychain .getInternetCredentials(server) .then(function(credentials) { - console.log('Credentials successfully loaded for user ' + credentials.username); + if (credentials) { + console.log('Credentials successfully loaded for user ' + credentials.username); + } }); Keychain diff --git a/RNKeychainManager/RNKeychainManager.m b/RNKeychainManager/RNKeychainManager.m index fdee2a7..69210c4 100644 --- a/RNKeychainManager/RNKeychainManager.m +++ b/RNKeychainManager/RNKeychainManager.m @@ -76,41 +76,47 @@ void rejectWithError(RCTPromiseRejectBlock reject, NSError *error) return reject(codeForError(error), messageForError(error), nil); } -CFStringRef accessibleValue(NSString *jsAccessibleKey) +CFStringRef accessibleValue(NSDictionary *options) { - if (jsAccessibleKey) { - NSDictionary *keyMap = @{ - @"AccessibleWhenUnlocked": (__bridge NSString *)kSecAttrAccessibleWhenUnlocked, - @"AccessibleAfterFirstUnlock": (__bridge NSString *)kSecAttrAccessibleAfterFirstUnlock, - @"AccessibleAlways": (__bridge NSString *)kSecAttrAccessibleAlways, - @"AccessibleWhenPasscodeSetThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, - @"AccessibleWhenUnlockedThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - @"AccessibleAfterFirstUnlockThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, - @"AccessibleAlwaysThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleAlwaysThisDeviceOnly - }; - NSString *result = [keyMap valueForKey:jsAccessibleKey]; - if (result) { - return (__bridge CFStringRef)result; - - } + if (options && options[@"accessible"] != nil) { + NSDictionary *keyMap = @{ + @"AccessibleWhenUnlocked": (__bridge NSString *)kSecAttrAccessibleWhenUnlocked, + @"AccessibleAfterFirstUnlock": (__bridge NSString *)kSecAttrAccessibleAfterFirstUnlock, + @"AccessibleAlways": (__bridge NSString *)kSecAttrAccessibleAlways, + @"AccessibleWhenPasscodeSetThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + @"AccessibleWhenUnlockedThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + @"AccessibleAfterFirstUnlockThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + @"AccessibleAlwaysThisDeviceOnly": (__bridge NSString *)kSecAttrAccessibleAlwaysThisDeviceOnly + }; + NSString *result = keyMap[options[@"accessible"]]; + if (result) { + return (__bridge CFStringRef)result; } - return kSecAttrAccessibleAfterFirstUnlock; + } + return kSecAttrAccessibleAfterFirstUnlock; } -RCT_EXPORT_METHOD(setGenericPasswordForService:(NSString*)service withUsername:(NSString*)username withPassword:(NSString*)password withAccessible:(NSString *)accessible resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - if(service == nil) { - service = [[NSBundle mainBundle] bundleIdentifier]; +NSString *serviceValue(NSDictionary *options) +{ + if (options && options[@"service"] != nil) { + return options[@"service"]; } + return [[NSBundle mainBundle] bundleIdentifier]; +} + +RCT_EXPORT_METHOD(setGenericPasswordForOptions:(NSDictionary *)options withUsername:(NSString *)username withPassword:(NSString *)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); // Create dictionary of search parameters - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, service, kSecAttrService, 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)(kSecClassGenericPassword), kSecClass, accessibleValue(accessible), kSecAttrAccessible, service, kSecAttrService, passwordData, kSecValueData, username, kSecAttrAccount, nil]; + NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; + dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, accessibleValue(options), kSecAttrAccessible, service, kSecAttrService, passwordData, kSecValueData, username, kSecAttrAccount, nil]; // Try to save to keychain osStatus = SecItemAdd((__bridge CFDictionaryRef) dict, NULL); @@ -124,13 +130,12 @@ RCT_EXPORT_METHOD(setGenericPasswordForService:(NSString*)service withUsername:( } -RCT_EXPORT_METHOD(getGenericPasswordForService:(NSString*)service resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - if(service == nil) { - service = [[NSBundle mainBundle] bundleIdentifier]; - } +RCT_EXPORT_METHOD(getGenericPasswordForOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); // Create dictionary of search parameters - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, service, kSecAttrService, 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; @@ -148,7 +153,7 @@ RCT_EXPORT_METHOD(getGenericPasswordForService:(NSString*)service resolver:(RCTP } // Found - NSString* username = (NSString*) [found objectForKey:(__bridge id)(kSecAttrAccount)]; + NSString* username = (NSString *) [found objectForKey:(__bridge id)(kSecAttrAccount)]; NSString* password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; return resolve(@{ @@ -159,13 +164,12 @@ RCT_EXPORT_METHOD(getGenericPasswordForService:(NSString*)service resolver:(RCTP } -RCT_EXPORT_METHOD(resetGenericPasswordForService:(NSString*)service resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - if(service == nil) { - service = [[NSBundle mainBundle] bundleIdentifier]; - } +RCT_EXPORT_METHOD(resetGenericPasswordForOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); // Create dictionary of search parameters - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass, service, kSecAttrService, kCFBooleanTrue, kSecReturnAttributes, kCFBooleanTrue, kSecReturnData, nil]; + 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); @@ -178,16 +182,17 @@ RCT_EXPORT_METHOD(resetGenericPasswordForService:(NSString*)service resolver:(RC } -RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString*)server withUsername:(NSString*)username withPassword:(NSString*)password withAccessible:(NSString *)accessible resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ +RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString *)server withUsername:(NSString*)username withPassword:(NSString*)password withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ // Create dictionary of search parameters - NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, kCFBooleanTrue, kSecReturnAttributes, nil]; + 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, accessibleValue(accessible), kSecAttrAccessible, server, kSecAttrServer, passwordData, kSecValueData, username, kSecAttrAccount, nil]; + dict = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassInternetPassword), kSecClass, accessibleValue(options), kSecAttrAccessible, server, kSecAttrServer, passwordData, kSecValueData, username, kSecAttrAccount, nil]; // Try to save to keychain osStatus = SecItemAdd((__bridge CFDictionaryRef) dict, NULL); @@ -200,13 +205,13 @@ RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString*)server withUsername return resolve(@(YES)); } -RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString*)server resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - +RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ // 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)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, kCFBooleanTrue, kSecReturnAttributes, kCFBooleanTrue, kSecReturnData, nil]; // Look up server in the keychain - NSDictionary* found = nil; + NSDictionary *found = nil; CFTypeRef foundTypeRef = NULL; OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) dict, (CFTypeRef*)&foundTypeRef); @@ -221,8 +226,8 @@ RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString*)server resolver:(RC } // Found - NSString* username = (NSString*) [found objectForKey:(__bridge id)(kSecAttrAccount)]; - NSString* password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; + NSString *username = (NSString *) [found objectForKey:(__bridge id)(kSecAttrAccount)]; + NSString *password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; return resolve(@{ @"server": server, @@ -232,10 +237,10 @@ RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString*)server resolver:(RC } -RCT_EXPORT_METHOD(resetInternetCredentialsForServer:(NSString*)server resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - +RCT_EXPORT_METHOD(resetInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ // 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)(kSecClassInternetPassword), kSecClass, server, kSecAttrServer, kCFBooleanTrue, kSecReturnAttributes, kCFBooleanTrue, kSecReturnData, nil]; // Remove any old values from the keychain OSStatus osStatus = SecItemDelete((__bridge CFDictionaryRef) dict); @@ -274,7 +279,7 @@ RCT_EXPORT_METHOD(requestSharedWebCredentials:(RCTPromiseResolveBlock)resolve re } -RCT_EXPORT_METHOD(setSharedWebCredentialsForServer:(NSString*)server withUsername:(NSString*)username withPassword:(NSString*)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(setSharedWebCredentialsForServer:(NSString *)server withUsername:(NSString *)username withPassword:(NSString *)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { SecAddSharedWebCredential( (__bridge CFStringRef)server, diff --git a/android/src/main/java/com/oblador/keychain/KeychainModule.java b/android/src/main/java/com/oblador/keychain/KeychainModule.java index 7318685..df07178 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainModule.java +++ b/android/src/main/java/com/oblador/keychain/KeychainModule.java @@ -16,6 +16,7 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import java.nio.charset.Charset; @@ -43,7 +44,7 @@ public class KeychainModule extends ReactContextBaseJavaModule { } @ReactMethod - public void setGenericPasswordForService(String service, String username, String password, String accessible, Promise promise) { + public void setGenericPasswordForOptions(String service, String username, String password, Promise promise) { if (!crypto.isAvailable()) { Log.e(KEYCHAIN_MODULE, "Crypto is missing"); promise.reject("KeychainModule: crypto is missing"); @@ -84,7 +85,7 @@ public class KeychainModule extends ReactContextBaseJavaModule { } @ReactMethod - public void getGenericPasswordForService(String service, Promise promise) { + public void getGenericPasswordForOptions(String service, Promise promise) { service = service == null ? EMPTY_STRING : service; String username = prefs.getString(service + ":u", "user_not_found"); @@ -119,7 +120,7 @@ public class KeychainModule extends ReactContextBaseJavaModule { } @ReactMethod - public void resetGenericPasswordForService(String service, Promise promise) { + public void resetGenericPasswordForOptions(String service, Promise promise) { service = service == null ? EMPTY_STRING : service; SharedPreferences.Editor prefsEditor = prefs.edit(); @@ -134,18 +135,18 @@ public class KeychainModule extends ReactContextBaseJavaModule { } @ReactMethod - public void setInternetCredentialsForServer(@NonNull String server, String username, String password, String accessible, Promise promise) { - setGenericPasswordForService(server, username, password, accessible, promise); + public void setInternetCredentialsForServer(@NonNull String server, String username, String password, ReadableMap unusedOptions, Promise promise) { + setGenericPasswordForOptions(server, username, password, promise); } @ReactMethod - public void getInternetCredentialsForServer(@NonNull String server, Promise promise) { - getGenericPasswordForService(server, promise); + public void getInternetCredentialsForServer(@NonNull String server, ReadableMap unusedOptions, Promise promise) { + getGenericPasswordForOptions(server, promise); } @ReactMethod - public void resetInternetCredentialsForServer(@NonNull String server, Promise promise) { - resetGenericPasswordForService(server, promise); + public void resetInternetCredentialsForServer(@NonNull String server, ReadableMap unusedOptions, Promise promise) { + resetGenericPasswordForOptions(server, promise); } diff --git a/index.js b/index.js index f3f9404..a4c1dec 100644 --- a/index.js +++ b/index.js @@ -10,82 +10,96 @@ type SecAccessible = | 'AccessibleAfterFirstUnlockThisDeviceOnly' | 'AccessibleAlwaysThisDeviceOnly' +type Options = { + accessible?: SecAccessible; + service?: string; +}; + /** * Saves the `username` and `password` combination for `server`. * @param {string} server URL to server. * @param {string} username Associated username or e-mail to be saved. * @param {string} password Associated password to be saved. - * @param {string} accessible (iOS only) kSecAccessibleKey + * @param {object} options Keychain options, iOS only * @return {Promise} Resolves to `true` when successful */ export function setInternetCredentials( server: string, username: string, password: string, - accessible?: SecAccessible + options?: Options ): Promise { - return RNKeychainManager.setInternetCredentialsForServer(server, username, password, accessible); + return RNKeychainManager.setInternetCredentialsForServer(server, username, password, options); } /** * Fetches login combination for `server`. * @param {string} server URL to server. + * @param {object} options Keychain options, iOS only * @return {Promise} Resolves to `{ server, username, password }` when successful */ export function getInternetCredentials( - server: string + server: string, + options?: Options ): Promise { - return RNKeychainManager.getInternetCredentialsForServer(server); + return RNKeychainManager.getInternetCredentialsForServer(server, options); } /** * Deletes all internet password keychain entries for `server`. * @param {string} server URL to server. + * @param {object} options Keychain options, iOS only * @return {Promise} Resolves to `true` when successful */ export function resetInternetCredentials( - server: string + server: string, + options?: Options ): Promise { - return RNKeychainManager.resetInternetCredentialsForServer(server); + return RNKeychainManager.resetInternetCredentialsForServer(server, options); +} + +function getOptionsArgument(serviceOrOptions?: string | KeychainOptions) { + if (Platform.OS !== 'ios') { + return typeof serviceOrOptions === 'object' ? serviceOrOptions.service : serviceOrOptions; + } + return typeof serviceOrOptions === 'string' ? { service: serviceOrOptions } : serviceOrOptions; } /** * Saves the `username` and `password` combination for `service`. * @param {string} username Associated username or e-mail to be saved. * @param {string} password Associated password to be saved. - * @param {string} service Reverse domain name qualifier for the service, defaults to `bundleId`. - * @param {string} accessible (iOS only) kSecAccessibleKey + * @param {string|object} serviceOrOptions Reverse domain name qualifier for the service, defaults to `bundleId` or an options object. * @return {Promise} Resolves to `true` when successful */ export function setGenericPassword( username: string, password: string, - service?: string - accessible?: SecAccessible + serviceOrOptions?: string | KeychainOptions ): Promise { - return RNKeychainManager.setGenericPasswordForService(service, username, password, accessible); + return RNKeychainManager.setGenericPasswordForOptions(getOptionsArgument(serviceOrOptions), username, password); } /** * Fetches login combination for `service`. - * @param {string} service Reverse domain name qualifier for the service, defaults to `bundleId`. + * @param {string|object} serviceOrOptions Reverse domain name qualifier for the service, defaults to `bundleId` or an options object. * @return {Promise} Resolves to `{ service, username, password }` when successful */ export function getGenericPassword( - service?: string + serviceOrOptions?: string | KeychainOptions ): Promise { - return RNKeychainManager.getGenericPasswordForService(service); + return RNKeychainManager.getGenericPasswordForOptions(getOptionsArgument(serviceOrOptions)); } /** * Deletes all generic password keychain entries for `service`. - * @param {string} service Reverse domain name qualifier for the service, defaults to `bundleId`. + * @param {string|object} serviceOrOptions Reverse domain name qualifier for the service, defaults to `bundleId` or an options object. * @return {Promise} Resolves to `true` when successful */ export function resetGenericPassword( - service?: string + serviceOrOptions?: string | KeychainOptions ): Promise { - return RNKeychainManager.resetGenericPasswordForService(service); + return RNKeychainManager.resetGenericPasswordForOptions(getOptionsArgument(serviceOrOptions)); } /**