Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Biometric data encryption and decryption #229

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
147 changes: 143 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -211,11 +232,22 @@ rnBiometrics.biometricKeysExist()
console.log('Keys do not exist or were deleted')
}
})

ReactNativeBiometrics.biometricEncryptionKeyExists()
.then((resultObject) => {
const { keysExist } = resultObject
tao-software marked this conversation as resolved.
Show resolved Hide resolved

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__

Expand All @@ -240,13 +272,24 @@ rnBiometrics.deleteKeys()
console.log('Unsuccessful deletion because there were no keys to delete')
}
})

ReactNativeBiometrics.deleteEncryptionKey()
.then((resultObject) => {
const { keysDeleted } = resultObject
tao-software marked this conversation as resolved.
Show resolved Hide resolved

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__

Expand Down Expand Up @@ -288,6 +331,102 @@ 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.**

> `allowDeviceCredentials` is not supported for data encryption.

__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 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__

```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.**
tao-software marked this conversation as resolved.
Show resolved Hide resolved

> `allowDeviceCredentials` is not supported for data encryption.

__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.
Expand Down
57 changes: 57 additions & 0 deletions android/src/main/java/com/rnbiometrics/DecryptDataCallback.java
Original file line number Diff line number Diff line change
@@ -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");
tao-software marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
61 changes: 61 additions & 0 deletions android/src/main/java/com/rnbiometrics/EncryptDataCallback.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Loading