From 67c235ea9ae541d10afd2f0fd9e1b41f1b8b74ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=BCntzler?= Date: Tue, 8 Nov 2022 14:45:48 +0100 Subject: [PATCH 1/8] added Data encryption --- README.md | 143 ++++++++- .../com/rnbiometrics/DecryptDataCallback.java | 57 ++++ .../com/rnbiometrics/EncryptDataCallback.java | 61 ++++ .../rnbiometrics/ReactNativeBiometrics.java | 162 ++++++++++ .../ReactNativeBiometricsPackage.java | 11 + index.ts | 294 +++++++++++++----- ios/ReactNativeBiometrics.m | 238 ++++++++++++-- 7 files changed, 852 insertions(+), 114 deletions(-) create mode 100644 android/src/main/java/com/rnbiometrics/DecryptDataCallback.java create mode 100644 android/src/main/java/com/rnbiometrics/EncryptDataCallback.java diff --git a/README.md b/README.md index b30b189..661940e 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,28 @@ rnBiometrics.createKeys() }) ``` -### biometricKeysExist() +### CreateEncryptionKey() + +Performs platform dependent setup for symmetric encryption of local-only secrets that will be stored in the device keystore (AES-GCM on Android, RSA-OAEP-SHA512-wrapped AES-GCM on iOS.) Returns a promise that resolves to the success/failure status. + +__Result Object__ + +| Property | Type | Description | +| --- | --- | --- | +| success | bool | A boolean indicating if the biometric prompt succeeded, `false` if the users cancels the biometrics prompt | + +```js +import ReactNativeBiometrics from 'react-native-biometrics' + +ReactNativeBiometrics.createEncryptionKey('Confirm fingerprint') + .then((resultObject) => { + const { success } = resultObject + console.log(success) + }) +``` + + +### biometricKeysExist(), biometricEncryptionKeyExists() Detects if keys have already been generated and exist in the keystore. Returns a `Promise` that resolves to an object indicating details about the keys. @@ -211,11 +232,22 @@ rnBiometrics.biometricKeysExist() console.log('Keys do not exist or were deleted') } }) + +ReactNativeBiometrics.biometricEncryptionKeyExists() + .then((resultObject) => { + const { keysExist } = resultObject + + if (keysExist) { + console.log('Encryption key exist') + } else { + console.log('Encryption key does not exist or was deleted') + } + }) ``` -### deleteKeys() +### deleteKeys() , deleteEncryptionKey() -Deletes the generated keys from the device keystore. Returns a `Promise` that resolves to an object indicating details about the deletion. +Deletes the generated key(s) from the device keystore. Returns a `Promise` that resolves to an object indicating details about the deletion. __Result Object__ @@ -240,13 +272,24 @@ rnBiometrics.deleteKeys() console.log('Unsuccessful deletion because there were no keys to delete') } }) + +ReactNativeBiometrics.deleteEncryptionKey() + .then((resultObject) => { + const { keysDeleted } = resultObject + + if (keysDeleted) { + console.log('Successful deletion') + } else { + console.log('Unsuccessful deletion because there were no keys to delete') + } + }) ``` ### createSignature(options) Prompts the user for their fingerprint or face id in order to retrieve the private key from the keystore, then uses the private key to generate a RSA PKCS#1v1.5 SHA 256 signature. Returns a `Promise` that resolves to an object with details about the signature. -**NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices. +**NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.** __Options Object__ @@ -288,6 +331,98 @@ rnBiometrics.createSignature({ }) ``` +### encryptData(options) + +Prompts the user for their fingerprint or face id in order to retrieve the key from the keystore, then uses it to encrypt the data. Returns a `Promise` that resolves to an object with the encrypted data and IV. + +**NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.** + +__Options Object__ + +| Parameter | Type | Description | iOS | Android | +| --- | --- | --- | --- | --- | +| promptMessage | string | Message that will be displayed in the fingerprint or face id prompt | ✔ | ✔ | +| payload | string | String of data to be encrypted | ✔ | ✔ | +| cancelButtonText | string | Text to be displayed for the cancel button on biometric prompts, defaults to `Cancel` | ✖ | ✔ | + +__Result Object__ + +| Property | Type | Description | +| --- | --- | --- | +| success | bool | A boolean indicating if the process was successful, `false` if the users cancels the biometrics prompt | +| encrypted | string | A base64 encoded string representing the encrypted data. `undefined` if the process was not successful. | +| iv | string | A base64 encoded string representing the AES initalisation vector. `undefined` if the process was not successful. | +| error | string | An error message indicating reasons why signature creation failed. `undefined` if there is no error. | + +__Example__ + +```js +import ReactNativeBiometrics from 'react-native-biometrics' + +let payload = 'hunter2' + +ReactNativeBiometrics.encryptData({ + promptMessage: 'Save password', + payload: payload + }) + .then((resultObject) => { + const { success, encrypted, iv } = resultObject + + if (success) { + console.log(encrypted, iv) + myStorageApi.set("encryptedPassword", encrypted) + myStorageApi.set("passwordIV", iv) + } + }) +``` + + +### decryptData(options) + +Prompts the user for their fingerprint or face id in order to retrieve the key from the keystore, then uses it to decrypt the payload with the supplied IV. Returns a `Promise` that resolves to an object with the decrypted data. + +**NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.** + +__Options Object__ + +| Parameter | Type | Description | iOS | Android | +| --- | --- | --- | --- | --- | +| promptMessage | string | Message that will be displayed in the fingerprint or face id prompt | ✔ | ✔ | +| payload | string | Base64 encoded data to decrypt | ✔ | ✔ | +| iv | string | Base64 encoded iv used to encrypt the data | ✔ | ✔ | +| cancelButtonText | string | Text to be displayed for the cancel button on biometric prompts, defaults to `Cancel` | ✖ | ✔ | + +__Result Object__ + +| Property | Type | Description | +| --- | --- | --- | +| success | bool | A boolean indicating if the process was successful, `false` if the users cancels the biometrics prompt | +| decrypted | string | A string representing the decrypted data. `undefined` if the process was not successful. | +| error | string | An error message indicating reasons why signature creation failed. `undefined` if there is no error. | + +__Example__ + +```js +import ReactNativeBiometrics from 'react-native-biometrics' + +let payload = myStorageApi.get("encryptedPassword") +let iv = myStorageApi.get("passwordIV") + +ReactNativeBiometrics.decryptData({ + promptMessage: 'Load password', + payload: payload, + iv: iv + }) + .then((resultObject) => { + const { success, decrypted } = resultObject + + if (success) { + console.log(decrypted) + //use password to log in + } + }) +``` + ### simplePrompt(options) Prompts the user for their fingerprint or face id. Returns a `Promise` that resolves if the user provides a valid biometrics or cancel the prompt, otherwise the promise rejects. diff --git a/android/src/main/java/com/rnbiometrics/DecryptDataCallback.java b/android/src/main/java/com/rnbiometrics/DecryptDataCallback.java new file mode 100644 index 0000000..be7298d --- /dev/null +++ b/android/src/main/java/com/rnbiometrics/DecryptDataCallback.java @@ -0,0 +1,57 @@ +package com.rnbiometrics; + +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; + +import java.nio.charset.Charset; + +import javax.crypto.Cipher; + +public class DecryptDataCallback extends BiometricPrompt.AuthenticationCallback { + private Promise promise; + private String payload; + + public DecryptDataCallback(Promise promise, String payload) { + super(); + this.promise = promise; + this.payload = payload; + } + + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_USER_CANCELED ) { + WritableMap resultMap = new WritableNativeMap(); + resultMap.putBoolean("success", false); + resultMap.putString("error", "User cancellation"); + this.promise.resolve(resultMap); + } else { + this.promise.reject(errString.toString(), errString.toString()); + } + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + + try { + BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject(); + Cipher cryptoCipher = cryptoObject.getCipher(); + byte[] decrypted = cryptoCipher.doFinal(Base64.decode(payload, Base64.DEFAULT)); + String encoded = new String(decrypted, Charset.forName("UTF-8")); + + WritableMap resultMap = new WritableNativeMap(); + resultMap.putBoolean("success", true); + resultMap.putString("decrypted", encoded); + promise.resolve(resultMap); + } catch (Exception e) { + promise.reject("Error encrypting data: " + e.getMessage(), "Error encrypting data"); + } + } +} diff --git a/android/src/main/java/com/rnbiometrics/EncryptDataCallback.java b/android/src/main/java/com/rnbiometrics/EncryptDataCallback.java new file mode 100644 index 0000000..f6e7ba9 --- /dev/null +++ b/android/src/main/java/com/rnbiometrics/EncryptDataCallback.java @@ -0,0 +1,61 @@ +package com.rnbiometrics; + +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; + +import java.security.Signature; + +import javax.crypto.Cipher; + +public class EncryptDataCallback extends BiometricPrompt.AuthenticationCallback { + private Promise promise; + private String payload; + + public EncryptDataCallback(Promise promise, String payload) { + super(); + this.promise = promise; + this.payload = payload; + } + + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_USER_CANCELED ) { + WritableMap resultMap = new WritableNativeMap(); + resultMap.putBoolean("success", false); + resultMap.putString("error", "User cancellation"); + this.promise.resolve(resultMap); + } else { + this.promise.reject(errString.toString(), errString.toString()); + } + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + + try { + BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject(); + Cipher cryptoCipher = cryptoObject.getCipher(); + byte[] encrypted = cryptoCipher.doFinal(this.payload.getBytes()); + String encryptedString = Base64.encodeToString(encrypted, Base64.DEFAULT) + .replaceAll("\r", "") + .replaceAll("\n", ""); + String encodedIV = Base64.encodeToString(cryptoCipher.getIV(), Base64.DEFAULT); + + WritableMap resultMap = new WritableNativeMap(); + resultMap.putBoolean("success", true); + resultMap.putString("encrypted", encryptedString); + resultMap.putString("iv", encodedIV); + promise.resolve(resultMap); + } catch (Exception e) { + promise.reject("Error encrypting data: " + e.getMessage(), "Error encrypting data"); + } + } +} diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java index 624ecd9..b767bd1 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java @@ -30,6 +30,11 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + /** * Created by brandon on 4/5/18. */ @@ -37,6 +42,8 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule { protected String biometricKeyAlias = "biometric_key"; + protected String biometricEncryptionKeyAlias = "biometric_encryption_key"; + protected int encryptionKeySize = 256; public ReactNativeBiometrics(ReactApplicationContext reactContext) { super(reactContext); @@ -125,6 +132,34 @@ private boolean isCurrentSDKMarshmallowOrLater() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } + @ReactMethod + public void createEncryptionKey(Promise promise) { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + throw new Exception("Cannot generate keys on android versions below 6.0"); + } + deleteBiometricEncryptionKey(); + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder( + biometricEncryptionKeyAlias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(encryptionKeySize) + .setUserAuthenticationRequired(true) + .build(); + KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + keyGenerator.init(keyGenParameterSpec); + keyGenerator.generateKey(); + + WritableMap resultMap = new WritableNativeMap(); + resultMap.putBoolean("success", true); //Not returning the key itself since it's symmetric + promise.resolve(resultMap); + } catch (Exception e) { + ReactNativeBiometricsPackage.rejectWithThrowable(promise, "Error generating encryption key", e); + } + } + @ReactMethod public void deleteKeys(Promise promise) { if (doesBiometricKeyExist()) { @@ -144,6 +179,99 @@ public void deleteKeys(Promise promise) { } } + @ReactMethod + public void deleteEncryptionKey(Promise promise) { + boolean deletionSuccessful = false; + if (doesBiometricEncryptionKeyExist()) { + deletionSuccessful = deleteBiometricEncryptionKey(); + if (!deletionSuccessful) { + promise.reject("Error deleting biometric encryption key", "Error deleting biometric encryption key"); + } + } + WritableMap resultMap = new WritableNativeMap(); + resultMap.putBoolean("keysDeleted", deletionSuccessful); + promise.resolve(resultMap); + } + + @ReactMethod + public void encryptData(final ReadableMap params, final Promise promise) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + throw new Exception("Cannot generate keys on android versions below 6.0"); + } + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(biometricEncryptionKeyAlias, null)); + + BiometricPrompt biometricPrompt = new BiometricPrompt( + (FragmentActivity) getCurrentActivity(), + Executors.newSingleThreadExecutor(), + new EncryptDataCallback(promise, params.getString("payload")) + ); + + PromptInfo promptInfo = new PromptInfo.Builder() + .setDeviceCredentialAllowed(false) + .setNegativeButtonText(params.getString("cancelButtonText")) + .setTitle(params.getString("promptMessage")) + .build(); + biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher)); + } catch (Exception e) { + ReactNativeBiometricsPackage.rejectWithThrowable(promise, "Error encrypting data", e); + } + } + } + ); + } + + /* + Decrypts a base64 encoded `payload` with the given base64 encoded `iv`, using the provided `cancelButtonText` and `promptMessage` strings on the biometric prompt + */ + @ReactMethod + public void decryptData(final ReadableMap params, final Promise promise) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + throw new Exception("Cannot generate keys on android versions below 6.0"); + } + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + cipher.init( + Cipher.DECRYPT_MODE, + (SecretKey) keyStore.getKey(biometricEncryptionKeyAlias, null), + new GCMParameterSpec(128, Base64.decode(params.getString("iv"), Base64.DEFAULT)) + ); + + BiometricPrompt biometricPrompt = new BiometricPrompt( + (FragmentActivity) getCurrentActivity(), + Executors.newSingleThreadExecutor(), + new DecryptDataCallback(promise, params.getString("payload")) + ); + + PromptInfo promptInfo = new PromptInfo.Builder() + .setDeviceCredentialAllowed(false) + .setNegativeButtonText(params.getString("cancelButtonText")) + .setTitle(params.getString("promptMessage")) + .build(); + biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher)); + } catch (Exception e) { + ReactNativeBiometricsPackage.rejectWithThrowable(promise, "Error decrypting data", e); + } + } + } + ); + } + @ReactMethod public void createSignature(final ReadableMap params, final Promise promise) { if (isCurrentSDKMarshmallowOrLater()) { @@ -245,6 +373,28 @@ public void biometricKeysExist(Promise promise) { } } + @ReactMethod + public void biometricEncryptionKeyExists(Promise promise) { + try { + WritableMap resultMap = new WritableNativeMap(); + resultMap.putBoolean("keysExist", doesBiometricEncryptionKeyExist()); + promise.resolve(resultMap); + } catch (Exception e) { + ReactNativeBiometricsPackage.rejectWithThrowable(promise, "Error checking for key", e); + } + } + + protected boolean doesBiometricEncryptionKeyExist() { + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + return keyStore.containsAlias(biometricEncryptionKeyAlias); + } catch (Exception e) { + return false; + } + } + protected boolean doesBiometricKeyExist() { try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); @@ -256,6 +406,18 @@ protected boolean doesBiometricKeyExist() { } } + protected boolean deleteBiometricEncryptionKey() { + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + keyStore.deleteEntry(biometricEncryptionKeyAlias); + return true; + } catch (Exception e) { + return false; + } + } + protected boolean deleteBiometricKey() { try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometricsPackage.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometricsPackage.java index 92174e1..0f69294 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometricsPackage.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometricsPackage.java @@ -2,9 +2,12 @@ import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -28,4 +31,12 @@ public List createNativeModules( return modules; } + + public static void rejectWithThrowable(final Promise promise, final String message, final Throwable e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String error = String.format("%s: %s\n%s", message, e.getMessage(), sw.toString()); + promise.reject(error, error); + } } diff --git a/index.ts b/index.ts index ef8f260..0c78873 100644 --- a/index.ts +++ b/index.ts @@ -35,6 +35,13 @@ interface CreateSignatureOptions { cancelButtonText?: string } +interface DecryptDataOptions { + promptMessage: string + payload: string + iv: string + cancelButtonText?: string +} + interface CreateSignatureResult { success: boolean signature?: string @@ -47,6 +54,19 @@ interface SimplePromptOptions { cancelButtonText?: string } +interface EncryptionResult { + success: boolean + encrypted?: string + iv?: string + error?: string +} + +interface DecryptionResult { + success: boolean + decrypted?: string + error?: string +} + interface SimplePromptResult { success: boolean error?: string @@ -86,7 +106,24 @@ export module ReactNativeBiometricsLegacy { * @returns {Promise} Promise that resolves to object with details about the newly generated public key */ export function createKeys(): Promise { - return new ReactNativeBiometrics().createKeys() + return new ReactNativeBiometrics().createKeys(); + } + + /** + * Creates a symmetric encryption key, returns promise that resolves to a boolean indicating success/failure. + * @returns {Promise} Promise that resolves to object with success status + */ + export function createEncryptionKey(): Promise { + return new ReactNativeBiometrics().createEncryptionKey() + } + + /** + * Returns promise that resolves to an object with object.keysExists = true | false + * indicating if the keys were found to exist or not + * @returns {Promise} Promise that resolves to object with details aobut the existence of keys + */ + export function biometricEncryptionKeyExists(): Promise { + return new ReactNativeBiometrics().biometricEncryptionKeyExists() } /** @@ -98,6 +135,15 @@ export module ReactNativeBiometricsLegacy { return new ReactNativeBiometrics().biometricKeysExist() } + /** + * Returns promise that resolves to an object with true | false + * indicating if the encryption key properly deleted + * @returns {Promise} Promise that resolves to an object with details about the deletion + */ + export function deleteEncryptionKey(): Promise { + return new ReactNativeBiometrics().deleteEncryptionKey() + } + /** * Returns promise that resolves to an object with true | false * indicating if the keys were properly deleted @@ -107,6 +153,34 @@ export module ReactNativeBiometricsLegacy { return new ReactNativeBiometrics().deleteKeys() } + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.decrypted as UTF-8 string + * @param {Object} decryptDataOptions + * @param {string} decryptDataOptions.promptMessage + * @param {string} decryptDataOptions.payload: base64 encoded data + * @param {string} decryptDataOptions.iv: base64 encoded IV + * @param {string} createSignatureOptions.cancelButtonText (Android only) + * @returns {Promise} Promise that resolves to an object containing encryption details + */ + export function decryptData(decryptDataOptions: DecryptDataOptions): Promise { + return new ReactNativeBiometrics().decryptData(decryptDataOptions) + } + + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.encrypted and object.iv, + * which is the base64 encoded encrypted payload and its encryption IV respectively. + * @param {Object} createSignatureOptions + * @param {string} createSignatureOptions.promptMessage + * @param {string} createSignatureOptions.payload: Should be ASCII or UTF-8 + * @param {string} createSignatureOptions.cancelButtonText (Android only) + * @returns {Promise} Promise that resolves to an object containing encryption details + */ + export function encryptData(createSignatureOptions: CreateSignatureOptions): Promise { + return new ReactNativeBiometrics().encryptData(createSignatureOptions) + } + /** * Prompts user with biometrics dialog using the passed in prompt message and * returns promise that resolves to an object with object.signature, @@ -135,90 +209,152 @@ export module ReactNativeBiometricsLegacy { } export default class ReactNativeBiometrics { - allowDeviceCredentials = false - - /** - * @param {Object} rnBiometricsOptions - * @param {boolean} rnBiometricsOptions.allowDeviceCredentials - */ - constructor(rnBiometricsOptions?: RNBiometricsOptions) { - const allowDeviceCredentials = rnBiometricsOptions?.allowDeviceCredentials ?? false - this.allowDeviceCredentials = allowDeviceCredentials - } + allowDeviceCredentials = false - /** - * Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID - * @returns {Promise} Promise that resolves to an object with details about biometrics available - */ - isSensorAvailable(): Promise { - return bridge.isSensorAvailable({ - allowDeviceCredentials: this.allowDeviceCredentials - }) - } + /** + * @param {Object} rnBiometricsOptions + * @param {boolean} rnBiometricsOptions.allowDeviceCredentials + */ + constructor(rnBiometricsOptions?: RNBiometricsOptions) { + const allowDeviceCredentials = rnBiometricsOptions?.allowDeviceCredentials ?? false + this.allowDeviceCredentials = allowDeviceCredentials + } - /** - * Creates a public private key pair,returns promise that resolves to - * an object with object.publicKey, which is the public key of the newly generated key pair - * @returns {Promise} Promise that resolves to object with details about the newly generated public key - */ - createKeys(): Promise { - return bridge.createKeys({ - allowDeviceCredentials: this.allowDeviceCredentials - }) - } + /** + * Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID + * @returns {Promise} Promise that resolves to an object with details about biometrics available + */ + isSensorAvailable(): Promise { + return bridge.isSensorAvailable({ + allowDeviceCredentials: this.allowDeviceCredentials + }) + } - /** - * Returns promise that resolves to an object with object.keysExists = true | false - * indicating if the keys were found to exist or not - * @returns {Promise} Promise that resolves to object with details aobut the existence of keys - */ - biometricKeysExist(): Promise { - return bridge.biometricKeysExist() - } + /** + * Creates a public private key pair,returns promise that resolves to + * an object with object.publicKey, which is the public key of the newly generated key pair + * @returns {Promise} Promise that resolves to object with details about the newly generated public key + */ + createKeys(): Promise { + return bridge.createKeys({ + allowDeviceCredentials: this.allowDeviceCredentials + }) + } - /** - * Returns promise that resolves to an object with true | false - * indicating if the keys were properly deleted - * @returns {Promise} Promise that resolves to an object with details about the deletion - */ - deleteKeys(): Promise { - return bridge.deleteKeys() - } + /** + * Creates a symmetric encryption key, returns promise that resolves to a boolean indicating success/failure. + * @returns {Promise} Promise that resolves to object with success status + */ + createEncryptionKey(): Promise { + return bridge.createEncryptionKey() + } - /** - * Prompts user with biometrics dialog using the passed in prompt message and - * returns promise that resolves to an object with object.signature, - * which is cryptographic signature of the payload - * @param {Object} createSignatureOptions - * @param {string} createSignatureOptions.promptMessage - * @param {string} createSignatureOptions.payload - * @returns {Promise} Promise that resolves to an object cryptographic signature details - */ - createSignature(createSignatureOptions: CreateSignatureOptions): Promise { - createSignatureOptions.cancelButtonText = createSignatureOptions.cancelButtonText ?? 'Cancel' - - return bridge.createSignature({ - allowDeviceCredentials: this.allowDeviceCredentials, - ...createSignatureOptions - }) + /** + * Returns promise that resolves to an object with object.keysExists = true | false + * indicating if the keys were found to exist or not + * @returns {Promise} Promise that resolves to object with details aobut the existence of keys + */ + biometricEncryptionKeyExists(): Promise { + return bridge.biometricEncryptionKeyExists() + } + + /** + * Returns promise that resolves to an object with object.keysExists = true | false + * indicating if the keys were found to exist or not + * @returns {Promise} Promise that resolves to object with details aobut the existence of keys + */ + biometricKeysExist(): Promise { + return bridge.biometricKeysExist() + } + + /** + * Returns promise that resolves to an object with true | false + * indicating if the encryption key properly deleted + * @returns {Promise} Promise that resolves to an object with details about the deletion + */ + deleteEncryptionKey(): Promise { + return bridge.deleteEncryptionKey() + } + + /** + * Returns promise that resolves to an object with true | false + * indicating if the keys were properly deleted + * @returns {Promise} Promise that resolves to an object with details about the deletion + */ + deleteKeys(): Promise { + return bridge.deleteKeys() + } + + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.decrypted as UTF-8 string + * @param {Object} decryptDataOptions + * @param {string} decryptDataOptions.promptMessage + * @param {string} decryptDataOptions.payload: base64 encoded data + * @param {string} decryptDataOptions.iv: base64 encoded IV + * @param {string} createSignatureOptions.cancelButtonText (Android only) + * @returns {Promise} Promise that resolves to an object containing encryption details + */ + decryptData(decryptDataOptions: DecryptDataOptions): Promise { + if (!decryptDataOptions.cancelButtonText) { + decryptDataOptions.cancelButtonText = 'Cancel' } - /** - * Prompts user with biometrics dialog using the passed in prompt message and - * returns promise that resolves to an object with object.success = true if the user passes, - * object.success = false if the user cancels, and rejects if anything fails - * @param {Object} simplePromptOptions - * @param {string} simplePromptOptions.promptMessage - * @param {string} simplePromptOptions.fallbackPromptMessage - * @returns {Promise} Promise that resolves an object with details about the biometrics result - */ - simplePrompt(simplePromptOptions: SimplePromptOptions): Promise { - simplePromptOptions.cancelButtonText = simplePromptOptions.cancelButtonText ?? 'Cancel' - simplePromptOptions.fallbackPromptMessage = simplePromptOptions.fallbackPromptMessage ?? 'Use Passcode' - - return bridge.simplePrompt({ - allowDeviceCredentials: this.allowDeviceCredentials, - ...simplePromptOptions - }) + return bridge.decryptData(decryptDataOptions) + } + + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.encrypted and object.iv, + * which is the base64 encoded encrypted payload and its encryption IV respectively. + * @param {Object} createSignatureOptions + * @param {string} createSignatureOptions.promptMessage + * @param {string} createSignatureOptions.payload: Should be ASCII or UTF-8 + * @param {string} createSignatureOptions.cancelButtonText (Android only) + * @returns {Promise} Promise that resolves to an object containing encryption details + */ + encryptData(createSignatureOptions: CreateSignatureOptions): Promise { + if (!createSignatureOptions.cancelButtonText) { + createSignatureOptions.cancelButtonText = 'Cancel' } + + return bridge.encryptData(createSignatureOptions) } + + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.signature, + * which is cryptographic signature of the payload + * @param {Object} createSignatureOptions + * @param {string} createSignatureOptions.promptMessage + * @param {string} createSignatureOptions.payload + * @returns {Promise} Promise that resolves to an object cryptographic signature details + */ + createSignature(createSignatureOptions: CreateSignatureOptions): Promise { + createSignatureOptions.cancelButtonText = createSignatureOptions.cancelButtonText ?? 'Cancel' + + return bridge.createSignature({ + allowDeviceCredentials: this.allowDeviceCredentials, + ...createSignatureOptions + }) + } + + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.success = true if the user passes, + * object.success = false if the user cancels, and rejects if anything fails + * @param {Object} simplePromptOptions + * @param {string} simplePromptOptions.promptMessage + * @param {string} simplePromptOptions.fallbackPromptMessage + * @returns {Promise} Promise that resolves an object with details about the biometrics result + */ + simplePrompt(simplePromptOptions: SimplePromptOptions): Promise { + simplePromptOptions.cancelButtonText = simplePromptOptions.cancelButtonText ?? 'Cancel' + simplePromptOptions.fallbackPromptMessage = simplePromptOptions.fallbackPromptMessage ?? 'Use Passcode' + + return bridge.simplePrompt({ + allowDeviceCredentials: this.allowDeviceCredentials, + ...simplePromptOptions + }) + } +} diff --git a/ios/ReactNativeBiometrics.m b/ios/ReactNativeBiometrics.m index 03ec388..06ba794 100644 --- a/ios/ReactNativeBiometrics.m +++ b/ios/ReactNativeBiometrics.m @@ -77,7 +77,7 @@ @implementation ReactNativeBiometrics } }; - [self deleteBiometricKey]; + [self deleteBiometricKey:biometricKeyTag]; NSError *gen_error = nil; id privateKey = CFBridgingRelease(SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyAttributes, (void *)&gen_error)); @@ -99,29 +99,91 @@ @implementation ReactNativeBiometrics }); } -RCT_EXPORT_METHOD(deleteKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - BOOL biometricKeyExists = [self doesBiometricKeyExist]; +RCT_EXPORT_METHOD(createEncryptionKey: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if ([UIDevice currentDevice].systemVersion.floatValue < 11) { + reject(@"storage_error", @"iOS 11 or higher is required to encrypt data", nil); + return; + } + + CFErrorRef error = NULL; + SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + kSecAccessControlBiometryAny|kSecAccessControlPrivateKeyUsage, &error); + if (sacObject == NULL || error != NULL) { + NSString *errorString = [NSString stringWithFormat:@"SecItemAdd can't create sacObject: %@", error]; + reject(@"storage_error", errorString, nil); + return; + } + + NSData *biometricKeyTag = [self getBiometricEncryptionKeyTag]; + NSDictionary *keyAttributes = @{ + (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom, + (id)kSecAttrKeySizeInBits: @256, + (id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave, + (id)kSecPrivateKeyAttrs: + @{ (id)kSecAttrIsPermanent: @YES, + (id)kSecAttrApplicationTag: biometricKeyTag, + (id)kSecAttrAccessControl: (__bridge_transfer id)sacObject, + }, + }; + + [self deleteBiometricKey: biometricKeyTag]; + + NSError *gen_error = nil; + SecKeyRef privateKey = (__bridge SecKeyRef) CFBridgingRelease(SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyAttributes, (void *)&gen_error)); + + if (privateKey == nil) { + NSString *message = [NSString stringWithFormat:@"Key generation error: %@", gen_error]; + reject(@"storage_error", message, nil); + return; + } + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); - if (biometricKeyExists) { - OSStatus status = [self deleteBiometricKey]; - - if (status == noErr) { NSDictionary *result = @{ - @"keysDeleted": @(YES), + @"success": @(YES), + @"pubkey": [(NSData*)CFBridgingRelease(SecKeyCopyExternalRepresentation(publicKey, nil)) base64EncodedStringWithOptions:0], }; resolve(result); - } else { - NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]]; - reject(@"deletion_error", message, nil); + }); +} + +RCT_EXPORT_METHOD(deleteKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *biometricKeyTag = [self getBiometricKeyTag]; + BOOL success = false; + if ([self doesBiometricKeyExist:biometricKeyTag]) { + OSStatus status = [self deleteBiometricKey:biometricKeyTag]; + success = status == noErr; + if (!success) { + reject(@"deletion_error", [NSString stringWithFormat:@"Key not found: %@", [self keychainErrorToString:status]], nil); + return; + } } - } else { + NSDictionary *result = @{ + @"keysDeleted": @(success), + }; + resolve(result); + }); +} + +RCT_EXPORT_METHOD(deleteEncryptionKey: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *biometricKeyTag = [self getBiometricEncryptionKeyTag]; + BOOL success = false; + if ([self doesBiometricKeyExist:biometricKeyTag]) { + OSStatus status = [self deleteBiometricKey:biometricKeyTag]; + success = status == noErr; + if (!success) { + reject(@"deletion_error", [NSString stringWithFormat:@"Key not found: %@", [self keychainErrorToString:status]], nil); + return; + } + } NSDictionary *result = @{ - @"keysDeleted": @(NO), + @"keysDeleted": @(success), }; resolve(result); - } - }); + }); } RCT_EXPORT_METHOD(createSignature: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -169,6 +231,106 @@ @implementation ReactNativeBiometrics }); } + + +RCT_EXPORT_METHOD(encryptData: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *promptMessage = [RCTConvert NSString:params[@"promptMessage"]]; + NSString *payload = [RCTConvert NSString:params[@"payload"]]; + + SecKeyRef privateKey; + OSStatus status = [self getEncryptionPrivateKey:promptMessage key:&privateKey]; + if (status != errSecSuccess) { + reject(@"storage_error", [NSString stringWithFormat:@"Key not found: %@", [self keychainErrorToString:status]], nil); + return; + } + + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + + Boolean algorithmSupported = SecKeyIsAlgorithmSupported(publicKey, kSecKeyOperationTypeEncrypt, kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM); + + if (!algorithmSupported) { + CFRelease(privateKey); + CFRelease(publicKey); + reject(@"storage_error", @"Encryption algorithm not supported", nil); + return; + } + + NSData* plainText = [payload dataUsingEncoding:NSUTF8StringEncoding]; + NSData* cipherText = nil; + CFErrorRef encryptError = NULL; + cipherText = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM, (__bridge CFDataRef)plainText, &encryptError)); + CFRelease(privateKey); + CFRelease(publicKey); + + if (!cipherText) { + NSError *err = CFBridgingRelease(encryptError); + NSString *message = [NSString stringWithFormat:@"Encryption error: %@", err]; + reject(@"encryption_error", message, nil); + } + NSString *ciphertextString = [cipherText base64EncodedStringWithOptions:0]; + NSDictionary *result = @{ + @"success": @(YES), + @"encrypted": ciphertextString, + @"iv": @"", // Not needed on iOS, encoded in the cipherText blob. Returned empty for androird interoperability + }; + resolve(result); + }); +} + +RCT_EXPORT_METHOD(decryptData: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *promptMessage = [RCTConvert NSString:params[@"promptMessage"]]; + NSString *payload = [RCTConvert NSString:params[@"payload"]]; + + SecKeyRef privateKey; + OSStatus status = [self getEncryptionPrivateKey:promptMessage key:&privateKey]; + if (status != errSecSuccess) { + reject(@"storage_error", [NSString stringWithFormat:@"Key not found: %@", [self keychainErrorToString:status]], nil); + return; + } + + Boolean algorithmSupported = SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt, kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM); + + if (!algorithmSupported) { + CFRelease(privateKey); + reject(@"storage_error", @"Encryption algorithm not supported", nil); + return; + } + + NSData *cipherText = [[NSData alloc] initWithBase64EncodedString:payload options:0]; + if (!cipherText) { + reject(@"decoding_error", @"Base64 decode failed", nil); + return; + } + + NSData *plainText = nil; + CFErrorRef decryptError = NULL; + plainText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM, (__bridge CFDataRef)cipherText, &decryptError)); + CFRelease(privateKey); + + if (!plainText) { + NSError *err = CFBridgingRelease(decryptError); + NSString *message = [NSString stringWithFormat:@"Decryption error: %@", err]; + reject(@"decryption_error", message, nil); + return; + } + + NSString *plaintextString = [[NSString alloc] initWithData:plainText encoding:NSUTF8StringEncoding]; + if (!plaintextString) { + reject(@"encoding_error", @"UTF8 encode failed", nil); + return; + } + NSDictionary *result = @{ + @"success": @(YES), + @"decrypted": plaintextString, + }; + resolve(result); + }); +} + + + RCT_EXPORT_METHOD(simplePrompt: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *promptMessage = [RCTConvert NSString:params[@"promptMessage"]]; @@ -207,34 +369,50 @@ @implementation ReactNativeBiometrics RCT_EXPORT_METHOD(biometricKeysExist: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - BOOL biometricKeyExists = [self doesBiometricKeyExist]; - - if (biometricKeyExists) { NSDictionary *result = @{ - @"keysExist": @(YES) + @"keysExist": @([self doesBiometricKeyExist: [self getBiometricKeyTag]]) }; resolve(result); - } else { + }); +} + +RCT_EXPORT_METHOD(biometricEncryptionKeyExists: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSDictionary *result = @{ - @"keysExist": @(NO) + @"keysExist": @([self doesBiometricKeyExist: [self getBiometricEncryptionKeyTag]]) }; resolve(result); - } }); } +- (OSStatus) getEncryptionPrivateKey: (NSString *) promptMessage key: (SecKeyRef *) key { + NSDictionary *query = @{ + (id)kSecClass: (id)kSecClassKey, + (id)kSecAttrApplicationTag: [self getBiometricEncryptionKeyTag], + (id)kSecAttrKeyType: (id)kSecAttrKeyTypeEC, + (id)kSecReturnRef: @YES, + (id)kSecUseOperationPrompt: promptMessage + }; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)key); + return status; +} + - (NSData *) getBiometricKeyTag { NSString *biometricKeyAlias = @"com.rnbiometrics.biometricKey"; NSData *biometricKeyTag = [biometricKeyAlias dataUsingEncoding:NSUTF8StringEncoding]; return biometricKeyTag; } -- (BOOL) doesBiometricKeyExist { - NSData *biometricKeyTag = [self getBiometricKeyTag]; +- (NSData *) getBiometricEncryptionKeyTag { + NSString *biometricKeyAlias = @"com.rnbiometrics.encryptionKey"; + NSData *biometricKeyTag = [biometricKeyAlias dataUsingEncoding:NSUTF8StringEncoding]; + return biometricKeyTag; +} + +- (BOOL) doesBiometricKeyExist: (NSData *) tag { NSDictionary *searchQuery = @{ (id)kSecClass: (id)kSecClassKey, - (id)kSecAttrApplicationTag: biometricKeyTag, - (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA, + (id)kSecAttrApplicationTag: tag, (id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIFail }; @@ -242,12 +420,10 @@ - (BOOL) doesBiometricKeyExist { return status == errSecSuccess || status == errSecInteractionNotAllowed; } --(OSStatus) deleteBiometricKey { - NSData *biometricKeyTag = [self getBiometricKeyTag]; +-(OSStatus) deleteBiometricKey: (NSData *) tag { NSDictionary *deleteQuery = @{ (id)kSecClass: (id)kSecClassKey, - (id)kSecAttrApplicationTag: biometricKeyTag, - (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA + (id)kSecAttrApplicationTag: tag, }; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery); From 2d65ea8a72853dc0f1b449c0c0950ec597657c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=BCntzler?= Date: Wed, 18 Jan 2023 13:06:16 +0100 Subject: [PATCH 2/8] Updated README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 661940e..1970941 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,8 @@ Prompts the user for their fingerprint or face id in order to retrieve the key f **NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.** +> `allowDeviceCredentials` is not supported for data encryption. + __Options Object__ | Parameter | Type | Description | iOS | Android | @@ -351,7 +353,7 @@ __Result Object__ | --- | --- | --- | | success | bool | A boolean indicating if the process was successful, `false` if the users cancels the biometrics prompt | | encrypted | string | A base64 encoded string representing the encrypted data. `undefined` if the process was not successful. | -| iv | string | A base64 encoded string representing the AES initalisation vector. `undefined` if the process was not successful. | +| iv | string | A base64 encoded string representing the AES initialization vector. `undefined` if the process was not successful. | | error | string | An error message indicating reasons why signature creation failed. `undefined` if there is no error. | __Example__ @@ -383,6 +385,8 @@ Prompts the user for their fingerprint or face id in order to retrieve the key f **NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.** +> `allowDeviceCredentials` is not supported for data encryption. + __Options Object__ | Parameter | Type | Description | iOS | Android | From 14ede78a4fb28acba8d8c4d02aec9f593fcb5a27 Mon Sep 17 00:00:00 2001 From: TAO Software Date: Mon, 23 Oct 2023 11:27:12 +0200 Subject: [PATCH 3/8] Update ios/ReactNativeBiometrics.m Fix typo Co-authored-by: Tamlyn Rhodes --- ios/ReactNativeBiometrics.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/ReactNativeBiometrics.m b/ios/ReactNativeBiometrics.m index 06ba794..3789822 100644 --- a/ios/ReactNativeBiometrics.m +++ b/ios/ReactNativeBiometrics.m @@ -272,7 +272,7 @@ @implementation ReactNativeBiometrics NSDictionary *result = @{ @"success": @(YES), @"encrypted": ciphertextString, - @"iv": @"", // Not needed on iOS, encoded in the cipherText blob. Returned empty for androird interoperability + @"iv": @"", // Not needed on iOS, encoded in the cipherText blob. Returned empty for android interoperability }; resolve(result); }); From f56a937ca72ad10a33cb0d5fed42a9b721a7ff33 Mon Sep 17 00:00:00 2001 From: TAO Software Date: Mon, 23 Oct 2023 11:28:19 +0200 Subject: [PATCH 4/8] Update README.md Fix typos Co-authored-by: Tamlyn Rhodes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1970941..f8b2177 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ ReactNativeBiometrics.encryptData({ Prompts the user for their fingerprint or face id in order to retrieve the key from the keystore, then uses it to decrypt the payload with the supplied IV. Returns a `Promise` that resolves to an object with the decrypted data. -**NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.** +**NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for decryption, it only occurs on actual devices.** > `allowDeviceCredentials` is not supported for data encryption. From 0992caae3de97a1ad0464d9e46774bffe45a89d2 Mon Sep 17 00:00:00 2001 From: TAO Software Date: Tue, 24 Oct 2023 13:29:32 +0200 Subject: [PATCH 5/8] Update android/src/main/java/com/rnbiometrics/DecryptDataCallback.java Co-authored-by: Tamlyn Rhodes --- android/src/main/java/com/rnbiometrics/DecryptDataCallback.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/rnbiometrics/DecryptDataCallback.java b/android/src/main/java/com/rnbiometrics/DecryptDataCallback.java index be7298d..170070d 100644 --- a/android/src/main/java/com/rnbiometrics/DecryptDataCallback.java +++ b/android/src/main/java/com/rnbiometrics/DecryptDataCallback.java @@ -51,7 +51,7 @@ public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationRes resultMap.putString("decrypted", encoded); promise.resolve(resultMap); } catch (Exception e) { - promise.reject("Error encrypting data: " + e.getMessage(), "Error encrypting data"); + promise.reject("Error decrypting data: " + e.getMessage(), "Error decrypting data"); } } } From 1417b9a328801bee48f725ee939f1d782a4cbb19 Mon Sep 17 00:00:00 2001 From: Sven Schwedas Date: Tue, 24 Oct 2023 13:46:44 +0200 Subject: [PATCH 6/8] Make naming more consistent --- README.md | 12 +++++----- .../rnbiometrics/ReactNativeBiometrics.java | 6 ++--- index.ts | 24 +++++++++---------- ios/ReactNativeBiometrics.m | 6 ++--- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index f8b2177..65b0cbe 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ rnBiometrics.createKeys() }) ``` -### CreateEncryptionKey() +### createEncryptionKeys() Performs platform dependent setup for symmetric encryption of local-only secrets that will be stored in the device keystore (AES-GCM on Android, RSA-OAEP-SHA512-wrapped AES-GCM on iOS.) Returns a promise that resolves to the success/failure status. @@ -198,7 +198,7 @@ __Result Object__ ```js import ReactNativeBiometrics from 'react-native-biometrics' -ReactNativeBiometrics.createEncryptionKey('Confirm fingerprint') +ReactNativeBiometrics.createEncryptionKeys('Confirm fingerprint') .then((resultObject) => { const { success } = resultObject console.log(success) @@ -206,7 +206,7 @@ ReactNativeBiometrics.createEncryptionKey('Confirm fingerprint') ``` -### biometricKeysExist(), biometricEncryptionKeyExists() +### biometricKeysExist(), biometricEncryptionKeysExist() Detects if keys have already been generated and exist in the keystore. Returns a `Promise` that resolves to an object indicating details about the keys. @@ -233,7 +233,7 @@ rnBiometrics.biometricKeysExist() } }) -ReactNativeBiometrics.biometricEncryptionKeyExists() +ReactNativeBiometrics.biometricEncryptionKeysExist() .then((resultObject) => { const { keysExist } = resultObject @@ -245,7 +245,7 @@ ReactNativeBiometrics.biometricEncryptionKeyExists() }) ``` -### deleteKeys() , deleteEncryptionKey() +### deleteKeys() , deleteEncryptionKeys() Deletes the generated key(s) from the device keystore. Returns a `Promise` that resolves to an object indicating details about the deletion. @@ -273,7 +273,7 @@ rnBiometrics.deleteKeys() } }) -ReactNativeBiometrics.deleteEncryptionKey() +ReactNativeBiometrics.deleteEncryptionKeys() .then((resultObject) => { const { keysDeleted } = resultObject diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java index d1c6056..73c7c9a 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java @@ -136,7 +136,7 @@ private boolean isCurrentSDKMarshmallowOrLater() { } @ReactMethod - public void createEncryptionKey(Promise promise) { + public void createEncryptionKeys(Promise promise) { try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { throw new Exception("Cannot generate keys on android versions below 6.0"); @@ -183,7 +183,7 @@ public void deleteKeys(Promise promise) { } @ReactMethod - public void deleteEncryptionKey(Promise promise) { + public void deleteEncryptionKeys(Promise promise) { boolean deletionSuccessful = false; if (doesBiometricEncryptionKeyExist()) { deletionSuccessful = deleteBiometricEncryptionKey(); @@ -377,7 +377,7 @@ public void biometricKeysExist(Promise promise) { } @ReactMethod - public void biometricEncryptionKeyExists(Promise promise) { + public void biometricEncryptionKeysExist(Promise promise) { try { WritableMap resultMap = new WritableNativeMap(); resultMap.putBoolean("keysExist", doesBiometricEncryptionKeyExist()); diff --git a/index.ts b/index.ts index d96ffde..185fa27 100644 --- a/index.ts +++ b/index.ts @@ -113,8 +113,8 @@ export module ReactNativeBiometricsLegacy { * Creates a symmetric encryption key, returns promise that resolves to a boolean indicating success/failure. * @returns {Promise} Promise that resolves to object with success status */ - export function createEncryptionKey(): Promise { - return new ReactNativeBiometrics().createEncryptionKey() + export function createEncryptionKeys(): Promise { + return new ReactNativeBiometrics().createEncryptionKeys() } /** @@ -122,8 +122,8 @@ export module ReactNativeBiometricsLegacy { * indicating if the keys were found to exist or not * @returns {Promise} Promise that resolves to object with details aobut the existence of keys */ - export function biometricEncryptionKeyExists(): Promise { - return new ReactNativeBiometrics().biometricEncryptionKeyExists() + export function biometricEncryptionKeysExist(): Promise { + return new ReactNativeBiometrics().biometricEncryptionKeysExist() } /** @@ -140,8 +140,8 @@ export module ReactNativeBiometricsLegacy { * indicating if the encryption key properly deleted * @returns {Promise} Promise that resolves to an object with details about the deletion */ - export function deleteEncryptionKey(): Promise { - return new ReactNativeBiometrics().deleteEncryptionKey() + export function deleteEncryptionKeys(): Promise { + return new ReactNativeBiometrics().deleteEncryptionKeys() } /** @@ -245,8 +245,8 @@ export default class ReactNativeBiometrics { * Creates a symmetric encryption key, returns promise that resolves to a boolean indicating success/failure. * @returns {Promise} Promise that resolves to object with success status */ - createEncryptionKey(): Promise { - return bridge.createEncryptionKey() + createEncryptionKeys(): Promise { + return bridge.createEncryptionKeys() } /** @@ -254,8 +254,8 @@ export default class ReactNativeBiometrics { * indicating if the keys were found to exist or not * @returns {Promise} Promise that resolves to object with details aobut the existence of keys */ - biometricEncryptionKeyExists(): Promise { - return bridge.biometricEncryptionKeyExists() + biometricEncryptionKeysExist(): Promise { + return bridge.biometricEncryptionKeysExist() } /** @@ -272,8 +272,8 @@ export default class ReactNativeBiometrics { * indicating if the encryption key properly deleted * @returns {Promise} Promise that resolves to an object with details about the deletion */ - deleteEncryptionKey(): Promise { - return bridge.deleteEncryptionKey() + deleteEncryptionKeys(): Promise { + return bridge.deleteEncryptionKeys() } /** diff --git a/ios/ReactNativeBiometrics.m b/ios/ReactNativeBiometrics.m index 3789822..be3b236 100644 --- a/ios/ReactNativeBiometrics.m +++ b/ios/ReactNativeBiometrics.m @@ -99,7 +99,7 @@ @implementation ReactNativeBiometrics }); } -RCT_EXPORT_METHOD(createEncryptionKey: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(createEncryptionKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if ([UIDevice currentDevice].systemVersion.floatValue < 11) { reject(@"storage_error", @"iOS 11 or higher is required to encrypt data", nil); @@ -167,7 +167,7 @@ @implementation ReactNativeBiometrics }); } -RCT_EXPORT_METHOD(deleteEncryptionKey: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(deleteEncryptionKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSData *biometricKeyTag = [self getBiometricEncryptionKeyTag]; BOOL success = false; @@ -376,7 +376,7 @@ @implementation ReactNativeBiometrics }); } -RCT_EXPORT_METHOD(biometricEncryptionKeyExists: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(biometricEncryptionKeysExist: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSDictionary *result = @{ @"keysExist": @([self doesBiometricKeyExist: [self getBiometricEncryptionKeyTag]]) From 9305dedd580c7cb77d8794ccb8969f80e374f76f Mon Sep 17 00:00:00 2001 From: Sven Schwedas Date: Tue, 24 Oct 2023 13:59:15 +0200 Subject: [PATCH 7/8] Document key deletion on create calls --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 65b0cbe..4793335 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,8 @@ rnBiometrics.isSensorAvailable() Generates a public private RSA 2048 key pair that will be stored in the device keystore. Returns a `Promise` that resolves to an object providing details about the keys. +⚠️ **Warning**: Subsequent calls to `createKeys()` will delete existing keys from the device keystore. + __Result Object__ | Property | Type | Description | @@ -189,6 +191,8 @@ rnBiometrics.createKeys() Performs platform dependent setup for symmetric encryption of local-only secrets that will be stored in the device keystore (AES-GCM on Android, RSA-OAEP-SHA512-wrapped AES-GCM on iOS.) Returns a promise that resolves to the success/failure status. +⚠️⚠️ **Warning**: Subsequent calls to `createEncryptionKeys()` will delete existing keys from the device keystore. As the keys are not designed to be extracted from the store, this will render all existing encrypted data inaccessible. Consider guarding calls with `biometricEncryptionKeysExist()`. + __Result Object__ | Property | Type | Description | From 9529876305f86ed841c1298b0d0f271b9a487900 Mon Sep 17 00:00:00 2001 From: Sven Schwedas Date: Tue, 24 Oct 2023 14:01:47 +0200 Subject: [PATCH 8/8] Slightly better error messages --- .../src/main/java/com/rnbiometrics/ReactNativeBiometrics.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java index 73c7c9a..dfd0222 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java @@ -204,7 +204,7 @@ public void encryptData(final ReadableMap params, final Promise promise) { public void run() { try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - throw new Exception("Cannot generate keys on android versions below 6.0"); + throw new Exception("Cannot encrypt data on android versions below 6.0"); } Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); @@ -243,7 +243,7 @@ public void decryptData(final ReadableMap params, final Promise promise) { public void run() { try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - throw new Exception("Cannot generate keys on android versions below 6.0"); + throw new Exception("Cannot decrypt data on android versions below 6.0"); } Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");