Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cf7e4d1
chore(inji-214): api for device supports hardware backed keystore
dhivya0413 Jul 17, 2023
342ab5a
chore(inji-214): add encrypt and decrypt functions to secure key store
tilak-puli Jul 19, 2023
aee7e9a
chore(inji-214): extract CipherBox and KeyGenerator from SecureKeystore
tilak-puli Jul 19, 2023
3c6ac81
chore(inji-214): update GenerateKeyPair to return PEM encoded public …
tilak-puli Jul 20, 2023
50cb5f4
chore(inji-214): create sign method to sign with RSA private Key
tilak-puli Jul 20, 2023
b958317
feat(inji-214): [Tilak|Dhivya] add hasAlias impl
dhivya0413 Jul 25, 2023
dc83d77
feat(inji-214): [Tilak|Dhivya] add hasAlias impl
dhivya0413 Jul 25, 2023
bebd99a
feat(inji-214): [Tilak|Dhivya] fix generate key pair pem string by us…
tilak-puli Jul 25, 2023
3dd785a
feat(inji-214): [Tilak|Dhivya] add generate HmacSha method to generat…
tilak-puli Jul 26, 2023
065d174
feat(inji-214): [Tilak|Dhivya] validate encrypted text before decryption
tilak-puli Jul 27, 2023
150bc35
feat(inji-214): [Tilak|Dhivya] resolve conflicts
dhivya0413 Jul 27, 2023
059481b
feat(inji-214): [Dhivya|Tilak] refactor encryption and sign the priva…
dhivya0413 Jul 27, 2023
98b1f66
feat(inji-214): [Tilak|Dhivya] add auth prompt for signing with keypair
tilak-puli Jul 30, 2023
7685f2f
feat(inji-214): [Tilak|Dhivya] use consistent error code for creat si…
tilak-puli Jul 30, 2023
d962d1c
feat(inji-214): [Tilak|Dhivya] enable device credential for auth in c…
tilak-puli Jul 30, 2023
4d88e61
feat(inji-214): [Tilak|Dhivya] remove device credential for auth in c…
tilak-puli Jul 30, 2023
08d9c8d
feat(inji-214): [Tilak|Dhivya] update documentation
tilak-puli Aug 1, 2023
8cf5c81
feat(inji-214): [Tilak|Dhivya] use key to generate hmac and remove da…
tilak-puli Aug 2, 2023
0a73fcf
feat(inji-214): [Tilak|Dhivya] use AndroidX for backward support and …
tilak-puli Aug 8, 2023
9b1a4d2
feat(inji-214): [Tilak|Dhivya] Use kotlin co routines to block thread…
tilak-puli Aug 9, 2023
6397a93
feat(inji-214): [Tilak|Dhivya] fix timeout based key auth is failing
tilak-puli Aug 9, 2023
6cc5a08
feat(inji-214): [Tilak|Dhivya] rename authenticateAndPerform paramete…
tilak-puli Aug 14, 2023
32c32ee
feat(INJI-214): generate hmacsha256 key
dhivya0413 Aug 11, 2023
cc8c763
feat(inji-214): [Tilak|Dhivya] throw KeyInvalidatedException if key i…
tilak-puli Aug 14, 2023
da8b19d
feat(inji-214): [Tilak|Dhivya] fix keyinfo throwing exception when ru…
tilak-puli Aug 14, 2023
6dfb6ee
feat(INJI-214): add the exception block and remove all keys
dhivya0413 Aug 14, 2023
cfd3507
feat(inji-214): add api for biometric is enabled
dhivya0413 Aug 16, 2023
80ab687
feat(inji-214): [Tilak|Dhivya] move remove all keys into keystore
tilak-puli Aug 16, 2023
62586f1
feat(inji-214): [Tilak|Dhivya] Disable Invalidation of keys on biomet…
tilak-puli Aug 17, 2023
1d26b1f
feat(inji-214): [Tilak|Dhivya] Handle user not auth exception for sig…
tilak-puli Aug 17, 2023
610df42
feat(inji-214): [Tilak|Dhivya] Add a api to update biometric popup text
tilak-puli Aug 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 87 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# react-native-secure-keystore
A React native library helps to securelys store keys
A React native library helps to securely store keys in Hardware Key Store and use them to create signatures.

`note: This library only supported for android.`

## Installation

```sh
Expand All @@ -8,14 +11,95 @@ npm install react-native-secure-keystore

## Usage

1. for RSA based Key Pair

```js
import SecureKeyStore from "react-native-secure-keystore";

// ...

if(!SecureKeyStore.deviceSupportsHardware) {
return
}

const alias = "1234ab";
const data = "any data";

const publicKey = await SecureKeyStore.generateKeyPair(alias);

const signature = await SecureKeyStore.sign(alias, data)

```


2. for symmetric key

```js
import { multiply } from "react-native-secure-keystore";
import SecureKeyStore from "react-native-secure-keystore";

// ...

const result = await multiply(3, 7);
if(!SecureKeyStore.deviceSupportsHardware) {
return
}

const alias = "1234ab";
const data = "any data";

await SecureKeyStore.generateKey(alias);

const encryptedData = await SecureKeyStore.encryptData(alias, data)
const decryptedData = await SecureKeyStore.decryptData(alias, encryptedData)

```


## API documentation

### deviceSupportsHardware

`deviceSupportsHardware() => boolean`

Check if the device supports hardware key store


### generateKey

`generateKey(alias: string) => void`

generates a symmetric key for encryption and decryption

### generateKeyPair

`generateKey(alias: string) => string`

generates a asymmetric RSA key Pair for signing

### encryptData

`encryptData(alias: string, data: string) => string`

Encrypts the given data using the key that is assigned to the alias. Returns back encrypted data as a string

### decryptData

`decryptData(alias: string, encryptionText: string) => string`

Decrypts the given encryptionText using the key that is assigned to the alias. Returns back the data as a string

### sign

`sign(alias: string, data: string) => string`

Create a signature for the given data using the key that is assigned to the alias. Returns back the signature as a string

### hasAlias

`hasAlias(alias: string) => boolean`

Check if the given alias is present in the key store


## Contributing

See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
Expand Down
3 changes: 3 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.madgag.spongycastle:core:1.56.0.0'
implementation 'com.madgag.spongycastle:prov:1.56.0.0'
implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.56.0.0'
// From node_modules
}

Expand Down
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SecureKeystore_kotlinVersion=1.7.0
SecureKeystore_minSdkVersion=21
SecureKeystore_minSdkVersion=23
SecureKeystore_targetSdkVersion=31
SecureKeystore_compileSdkVersion=31
SecureKeystore_ndkversion=21.4.7075529
1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativesecurekeystore">

<uses-permission android:name="android.permission.USE_BIOMETRIC" />
</manifest>
13 changes: 13 additions & 0 deletions android/src/main/java/com/reactnativesecurekeystore/CipherBox.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.reactnativesecurekeystore

import com.reactnativesecurekeystore.dto.EncryptedOutput
import java.security.Key
import java.security.PrivateKey
import java.security.Signature

interface CipherBox {
fun encryptData(key: Key, data: String): EncryptedOutput
fun decryptData(key: Key, encryptedOutput: EncryptedOutput): ByteArray
fun createSignature(key: PrivateKey, data: String): Signature
fun generateHmacSha(data: String): ByteArray
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.reactnativesecurekeystore

import android.security.keystore.KeyProperties
import com.reactnativesecurekeystore.dto.EncryptedOutput
import java.security.Key
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.Signature
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec


const val CIPHER_ALGORITHM =
"${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/${KeyProperties.ENCRYPTION_PADDING_NONE}"
const val GCM_TAG_LEN = 128
const val SIGN_ALGORITHM = "SHA256with${KeyProperties.KEY_ALGORITHM_RSA}"
const val HMAC_ALGORITHM = "SHA256"

class CipherBoxImpl : CipherBox {
override fun encryptData(key: Key, data: String): EncryptedOutput {
val cipher = Cipher.getInstance(CIPHER_ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, key)

val encryptedData = cipher.doFinal(data.toByteArray());

return EncryptedOutput(encryptedData, cipher.iv)
}

override fun createSignature(key: PrivateKey, data: String): Signature {
val hash = data.toByteArray(charset("UTF8"))
val signature = Signature.getInstance(SIGN_ALGORITHM).run {
initSign(key)
update(hash)
this
}

return signature
}

override fun generateHmacSha(data: String): ByteArray {
val messageDigest = MessageDigest.getInstance(HMAC_ALGORITHM)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope we know that HMAC SHA256 is different from SHA256. This implementation is simply calculating SHA256 but not HMAC SHA256. HMAC would require a AES key additionally.

messageDigest.update(data.toByteArray())
return messageDigest.digest()
}

override fun decryptData(key: Key, encryptedOutput: EncryptedOutput): ByteArray {
val cipher = Cipher.getInstance(CIPHER_ALGORITHM)

val spec = GCMParameterSpec(GCM_TAG_LEN, encryptedOutput.iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)

return cipher.doFinal(encryptedOutput.encryptedData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.reactnativesecurekeystore

import android.os.Build
import android.security.keystore.KeyInfo
import android.util.Log
import java.security.GeneralSecurityException
import java.util.concurrent.atomic.AtomicBoolean
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory


class DeviceCapability(private val secureKeystore: SecureKeystore, private val KeyGenerator: KeyGeneratorImpl) {
private val mutex = Object()

@Transient
var isSupportsSecureHardware: AtomicBoolean? = null

fun supportsHardwareKeyStore(): Boolean {
if (isSupportsSecureHardware != null) return isSupportsSecureHardware!!.get()

synchronized(mutex) {
/** double check if it has value in sync block */
if (isSupportsSecureHardware != null) return isSupportsSecureHardware!!.get()
// Check the key stored in secure hardware using temporary key alias, and removed after checked
val key = KeyGenerator.generateKey(CHECK_HARDWARE_SUPPORT_KEY_ALIAS)
isSupportsSecureHardware =
AtomicBoolean(getSecurityLevel(key) == DeviceSecurityLevel.SECURE_HARDWARE)
Log.i("SecureStorage", "Device Supports Hardware $isSupportsSecureHardware")
secureKeystore.removeKey(CHECK_HARDWARE_SUPPORT_KEY_ALIAS)
return isSupportsSecureHardware!!.get()
}
}

/** Get the supported level of security for provided Key instance. */
@Throws(GeneralSecurityException::class)
fun getSecurityLevel(key: SecretKey): DeviceSecurityLevel {
val keyInfo: KeyInfo = getKeyInfo(key)

val insideSecureHardware: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
/** SecurityLevel 1 indicates SECURITY_LEVEL_TRUSTED_ENVIRONMENT
* SecurityLevel 2 indicates SECURITY_LEVEL_STRONGBOX */
(keyInfo.securityLevel == 1) or (keyInfo.securityLevel == 2)
} else {
keyInfo.isInsideSecureHardware()
}
Log.i("keystore", "Device has secure Hardware $insideSecureHardware")
if (insideSecureHardware) {
return DeviceSecurityLevel.SECURE_HARDWARE
}
return DeviceSecurityLevel.SECURE_SOFTWARE
}

/** Get information about provided key. */
@Throws(GeneralSecurityException::class)
fun getKeyInfo(key: SecretKey): KeyInfo {
Log.i("keystore", "KeyInfo Details$key -> ${key.algorithm}")
val secretKeyFactory = SecretKeyFactory.getInstance(key.algorithm, KEYSTORE_TYPE)
return secretKeyFactory.getKeySpec(
key as SecretKey?,
KeyInfo::class.java
) as KeyInfo
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.reactnativesecurekeystore

/** Minimal required level of the security implementation. */
enum class DeviceSecurityLevel {
/** Requires for the key to be stored in the Android Keystore, separate from the encrypted data. */
SECURE_SOFTWARE,

/** Requires for the key to be stored on a secure hardware (Trusted Execution Environment or Secure Environment). */
SECURE_HARDWARE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.reactnativesecurekeystore

import java.security.KeyPair
import javax.crypto.SecretKey

interface KeyGenerator {
fun generateKey(alias: String): SecretKey
fun generateKeyPair(alias: String): KeyPair
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.reactnativesecurekeystore

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.security.keystore.KeyProperties.*
import java.security.KeyPair
import java.security.KeyPairGenerator
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey

const val KEY_PAIR_KEY_SIZE = 4096
const val KEY_AUTH_TIMEOUT = 10 * 60 * 1000

class KeyGeneratorImpl : com.reactnativesecurekeystore.KeyGenerator {
private val keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM_AES, KEYSTORE_TYPE)
private val keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, KEYSTORE_TYPE)

/** Generate secret key and store it in AndroidKeystore */
override fun generateKey(alias: String): SecretKey {
keyGenerator.init(
getKeyGenSpecBuilder(alias).build()
)

// Assumption: Generate Key also stores Key in Keystore
return keyGenerator.generateKey()
}

/** Generate a new key pair */
override fun generateKeyPair(alias: String): KeyPair {
keyPairGenerator.initialize(
getKeyPairGenSpecBuilder(alias).build()
)

return keyPairGenerator.generateKeyPair()
}

private fun getKeyGenSpecBuilder(alias: String): KeyGenParameterSpec.Builder {
val purposes = PURPOSE_DECRYPT or PURPOSE_ENCRYPT or PURPOSE_SIGN

return KeyGenParameterSpec.Builder(alias, purposes)
.setKeySize(ENCRYPTION_KEY_SIZE)
.setBlockModes(BLOCK_MODE_GCM)
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
}

private fun getKeyPairGenSpecBuilder(alias: String): KeyGenParameterSpec.Builder {
val purposes = PURPOSE_ENCRYPT or PURPOSE_DECRYPT or PURPOSE_SIGN or PURPOSE_VERIFY

return KeyGenParameterSpec.Builder(alias, purposes)
.setKeySize(KEY_PAIR_KEY_SIZE)
.setDigests(DIGEST_SHA256, DIGEST_SHA512, DIGEST_SHA1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should support SHA1 digest

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. I will remove it.

.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_PKCS1)
.setSignaturePaddings(SIGNATURE_PADDING_RSA_PKCS1)
.setUserAuthenticationRequired(true)
}
}
Loading