Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions android/src/main/java/com/oblador/keychain/KeychainModule.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
Expand Down
242 changes: 242 additions & 0 deletions ios/RNKeychainManager/RNKeychainManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading