From 43e5512cabb8ee064fd9e503be943dcf2c7d7669 Mon Sep 17 00:00:00 2001 From: Igor Mandrigin Date: Wed, 19 Dec 2018 09:11:10 +0100 Subject: [PATCH] Minimal security guarantees for react-native-keychain (#6) * Implement security level guarantees for Android. Supported security levels: - ANY - SECURE_SOFTWARE - SECURE_HARDWARE (TEE or SE guarantees). (1) Add `getSecurityLevel()` API that returns which security level is supported on this Android version and the specific device. (2) For APIs that store credentials, an additional optional parameter was added that fails storing the credentials if the security level is not what is expected. ``` // Store the credentials. // Will fail if Keychain can't guarantee at least SECURE_HARDWARE level of encryption key. await Keychain.setGenericPassword(username, password, Keychain.SECURITY_LEVEL.SECURE_HARDWARE); ``` (3) StongBox support on Android 9+ (and supported devices [Pixel 3]). Co-Authored-By: mandrigin --- README.md | 16 ++- RNKeychainManager/RNKeychainManager.m | 4 +- android/build.gradle | 12 +- .../com/oblador/keychain/KeychainModule.java | 77 +++++++++-- .../com/oblador/keychain/SecurityLevel.java | 12 ++ .../keychain/cipherStorage/CipherStorage.java | 16 ++- .../CipherStorageFacebookConceal.java | 21 ++- .../CipherStorageKeystoreAESCBC.java | 127 +++++++++++++++--- index.js | 54 ++++++-- 9 files changed, 285 insertions(+), 54 deletions(-) create mode 100644 android/src/main/java/com/oblador/keychain/SecurityLevel.java 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) ); }