diff --git a/README.md b/README.md index a5dd38f..e32ce1e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ See `KeychainExample` for fully working project example. Both `setGenericPassword` and `setInternetCredentials` are limited to strings only, so if you need to store objects etc, please use `JSON.stringify`/`JSON.parse` when you store/access it. -### `setGenericPassword(username, password, [{ accessControl, accessible, accessGroup, service }])` +### `setGenericPassword(username, password, securityLevel, [{ accessControl, accessible, accessGroup, service }])` Will store the username/password combination in the secure storage. Resolves to `true` or rejects in case of an error. @@ -57,7 +57,7 @@ Will retreive the username/password combination from the secure storage. Resolve Will remove the username/password combination from the secure storage. -### `setInternetCredentials(server, username, password, [{ accessControl, accessible, accessGroup }])` +### `setInternetCredentials(server, username, password, securityLevel, [{ accessControl, accessible, accessGroup }])` Will store the server/username/password combination in the secure storage. @@ -85,6 +85,18 @@ Inquire if the type of local authentication policy is supported on this device w Get what type of hardware biometry support the device has. Resolves to a `Keychain.BIOMETRY_TYPE` value when supported, otherwise `null`. +### `getSecurityLevel()` + +Get security level that is supported on the current device with the current OS. + +### Security Levels (Android only) + +If set, `securityLevel` parameter specifies minimum security level that the encryption key storage should guarantee for storing credentials to succeed. + +* `ANY` - no security guarantees needed (default value); Credentials can be stored in FB Secure Storage; +* `SECURE_SOFTWARE` - requires for the key to be stored in the Android Keystore, separate from the encrypted data; +* `SECURE_HARDWARE` - requires for the key to be stored on a secure hardware (Trusted Execution Environment or Secure Environment). Read [this article](https://developer.android.com/training/articles/keystore#ExtractionPrevention) for more information. + ### Options | Key | Platform | Description | Default | diff --git a/RNKeychainManager/RNKeychainManager.m b/RNKeychainManager/RNKeychainManager.m index 381981b..8d48a07 100644 --- a/RNKeychainManager/RNKeychainManager.m +++ b/RNKeychainManager/RNKeychainManager.m @@ -285,7 +285,7 @@ RCT_EXPORT_METHOD(getSupportedBiometryType:(RCTPromiseResolveBlock)resolve rejec } #endif -RCT_EXPORT_METHOD(setGenericPasswordForOptions:(NSDictionary *)options withUsername:(NSString *)username withPassword:(NSString *)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(setGenericPasswordForOptions:(NSDictionary *)options withUsername:(NSString *)username withPassword:(NSString *)password withSecurityLevel:(__unused NSString *)level resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSString *service = serviceValue(options); NSDictionary *attributes = attributes = @{ @@ -358,7 +358,7 @@ RCT_EXPORT_METHOD(resetGenericPasswordForOptions:(NSDictionary *)options resolve return resolve(@(YES)); } -RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString *)server withUsername:(NSString*)username withPassword:(NSString*)password withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString *)server withUsername:(NSString*)username withPassword:(NSString*)password withSecurityLevel:(__unused NSString *)level withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [self deleteCredentialsForServer:server]; diff --git a/android/build.gradle b/android/build.gradle index 932bba4..5e229e1 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -10,13 +10,17 @@ buildscript { apply plugin: 'com.android.library' +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion safeExtGet('compileSdkVersion', 28) + buildToolsVersion safeExtGet('buildToolsVersion', '26.0.3') defaultConfig { - minSdkVersion 16 - targetSdkVersion 23 + minSdkVersion safeExtGet('minSdkVersion', 16) + targetSdkVersion safeExtGet('targetSdkVersion', 26) versionCode 1 versionName "1.0" } diff --git a/android/src/main/java/com/oblador/keychain/KeychainModule.java b/android/src/main/java/com/oblador/keychain/KeychainModule.java index 60f76b9..edfd6d8 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainModule.java +++ b/android/src/main/java/com/oblador/keychain/KeychainModule.java @@ -20,7 +20,6 @@ import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAESCBC; import com.oblador.keychain.exceptions.CryptoFailedException; import com.oblador.keychain.exceptions.EmptyParameterException; import com.oblador.keychain.exceptions.KeyStoreAccessException; -import com.oblador.keychain.DeviceAvailability; import java.util.HashMap; import java.util.Map; @@ -56,16 +55,23 @@ public class KeychainModule extends ReactContextBaseJavaModule { } @ReactMethod - public void setGenericPasswordForOptions(String service, String username, String password, Promise promise) { + public void getSecurityLevel(Promise promise) { + promise.resolve(getSecurityLevel().name()); + } + + @ReactMethod + public void setGenericPasswordForOptions(String service, String username, String password, String minimumSecurityLevel, Promise promise) { try { + SecurityLevel level = SecurityLevel.valueOf(minimumSecurityLevel); if (username == null || username.isEmpty() || password == null || password.isEmpty()) { throw new EmptyParameterException("you passed empty or null username/password"); } service = getDefaultServiceIfNull(service); CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel(); + validateCipherStorageSecurityLevel(currentCipherStorage, level); - EncryptionResult result = currentCipherStorage.encrypt(service, username, password); + EncryptionResult result = currentCipherStorage.encrypt(service, username, password, level); prefsStorage.storeEncryptedEntry(service, result); promise.resolve(true); @@ -103,11 +109,17 @@ public class KeychainModule extends ReactContextBaseJavaModule { // decrypt using the older cipher storage decryptionResult = oldCipherStorage.decrypt(service, resultSet.usernameBytes, resultSet.passwordBytes); // encrypt using the current cipher storage - EncryptionResult encryptionResult = currentCipherStorage.encrypt(service, decryptionResult.username, decryptionResult.password); - // store the encryption result - prefsStorage.storeEncryptedEntry(service, encryptionResult); - // clean up the old cipher storage - oldCipherStorage.removeKey(service); + + try { + // don't allow to degrade security level when transferring, the new storage should be as safe as the old one. + EncryptionResult encryptionResult = currentCipherStorage.encrypt(service, decryptionResult.username, decryptionResult.password, decryptionResult.getSecurityLevel()); + // store the encryption result + prefsStorage.storeEncryptedEntry(service, encryptionResult); + // clean up the old cipher storage + oldCipherStorage.removeKey(service); + } catch (CryptoFailedException e) { + Log.e(KEYCHAIN_MODULE, "Migrating to a less safe storage is not allowed. Keeping the old one"); + } } WritableMap credentials = Arguments.createMap(); @@ -150,8 +162,8 @@ public class KeychainModule extends ReactContextBaseJavaModule { } @ReactMethod - public void setInternetCredentialsForServer(@NonNull String server, String username, String password, ReadableMap unusedOptions, Promise promise) { - setGenericPasswordForOptions(server, username, password, promise); + public void setInternetCredentialsForServer(@NonNull String server, String username, String password, String minimumSecurityLevel, ReadableMap unusedOptions, Promise promise) { + setGenericPasswordForOptions(server, username, password, minimumSecurityLevel, promise); } @ReactMethod @@ -187,8 +199,11 @@ public class KeychainModule extends ReactContextBaseJavaModule { int cipherStorageAPILevel = cipherStorage.getMinSupportedApiLevel(); // Is the cipherStorage supported on the current API level? boolean isSupported = (cipherStorageAPILevel <= currentAPILevel); + if (!isSupported) { + continue; + } // Is the API level better than the one we previously selected (if any)? - if (isSupported && (currentCipherStorage == null || cipherStorageAPILevel > currentCipherStorage.getMinSupportedApiLevel())) { + if (currentCipherStorage == null || cipherStorageAPILevel > currentCipherStorage.getMinSupportedApiLevel()) { currentCipherStorage = cipherStorage; } } @@ -198,6 +213,19 @@ public class KeychainModule extends ReactContextBaseJavaModule { return currentCipherStorage; } + private void validateCipherStorageSecurityLevel(CipherStorage cipherStorage, SecurityLevel requiredLevel) throws CryptoFailedException { + if (cipherStorage.securityLevel().satisfiesSafetyThreshold(requiredLevel)) { + return; + } + + throw new CryptoFailedException( + String.format( + "Cipher Storage is too weak. Required security level is: %s, but only %s is provided", + requiredLevel.name(), + cipherStorage.securityLevel().name())); + } + + private CipherStorage getCipherStorageByName(String cipherStorageName) { return cipherStorageMap.get(cipherStorageName); } @@ -206,6 +234,33 @@ public class KeychainModule extends ReactContextBaseJavaModule { return DeviceAvailability.isFingerprintAuthAvailable(getCurrentActivity()); } + private boolean isSecureHardwareAvailable() { + try { + return getCipherStorageForCurrentAPILevel().supportsSecureHardware(); + } catch (CryptoFailedException e) { + return false; + } + } + + private SecurityLevel getSecurityLevel() { + try { + CipherStorage storage = getCipherStorageForCurrentAPILevel(); + if (!storage.securityLevel().satisfiesSafetyThreshold(SecurityLevel.SECURE_SOFTWARE)) { + return SecurityLevel.ANY; + } + + if (isSecureHardwareAvailable()) { + return SecurityLevel.SECURE_HARDWARE; + } else { + return SecurityLevel.SECURE_SOFTWARE; + } + } catch (CryptoFailedException e) { + return SecurityLevel.ANY; + } + } + + + @NonNull private String getDefaultServiceIfNull(String service) { return service == null ? EMPTY_STRING : service; diff --git a/android/src/main/java/com/oblador/keychain/SecurityLevel.java b/android/src/main/java/com/oblador/keychain/SecurityLevel.java new file mode 100644 index 0000000..f9a41a5 --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/SecurityLevel.java @@ -0,0 +1,12 @@ +package com.oblador.keychain; + +public enum SecurityLevel { + ANY, + SECURE_SOFTWARE, + SECURE_HARDWARE; // Trusted Execution Environment or Secure Environment guarantees + + public boolean satisfiesSafetyThreshold(SecurityLevel threshold) { + return this.compareTo(threshold) >= 0; + } +} + diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.java b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.java index 0cdaaf7..3ca8289 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.java +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.java @@ -2,6 +2,7 @@ package com.oblador.keychain.cipherStorage; import android.support.annotation.NonNull; +import com.oblador.keychain.SecurityLevel; import com.oblador.keychain.exceptions.CryptoFailedException; import com.oblador.keychain.exceptions.KeyStoreAccessException; @@ -26,12 +27,19 @@ public interface CipherStorage { } class DecryptionResult extends CipherResult { - public DecryptionResult(String username, String password) { + private SecurityLevel securityLevel; + + public DecryptionResult(String username, String password, SecurityLevel level) { super(username, password); + securityLevel = level; } + + public SecurityLevel getSecurityLevel() { + return securityLevel; + } } - EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password) throws CryptoFailedException; + EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password, SecurityLevel level) throws CryptoFailedException; DecryptionResult decrypt(@NonNull String service, @NonNull byte[] username, @NonNull byte[] password) throws CryptoFailedException; @@ -40,4 +48,8 @@ public interface CipherStorage { String getCipherStorageName(); int getMinSupportedApiLevel(); + + SecurityLevel securityLevel(); + + boolean supportsSecureHardware(); } diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java index 58f06f6..b008d75 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java @@ -10,6 +10,7 @@ import com.facebook.crypto.CryptoConfig; import com.facebook.crypto.Entity; import com.facebook.crypto.keychain.KeyChain; import com.facebook.react.bridge.ReactApplicationContext; +import com.oblador.keychain.SecurityLevel; import com.oblador.keychain.exceptions.CryptoFailedException; import java.nio.charset.Charset; @@ -35,7 +36,22 @@ public class CipherStorageFacebookConceal implements CipherStorage { } @Override - public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password) throws CryptoFailedException { + public SecurityLevel securityLevel() { + return SecurityLevel.ANY; + } + + @Override + public boolean supportsSecureHardware() { + return false; + } + + @Override + public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password, SecurityLevel level) throws CryptoFailedException { + + if (!this.securityLevel().satisfiesSafetyThreshold(level)) { + throw new CryptoFailedException(String.format("Insufficient security level (wants %s; got %s)", level, this.securityLevel())); + } + if (!crypto.isAvailable()) { throw new CryptoFailedException("Crypto is missing"); } @@ -66,7 +82,8 @@ public class CipherStorageFacebookConceal implements CipherStorage { return new DecryptionResult( new String(decryptedUsername, Charset.forName("UTF-8")), - new String(decryptedPassword, Charset.forName("UTF-8"))); + new String(decryptedPassword, Charset.forName("UTF-8")), + SecurityLevel.ANY); } catch (Exception e) { throw new CryptoFailedException("Decryption failed for service " + service, e); } diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAESCBC.java b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAESCBC.java index 7cc45d2..de8c951 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAESCBC.java +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAESCBC.java @@ -3,9 +3,12 @@ package com.oblador.keychain.cipherStorage; import android.annotation.TargetApi; import android.os.Build; import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyInfo; import android.security.keystore.KeyProperties; import android.support.annotation.NonNull; +import android.util.Log; +import com.oblador.keychain.SecurityLevel; import com.oblador.keychain.exceptions.CryptoFailedException; import com.oblador.keychain.exceptions.KeyStoreAccessException; @@ -21,15 +24,19 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; -import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import android.security.keystore.StrongBoxUnavailableException; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; public class CipherStorageKeystoreAESCBC implements CipherStorage { + public static final String TAG = "KeystoreAESCBC"; public static final String CIPHER_STORAGE_NAME = "KeystoreAESCBC"; public static final String DEFAULT_SERVICE = "RN_KEYCHAIN_DEFAULT_ALIAS"; public static final String KEYSTORE_TYPE = "AndroidKeyStore"; @@ -52,30 +59,51 @@ public class CipherStorageKeystoreAESCBC implements CipherStorage { return Build.VERSION_CODES.M; } + @Override + public SecurityLevel securityLevel() { + // it can guarantee security levels up to SECURE_HARDWARE/SE/StrongBox + return SecurityLevel.SECURE_HARDWARE; + } + + @Override + public boolean supportsSecureHardware() { + final String testKeyAlias = "AndroidKeyStore#supportsSecureHardware"; + + try { + SecretKey key = tryGenerateRegularSecurityKey(testKeyAlias); + return validateKeySecurityLevel(SecurityLevel.SECURE_HARDWARE, key); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) { + return false; + } finally { + try { + removeKey(testKeyAlias); + } catch (KeyStoreAccessException e) { + Log.e(TAG, "Unable to remove temp key from keychain", e); + } + } + } + @TargetApi(Build.VERSION_CODES.M) @Override - public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password) throws CryptoFailedException { + public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password, SecurityLevel level) throws CryptoFailedException { service = getDefaultServiceIfEmpty(service); try { KeyStore keyStore = getKeyStoreAndLoad(); if (!keyStore.containsAlias(service)) { - AlgorithmParameterSpec spec; - spec = new KeyGenParameterSpec.Builder( - service, - KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) - .setBlockModes(ENCRYPTION_BLOCK_MODE) - .setEncryptionPaddings(ENCRYPTION_PADDING) - .setRandomizedEncryptionRequired(true) - //.setUserAuthenticationRequired(true) // Will throw InvalidAlgorithmParameterException if there is no fingerprint enrolled on the device - .setKeySize(ENCRYPTION_KEY_SIZE) - .build(); + // Firstly, try to generate the key as safe as possible (strongbox). + // see https://developer.android.com/training/articles/keystore#HardwareSecurityModule + SecretKey secretKey = tryGenerateStrongBoxSecurityKey(service); + if (secretKey == null) { + // If that is not possible, we generate the key in a regular way + // (it still might be generated in hardware, but not in StrongBox) + secretKey = tryGenerateRegularSecurityKey(service); + } - KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_TYPE); - generator.init(spec); - - generator.generateKey(); + if (!validateKeySecurityLevel(level, secretKey)) { + throw new CryptoFailedException("Cannot generate keys with required security guarantees"); + } } Key key = keyStore.getKey(service, null); @@ -88,6 +116,25 @@ public class CipherStorageKeystoreAESCBC implements CipherStorage { throw new CryptoFailedException("Could not encrypt data for service " + service, e); } catch (KeyStoreException | KeyStoreAccessException e) { throw new CryptoFailedException("Could not access Keystore for service " + service, e); + } catch (Exception e) { + throw new CryptoFailedException("Unknown error: " + e.getMessage(), e); + } + } + + @TargetApi(Build.VERSION_CODES.M) + private boolean validateKeySecurityLevel(SecurityLevel level, SecretKey generatedKey) { + return getSecurityLevel(generatedKey).satisfiesSafetyThreshold(level); + } + + @TargetApi(Build.VERSION_CODES.M) + private SecurityLevel getSecurityLevel(SecretKey key) { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance(key.getAlgorithm(), KEYSTORE_TYPE); + KeyInfo keyInfo; + keyInfo = (KeyInfo) factory.getKeySpec(key, KeyInfo.class); + return keyInfo.isInsideSecureHardware() ? SecurityLevel.SECURE_HARDWARE : SecurityLevel.SECURE_SOFTWARE; + } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) { + return SecurityLevel.ANY; } } @@ -103,11 +150,13 @@ public class CipherStorageKeystoreAESCBC implements CipherStorage { String decryptedUsername = decryptBytes(key, username); String decryptedPassword = decryptBytes(key, password); - return new DecryptionResult(decryptedUsername, decryptedPassword); + return new DecryptionResult(decryptedUsername, decryptedPassword, getSecurityLevel((SecretKey) key)); } catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException e) { throw new CryptoFailedException("Could not get key from Keystore", e); } catch (KeyStoreAccessException e) { throw new CryptoFailedException("Could not access Keystore", e); + } catch (Exception e) { + throw new CryptoFailedException("Unknown error: " + e.getMessage(), e); } } @@ -123,6 +172,8 @@ public class CipherStorageKeystoreAESCBC implements CipherStorage { } } catch (KeyStoreException e) { throw new KeyStoreAccessException("Failed to access Keystore", e); + } catch (Exception e) { + throw new KeyStoreAccessException("Unknown error " + e.getMessage(), e); } } @@ -189,4 +240,46 @@ public class CipherStorageKeystoreAESCBC implements CipherStorage { private String getDefaultServiceIfEmpty(@NonNull String service) { return service.isEmpty() ? DEFAULT_SERVICE : service; } + + @TargetApi(Build.VERSION_CODES.P) + private SecretKey tryGenerateStrongBoxSecurityKey(String service) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException, NoSuchProviderException { + // StrongBox is only supported on Android P and higher + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return null; + } + try { + return generateKey(getKeyGenSpecBuilder(service).setIsStrongBoxBacked(true).build()); + } catch (StrongBoxUnavailableException e) { + Log.i(TAG, "StrongBox is unavailable on this device"); + return null; + } + } + + @TargetApi(Build.VERSION_CODES.M) + private SecretKey tryGenerateRegularSecurityKey(String service) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException, NoSuchProviderException { + return generateKey(getKeyGenSpecBuilder(service).build()); + } + + // returns true if the key was generated successfully + @TargetApi(Build.VERSION_CODES.M) + private SecretKey generateKey(KeyGenParameterSpec spec) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException { + KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_TYPE); + generator.init(spec); + return generator.generateKey(); + } + + @TargetApi(Build.VERSION_CODES.M) + private KeyGenParameterSpec.Builder getKeyGenSpecBuilder(String service) { + return new KeyGenParameterSpec.Builder( + service, + KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) + .setBlockModes(ENCRYPTION_BLOCK_MODE) + .setEncryptionPaddings(ENCRYPTION_PADDING) + .setRandomizedEncryptionRequired(true) + //.setUserAuthenticationRequired(true) // Will throw InvalidAlgorithmParameterException if there is no fingerprint enrolled on the device + .setKeySize(ENCRYPTION_KEY_SIZE); + } } diff --git a/index.js b/index.js index 0fd244a..74ad6dd 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,12 @@ import { NativeModules, Platform } from 'react-native'; const { RNKeychainManager } = NativeModules; +export const SECURITY_LEVEL = { + ANY: 'ANY', + SECURE_SOFTWARE: 'SECURE_SOFTWARE', + SECURE_HARDWARE: 'SECURE_HARDWARE', +}; + export const ACCESSIBLE = { WHEN_UNLOCKED: 'AccessibleWhenUnlocked', AFTER_FIRST_UNLOCK: 'AccessibleAfterFirstUnlock', @@ -33,6 +39,11 @@ export const BIOMETRY_TYPE = { FINGERPRINT: 'Fingerprint', }; +type SecMinimumLevel = + | 'ANY' + | 'SECURE_SOFTWARE' + | 'SECURE_HARDWARE' ; + type SecAccessible = | 'AccessibleWhenUnlocked' | 'AccessibleAfterFirstUnlock' @@ -62,6 +73,18 @@ type Options = { service?: string, }; +/** + * (Android only) Returns guaranteed security level supported by this library + * on the current device. + * @return {Promise} Resolves to `SECURITY_LEVEL` when supported, otherwise `null`. + */ +export function getSecurityLevel(): Promise { + if (!RNKeychainManager.getSecurityLevel){ + return Promise.resolve(null); + } + return RNKeychainManager.getSecurityLevel(); +} + /** * Inquire if the type of local authentication policy (LAPolicy) is supported * on this device with the device settings the user chose. @@ -91,6 +114,8 @@ export function getSupportedBiometryType(): Promise { * @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} minimumSecurityLevel `SECURITY_LEVEL` defines which security + * level is minimally acceptable for this password. * @param {object} options Keychain options, iOS only * @return {Promise} Resolves to `true` when successful */ @@ -98,12 +123,14 @@ export function setInternetCredentials( server: string, username: string, password: string, + minimumSecurityLevel?: SecMinimumLevel, options?: Options ): Promise { return RNKeychainManager.setInternetCredentialsForServer( server, username, password, + getMinimumSecurityLevel(minimumSecurityLevel), options ); } @@ -145,35 +172,34 @@ function getOptionsArgument(serviceOrOptions?: string | Options) { : serviceOrOptions; } +function getMinimumSecurityLevel(minimumSecurityLevel?: SecMinimumLevel) { + if (minimumSecurityLevel === undefined) { + return SECURITY_LEVEL.ANY; + } else { + return minimumSecurityLevel + } +} + /** * 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} minimumSecurityLevel `SECURITY_LEVEL` defines which security + * level is minimally acceptable for this password. * @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, + minimumSecurityLevel?: SecMinimumLevel, serviceOrOptions?: string | Options ): Promise { return RNKeychainManager.setGenericPasswordForOptions( getOptionsArgument(serviceOrOptions), username, - password - ); -} - -/** - * Saves the `username` for further use on get requests. - * @param {string} username Associated username or e-mail to be saved. - * @return {Promise} Resolves to `true` when successful - */ -export function setUsername( - username: string -): Promise { - return RNKeychainManager.setUsername( - username + password, + getMinimumSecurityLevel(minimumSecurityLevel) ); }