Skip to content

Commit ba01dd7

Browse files
authored
feat: isPasscodeAuthAvailable (#743)
1 parent 2ff1c89 commit ba01dd7

File tree

6 files changed

+84
-42
lines changed

6 files changed

+84
-42
lines changed

android/src/main/java/com/oblador/keychain/DeviceAvailability.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ object DeviceAvailability {
2828
BiometricManager.BIOMETRIC_SUCCESS
2929
}
3030

31-
fun isDeviceCredentialAuthAvailable(context: Context): Boolean {
32-
return BiometricManager.from(context)
33-
.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) ==
34-
BiometricManager.BIOMETRIC_SUCCESS && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
31+
fun isDevicePasscodeAvailable(context: Context): Boolean {
32+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
33+
BiometricManager.from(context)
34+
.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) ==
35+
BiometricManager.BIOMETRIC_SUCCESS
36+
} else {
37+
false
38+
}
3539
}
3640

3741
fun isFingerprintAuthAvailable(context: Context): Boolean {

android/src/main/java/com/oblador/keychain/KeychainModule.kt

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,11 @@ class KeychainModule(reactContext: ReactApplicationContext) :
186186
val level = getSecurityLevelOrDefault(options)
187187
val storage = getSelectedStorage(options)
188188
throwIfInsufficientLevel(storage, level)
189-
val promptInfo = getPromptInfo(options)
189+
val accessControl = getAccessControlOrDefault(options)
190+
val usePasscode = getUsePasscode(accessControl) && isPasscodeAvailable
191+
val useBiometry =
192+
getUseBiometry(accessControl) && (isFingerprintAuthAvailable || isFaceAuthAvailable || isIrisAuthAvailable)
193+
val promptInfo = getPromptInfo(options, usePasscode, useBiometry)
190194
val result =
191195
encryptToResult(alias, storage, username, password, level, promptInfo)
192196
prefsStorage.storeEncryptedEntry(alias, result)
@@ -249,7 +253,11 @@ class KeychainModule(reactContext: ReactApplicationContext) :
249253
return@launch
250254
}
251255
val storageName = resultSet.cipherStorageName
252-
val promptInfo = getPromptInfo(options)
256+
val accessControl = getAccessControlOrDefault(options)
257+
val usePasscode = getUsePasscode(accessControl) && isPasscodeAvailable
258+
val useBiometry =
259+
getUseBiometry(accessControl) && (isFingerprintAuthAvailable || isFaceAuthAvailable || isIrisAuthAvailable)
260+
val promptInfo = getPromptInfo(options, usePasscode, useBiometry)
253261
val cipher = getCipherStorageByName(storageName)
254262
val decryptionResult =
255263
decryptCredentials(alias, cipher!!, resultSet, promptInfo)
@@ -295,9 +303,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
295303
for (cipher in ciphers) {
296304
val aliases = cipher!!.getAllKeys()
297305
for (alias in aliases) {
298-
if (alias != WARMING_UP_ALIAS) {
299-
result.add(alias)
300-
}
306+
result.add(alias)
301307
}
302308
}
303309
return result
@@ -384,6 +390,17 @@ class KeychainModule(reactContext: ReactApplicationContext) :
384390
resetGenericPassword(alias, promise)
385391
}
386392

