diff --git a/android/src/main/java/com/oblador/keychain/KeychainModule.kt b/android/src/main/java/com/oblador/keychain/KeychainModule.kt index 688b7344..887cb8a8 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainModule.kt +++ b/android/src/main/java/com/oblador/keychain/KeychainModule.kt @@ -1,5 +1,7 @@ package com.oblador.keychain +import android.content.Context +import android.content.SharedPreferences import android.os.Build import android.text.TextUtils import android.util.Log @@ -434,6 +436,112 @@ class KeychainModule(reactContext: ReactApplicationContext) : promise.resolve(getSecurityLevel(useBiometry, usePasscode).name) } + /** + * Get value for a specific key from SharedPreferences + */ + @ReactMethod + fun getItemForKey(key: String, options: ReadableMap?, promise: Promise) { + try { + val prefs = getSharedPrefs(options) + val value = prefs.getString(key, null) + + if (value != null) { + promise.resolve(value) + } else { + promise.resolve(null) + } + } catch (e: Exception) { + Log.e(KEYCHAIN_MODULE, "Error getting item for key: ${e.message}", e) + promise.reject(Errors.E_UNKNOWN_ERROR, e) + } + } + + /** + * Get all items from SharedPreferences + */ + @ReactMethod + fun getAllItems(options: ReadableMap?, promise: Promise) { + try { + val prefs = getSharedPrefs(options) + val allEntries = prefs.all + val resultMap = Arguments.createMap() + + for ((key, value) in allEntries) { + // Convert value to string and add to result map + resultMap.putString(key, value?.toString()) + } + + promise.resolve(resultMap) + } catch (e: Exception) { + Log.e(KEYCHAIN_MODULE, "Error getting all items: ${e.message}", e) + promise.reject(Errors.E_UNKNOWN_ERROR, e) + } + } + + /** + * Set value for a specific key in SharedPreferences + */ + @ReactMethod + fun setItemForKey(key: String, value: String, options: ReadableMap?, promise: Promise) { + try { + val prefs = getSharedPrefs(options) + val editor = prefs.edit() + editor.putString(key, value) + editor.apply() + + promise.resolve(value) + } catch (e: Exception) { + Log.e(KEYCHAIN_MODULE, "Error setting item for key: ${e.message}", e) + promise.reject(Errors.E_UNKNOWN_ERROR, e) + } + } + + /** + * Remove item for a specific key from SharedPreferences + */ + @ReactMethod + fun removeItemForKey(key: String, options: ReadableMap?, promise: Promise) { + try { + val prefs = getSharedPrefs(options) + val editor = prefs.edit() + editor.remove(key) + editor.apply() + + promise.resolve(true) + } catch (e: Exception) { + Log.e(KEYCHAIN_MODULE, "Error removing item for key: ${e.message}", e) + promise.reject(Errors.E_UNKNOWN_ERROR, e) + } + } + + /** + * Clear all items from SharedPreferences for the given service + */ + @ReactMethod + fun clearItems(options: ReadableMap?, promise: Promise) { + try { + val prefs = getSharedPrefs(options) + val editor = prefs.edit() + editor.clear() + editor.apply() + + promise.resolve(true) + } catch (e: Exception) { + Log.e(KEYCHAIN_MODULE, "Error clearing items: ${e.message}", e) + promise.reject(Errors.E_UNKNOWN_ERROR, e) + } + } + + /** + * Helper function to get SharedPreferences instance with the correct name + */ + private fun getSharedPrefs(options: ReadableMap?): SharedPreferences { + val service = options?.getString(Maps.SERVICE) ?: "shared_preferences" + return reactApplicationContext.getSharedPreferences(service, Context.MODE_PRIVATE) + } + + // endregion + private fun addCipherStorageToMap(cipherStorage: CipherStorage) { cipherStorageMap[cipherStorage.getCipherStorageName()] = cipherStorage } diff --git a/ios/RNKeychainManager/RNKeychainManager.m b/ios/RNKeychainManager/RNKeychainManager.m index 6888ff0d..dadfc695 100644 --- a/ios/RNKeychainManager/RNKeychainManager.m +++ b/ios/RNKeychainManager/RNKeychainManager.m @@ -706,4 +706,246 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server withOptions:(NSDiction } } +RCT_EXPORT_METHOD(getItemForKey:(NSString *)key + withOptions:(NSDictionary * __nullable)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); + NSString *authenticationPrompt = authenticationPromptValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); + NSString *accessGroup = accessGroupValue(options); + + NSMutableDictionary *query = [@{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrAccount: key, // Query by specific key + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), + (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne + } mutableCopy]; + + // Only add authentication prompt if it exists + if (authenticationPrompt != nil) { + query[(__bridge NSString *)kSecUseOperationPrompt] = authenticationPrompt; + } + + if (accessGroup != nil) { + query[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup; + } + + // Look up service in the keychain + NSDictionary *found = nil; + CFTypeRef foundTypeRef = NULL; + OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef*)&foundTypeRef); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return rejectWithError(reject, error); + } + + found = (__bridge NSDictionary*)(foundTypeRef); + if (!found) { + return resolve([NSNull null]); // Return null for not found + } + + // Found - return just the value string directly + NSString *value = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; + + CFRelease(foundTypeRef); + + return resolve(value); +} + +RCT_EXPORT_METHOD(getAllGenericPasswords:(NSDictionary * __nullable)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); + NSString *accessGroup = accessGroupValue(options); + + NSMutableDictionary *query = [@{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), + (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitAll + } mutableCopy]; + + if (accessGroup != nil) { + query[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup; + } + + NSMutableArray *items = [NSMutableArray new]; + CFTypeRef result = NULL; + OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return rejectWithError(reject, error); + } + + if (osStatus != errSecItemNotFound && result != NULL) { + NSArray *results = (__bridge NSArray*)result; + for (NSDictionary *item in results) { + NSString *key = (NSString *)[item objectForKey:(__bridge id)(kSecAttrAccount)]; + NSString *value = [[NSString alloc] initWithData:[item objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; + + if (key && value) { + [items addObject:@{ + @"key": key, + @"service": service, + @"value": value + }]; + } + } + CFRelease(result); + } + + return resolve(@[items]); +} + +RCT_EXPORT_METHOD(setItemForKey:(NSString *)key + withValue:(NSString *)value + withOptions:(NSDictionary * __nullable)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); + CFStringRef accessible = accessibleValue(options); + NSString *accessGroup = accessGroupValue(options); + SecAccessControlCreateFlags accessControl = accessControlValue(options); + + // First, delete any existing entry with this key + NSMutableDictionary *deleteQuery = [@{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrAccount: key, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync) + } mutableCopy]; + + if (accessGroup != nil) { + deleteQuery[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup; + } + + SecItemDelete((__bridge CFDictionaryRef)deleteQuery); + + // Now add the new entry + NSMutableDictionary *attributes = [@{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrAccount: key, // The key becomes the "account" + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), + (__bridge NSString *)kSecValueData: [value dataUsingEncoding:NSUTF8StringEncoding] + } mutableCopy]; + + if (@available(macOS 10.15, iOS 13.0, *)) { + attributes[(__bridge NSString *)kSecUseDataProtectionKeychain] = @(YES); + } + + if (accessControl) { + NSError *aerr = nil; + #if TARGET_OS_IOS || TARGET_OS_VISION + BOOL canAuthenticate = [[LAContext new] canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&aerr]; + if (aerr || !canAuthenticate) { + return rejectWithError(reject, aerr); + } + #endif + + CFErrorRef error = NULL; + SecAccessControlRef sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + accessible, + accessControl, + &error); + + if (error) { + return rejectWithError(reject, aerr); + } + attributes[(__bridge NSString *)kSecAttrAccessControl] = (__bridge id)sacRef; + } else { + attributes[(__bridge NSString *)kSecAttrAccessible] = (__bridge id)accessible; + } + + if (accessGroup != nil) { + attributes[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup; + } + + OSStatus osStatus = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL); + + if (osStatus != noErr) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return rejectWithError(reject, error); + } + + return resolve(@{ + @"key": key, + @"service": service, + @"value": value, + @"storage": @"keychain" + }); +} + +RCT_EXPORT_METHOD(removeItemForKey:(NSString *)key + withOptions:(NSDictionary * __nullable)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); + NSString *accessGroup = accessGroupValue(options); + + NSMutableDictionary *query = [@{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrAccount: key, // Delete specific key + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync) + } mutableCopy]; + + if (accessGroup != nil) { + query[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup; + } + + OSStatus osStatus = SecItemDelete((__bridge CFDictionaryRef)query); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return rejectWithError(reject, error); + } + + return resolve(@(YES)); +} + +RCT_EXPORT_METHOD(clearItems:(NSDictionary * __nullable)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *service = serviceValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); + NSString *accessGroup = accessGroupValue(options); + + NSMutableDictionary *query = [@{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync) + } mutableCopy]; + + if (accessGroup != nil) { + query[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup; + } + + OSStatus osStatus = SecItemDelete((__bridge CFDictionaryRef)query); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return rejectWithError(reject, error); + } + + // Return success even if no items were found + return resolve(@(YES)); +} + @end diff --git a/src/index.ts b/src/index.ts index 1de35dad..7f867477 100644 --- a/src/index.ts +++ b/src/index.ts @@ -368,6 +368,103 @@ export function isPasscodeAuthAvailable(): Promise { return RNKeychainManager.isPasscodeAuthAvailable(); } + +/** + * Fetches the value for a specific key. + * + * @param {string} key - The key to retrieve the value for. + * @param {BaseOptions} [options] - A keychain options object. + * + * @returns {Promise} Resolves to the value string when successful, or null if not found + * + * @example + * ```typescript + * const value = await Keychain.getItemForKey('myKey', options); + * if (value) { + * console.log('Value loaded:', value); + * } else { + * console.log('No value stored for key'); + * } + * ``` + */ +export function getItemForKey(key: string, options?: BaseOptions): Promise { + return RNKeychainManager.getItemForKey(key, options); +} + + +/** + * Gets all items. + * + * @param {BaseOptions} [options] - A keychain options object. + * + * @returns {Promise>} Resolves to an object with all key-value pairs. + * + * @example + * ```typescript + * const items = await Keychain.getAllItems(options); + * console.log('All items:', items); + * ``` + */ +export function getAllItems(options?: BaseOptions): Promise> { + return RNKeychainManager.getAllItems(options); +} + + +/** + * Saves a value for a specific key. + * + * @param {string} key - The key to associate with the value. + * @param {string} value - The value to be saved. + * @param {BaseOptions} [options] - A keychain options object. + * + * @returns {Promise} Resolves to the stored value when successful + * + * @example + * ```typescript + * await Keychain.setItemForKey('myKey', 'value', options); + * ``` + */ +export function setItemForKey(key: string, value: string, options?: BaseOptions): Promise { + return RNKeychainManager.setItemForKey(key, value, options); +} + + +/** + * Removes the value for a specific key. + * + * @param {string} key - The key to remove. + * @param {BaseOptions} [options] - A keychain options object. + * + * @returns {Promise} Resolves to `true` when successful + * + * @example + * ```typescript + * const success = await Keychain.removeItemForKey('myKey', options); + * console.log('Key removed:', success); + * ``` + */ +export function removeItemForKey(key: string, options?: BaseOptions): Promise { + return RNKeychainManager.removeItemForKey(key, options); +} + + +/** + * Removes all items matching the service. + * + * @param {BaseOptions} [options] - A keychain options object. + * + * @returns {Promise} Resolves to `true` when successful + * + * @example + * ```typescript + * const success = await Keychain.clearItems(options); + * console.log('All items cleared:', success); + * ``` + */ +export function clearItems(options?: BaseOptions): Promise { + return RNKeychainManager.clearItems(options); +} + export * from './enums'; export * from './types'; /** @ignore */ @@ -391,4 +488,9 @@ export default { resetGenericPassword, requestSharedWebCredentials, setSharedWebCredentials, + getItemForKey, + getAllItems, + setItemForKey, + removeItemForKey, + clearItems };