393+
@ReactMethod
394+
fun isPasscodeAuthAvailable(promise: Promise) {
395+
try {
396+
val reply: Boolean = DeviceAvailability.isDevicePasscodeAvailable(reactApplicationContext)
397+
promise.resolve(reply)
398+
} catch (fail: Throwable) {
399+
Log.e(KEYCHAIN_MODULE, fail.message, fail)
400+
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
401+
}
402+
}
403+
387404
@ReactMethod
388405
fun getSupportedBiometryType(promise: Promise) {
389406
try {
@@ -542,14 +559,6 @@ class KeychainModule(reactContext: ReactApplicationContext) :
542559
oldCipherStorage.removeKey(service)
543560
}
544561

545-
@get:Throws(CryptoFailedException::class)
546-
val cipherStorageForCurrentAPILevel: CipherStorage
547-
/**
548-
* The "Current" CipherStorage is the cipherStorage with the highest API level that is lower
549-
* than or equal to the current API level
550-
*/
551-
get() = getCipherStorageForCurrentAPILevel(true, true)
552-
553562
/**
554563
* The "Current" CipherStorage is the cipherStorage with the highest API level that is lower than
555564
* or equal to the current API level. Parameter allow to reduce level.
@@ -620,7 +629,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
620629

621630
val isPasscodeAvailable: Boolean
622631
/** Is secured hardware a part of current storage or not. */
623-
get() = DeviceAvailability.isDeviceCredentialAuthAvailable(reactApplicationContext)
632+
get() = DeviceAvailability.isDevicePasscodeAvailable(reactApplicationContext)
624633

625634
/** Resolve storage to security level it provides. */
626635
private fun getSecurityLevel(useBiometry: Boolean, usePasscode: Boolean): SecurityLevel {
@@ -646,7 +655,6 @@ class KeychainModule(reactContext: ReactApplicationContext) :
646655
const val FACE_SUPPORTED_NAME = "Face"
647656
const val IRIS_SUPPORTED_NAME = "Iris"
648657
const val EMPTY_STRING = ""
649-
const val WARMING_UP_ALIAS = "warmingUp"
650658
private val LOG_TAG = KeychainModule::class.java.simpleName
651659

652660

@@ -731,10 +739,11 @@ class KeychainModule(reactContext: ReactApplicationContext) :
731739
}
732740

733741
/** Extract user specified prompt info from options. */
734-
private fun getPromptInfo(options: ReadableMap?): PromptInfo {
735-
val accessControl = getAccessControlOrDefault(options)
736-
val usePasscode = getUsePasscode(accessControl)
737-
val useBiometry = getUseBiometry(accessControl)
742+
private fun getPromptInfo(
743+
options: ReadableMap?,
744+
usePasscode: Boolean,
745+
useBiometry: Boolean
746+
): PromptInfo {
738747
val promptInfoOptionsMap =
739748
if (options != null && options.hasKey(Maps.AUTH_PROMPT)) options.getMap(Maps.AUTH_PROMPT)
740749
else null
@@ -750,22 +759,20 @@ class KeychainModule(reactContext: ReactApplicationContext) :
750759
promptInfoBuilder.setDescription(it)
751760
}
752761

753-
val allowedAuthenticators = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
754-
when {
755-
usePasscode && useBiometry ->
756-
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
762+
val allowedAuthenticators = when {
763+
usePasscode && useBiometry ->
764+
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
757765

758-
usePasscode ->
759-
BiometricManager.Authenticators.DEVICE_CREDENTIAL
766+
usePasscode ->
767+
BiometricManager.Authenticators.DEVICE_CREDENTIAL
760768

761-
else ->
762-
BiometricManager.Authenticators.BIOMETRIC_STRONG
763-
}
764-
} else {
765-
BiometricManager.Authenticators.BIOMETRIC_STRONG
769+
else ->
770+
null
766771
}
767772

768-
promptInfoBuilder.setAllowedAuthenticators(allowedAuthenticators)
773+
if (allowedAuthenticators != null) {
774+
promptInfoBuilder.setAllowedAuthenticators(allowedAuthenticators)
775+
}
769776

770777
if (!usePasscode) {
771778
promptInfoOptionsMap?.getString(AuthPromptOptions.CANCEL)?.let {

android/src/main/java/com/oblador/keychain/cipherStorage/CipherCache.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,23 @@ import javax.crypto.NoSuchPaddingException
1111
*/
1212
object CipherCache {
1313
private val LOG_TAG = CipherCache::class.java.simpleName
14-
14+
1515
private val cipherCache = ThreadLocal<MutableMap<String, Cipher>>()
1616

1717
/**
1818
* Gets or creates a Cipher instance for the specified transformation.
1919
* This method is thread-safe and caches Cipher instances per thread.
2020
*
2121
* @param transformation The name of the transformation, e.g., "AES/CBC/PKCS7Padding"
22-
* @param prefix An optional identifier added to the cache key to distinguish between different uses of the same transformation.
23-
* The prefix only affects how the cipher is stored in the cache and does not modify the cipher's behavior.
2422
* @return A Cipher instance for the requested transformation
2523
* @throws NoSuchAlgorithmException if the transformation algorithm is not available
2624
* @throws NoSuchPaddingException if the padding scheme is not available
2725
*/
2826
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
29-
fun getCipher(transformation: String, prefix: String? = null): Cipher {
27+
fun getCipher(transformation: String): Cipher {
3028
return synchronized(this) {
31-
val cacheKey = prefix?.let { "${it}_$transformation" } ?: transformation
3229
(cipherCache.get() ?: mutableMapOf<String, Cipher>().also { cipherCache.set(it) })
33-
.getOrPut(cacheKey) { Cipher.getInstance(transformation) }
30+
.getOrPut(transformation) { Cipher.getInstance(transformation) }
3431
}
3532
}
3633

ios/RNKeychainManager/RNKeychainManager.m

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,22 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server withOptions:(NSDiction
398398
}
399399
#endif
400400

401+
#if TARGET_OS_IOS
402+
RCT_EXPORT_METHOD(isPasscodeAuthAvailable:(RCTPromiseResolveBlock)resolve
403+
rejecter:(RCTPromiseRejectBlock)reject)
404+
{
405+
NSError *aerr = nil;
406+
LAContext *context = [LAContext new];
407+
BOOL canBeProtected = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&aerr];
408+
409+
if (!aerr && canBeProtected) {
410+
return resolve(@(YES));
411+
}
412+
413+
return resolve(@(NO));
414+
}
415+
#endif
416+
401417
#if TARGET_OS_IOS || TARGET_OS_VISION
402418
RCT_EXPORT_METHOD(getSupportedBiometryType:(RCTPromiseResolveBlock)resolve
403419
rejecter:(RCTPromiseRejectBlock)reject)

src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,24 @@ export function getSecurityLevel(
350350
return RNKeychainManager.getSecurityLevel(options);
351351
}
352352

353+
/**
354+
* Checks if passcode authentication is available on the current device.
355+
*
356+
* @returns {Promise<boolean>} Resolves to `true` if passcode authentication is available, otherwise `false`.
357+
*
358+
* @example
359+
* ```typescript
360+
* const isAvailable = await Keychain.isPasscodeAuthAvailable();
361+
* console.log('Passcode authentication available:', isAvailable);
362+
* ```
363+
*/
364+
export function isPasscodeAuthAvailable(): Promise<boolean> {
365+
if (!RNKeychainManager.isPasscodeAuthAvailable) {
366+
return Promise.resolve(false);
367+
}
368+
return RNKeychainManager.isPasscodeAuthAvailable();
369+
}
370+
353371
export * from './enums';
354372
export * from './types';
355373
/** @ignore */
@@ -364,6 +382,7 @@ export default {
364382
canImplyAuthentication,
365383
getSupportedBiometryType,
366384
setInternetCredentials,
385+
isPasscodeAuthAvailable,
367386
getInternetCredentials,
368387
resetInternetCredentials,
369388
setGenericPassword,

src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export type BaseOptions = {
4343
accessGroup?: string;
4444
};
4545

46-
/** Base options for keychain functions. */
4746
export type SetOptions = {
4847
/** Specifies when a keychain item is accessible.
4948
* @platform iOS, visionOS

0 commit comments

Comments
 (0)