From 47f35f51233b49241cb42648a3633bdfa3370756 Mon Sep 17 00:00:00 2001 From: abhip2565 Date: Mon, 2 Mar 2026 11:18:32 +0530 Subject: [PATCH 1/3] [INJIMOB-3779] enhance readme Signed-off-by: abhip2565 --- README.md | 561 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 464 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 5b759bcd..2ed86d09 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,362 @@ # Secure-Keystore -A module to create and store keys in the Android hardware keystore, which helps with encryption, decryption, and HMAC calculation. - -## Usage as a Kotlin library (for native android) -The secure-keystore kotlin artifact (.aar) has been published to Maven. -### Adding as a Maven dependency. -- In settings.gradle.kts of your app modify the following: - ``` - dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - maven("https://oss.sonatype.org/content/repositories/snapshots/") - } - } - ``` - -- In your app's `build.gradle.kts`, add the following: - ```kotlin - dependencies { - implementation("io.mosip:secure-keystore:1.0-SNAPSHOT") - } - ``` - -The Kotlin library has been added to your project. - -## Usage as a React-Native Wrapper - -### Installation + +A Kotlin/Android library for secure key management using Android's hardware-backed keystore. Provides encryption, decryption, signing, and HMAC operations with biometric authentication support. + +--- + +## Feature Support + +The Secure-Keystore library provides hardware-backed key management for symmetric, asymmetric, and HMAC keys using Android’s Keystore system. + +- **AES keys** support encryption and decryption with optional biometric protection. +- **RSA and EC P-256 keys** support digital signing operations. Public keys can be exported, while private keys remain non-exportable within the keystore. +- **HMAC keys** support HMAC-SHA256 message authentication using hardware-protected symmetric keys. +- All primary cryptographic keys (AES, RSA, EC P-256, HMAC) are generated and stored within the Android hardware-backed keystore when available. +- **Generic key storage** is supported for externally provided key pairs in encrypted preferences (e.g., OKP, secp256k1), allowing storage and retrieval of both public and private keys outside keystore. + +This architecture ensures strong key isolation, optional biometric enforcement, and a clear separation between hardware-protected cryptographic keys and application-managed generic keys. + +## πŸ” Key Type Capability Matrix + +| Key Type | Signing | Encryption / Decryption | HMAC | Hardware-Backed | Storage | Retrieval | +|---------------------------|----------|--------------------------|------|---------------|----------|----------------------| +| **AES (Symmetric)** | ❌ | βœ… | ❌ | βœ… | βœ… | Public Key | +| **RSA (Asymmetric)** | βœ… | ❌ | ❌ | βœ… | βœ… | Public Key | +| **EC P-256 (Asymmetric)** | βœ… | ❌ | ❌ | βœ… | βœ… | Public Key | +| **HMAC (SHA-256)** | ❌ | ❌ | βœ… | βœ… | βœ… | ❌ | +| **OKP** | ❌ | ❌ | ❌ | ❌ | βœ… | Public & Private Key | +| **EC secp256k1** | ❌ | ❌ | ❌ | ❌ | βœ… | Public & Private Key | +--- + +## πŸ“¦ Installation + +### For Native Android (Kotlin) + +Add Maven Central and Sonatype snapshots repository to your `settings.gradle.kts`: + +```kotlin +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven("https://oss.sonatype.org/content/repositories/snapshots/") + } +} +``` + +Add the dependency to your app's `build.gradle.kts`: + +```kotlin +dependencies { + implementation("io.mosip:secure-keystore:0.5.0-SNAPSHOT") +} +``` + +### For React Native ```sh npm install @mosip/secure-keystore ``` -## API Documentation +--- + +## πŸ“– API Documentation -### deviceSupportsHardware +### Device Capability -`deviceSupportsHardware() => boolean` +#### deviceSupportsHardware Check if the device supports hardware keystore. -### hasAlias +**Signature:** +```kotlin +fun deviceSupportsHardware(): Boolean +``` + +**Returns:** + +| Type | Description | +|------|-------------| +| Boolean | `true` if hardware keystore is supported, `false` otherwise | + +--- + +### Key Management -`hasAlias(alias: String) => boolean` +#### hasAlias Check if the given alias is present in the keystore. -### generateKey +**Signature:** +```kotlin +fun hasAlias(alias: String): Boolean +``` + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| alias | String | Yes | The key identifier to check | + +**Returns:** -`generateKey(alias: String, isAuthRequired: boolean, authTimeout?: number) => void` +| Type | Description | +|------|-------------| +| Boolean | `true` if key exists, `false` otherwise | + +--- + +#### removeKey + +Removes a key associated with the alias from the keystore. + +**Signature:** +```kotlin +fun removeKey(alias: String) +``` + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| alias | String | Yes | The key identifier to remove | + +--- + +#### removeAllKeys + +Removes all keys stored in the keystore. + +**Signature:** +```kotlin +fun removeAllKeys() +``` + +--- + +### Symmetric Key Operations + +#### generateKey Generates a symmetric key for encryption and decryption. -### generateKeyPair +**Signature:** +```kotlin +fun generateKey( + alias: String, + isAuthRequired: Boolean, + authTimeout: Int? = null +) +``` + +**Parameters:** + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| alias | String | Yes | N/A | Unique identifier for the key | +| isAuthRequired | Boolean | Yes | N/A | Whether biometric/device authentication is required | +| authTimeout | Int? | No | null | Authentication timeout in seconds | + +**Example:** +```kotlin +// Generate key without authentication +SecureKeystore.generateKey( + alias = "my_encryption_key", + isAuthRequired = false +) + +// Generate key with authentication and 30-second timeout +SecureKeystore.generateKey( + alias = "secure_key", + isAuthRequired = true, + authTimeout = 30 +) +``` + +--- + +#### encryptData + +Encrypts the given data (encoded in Base64) using the key assigned to the alias. + +**Signature:** +```kotlin +fun encryptData( + alias: String, + data: String, + onSuccess: (encryptedText: String) -> Unit, + onFailure: (code: Int, message: String) -> Unit, + context: Context +) +``` + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| alias | String | Yes | Key identifier for encryption | +| data | String | Yes | Plain text data to encrypt (Base64 encoded) | +| onSuccess | (String) -> Unit | Yes | Callback with encrypted text | +| onFailure | (Int, String) -> Unit | Yes | Callback with error code and message | +| context | Context | Yes | Android context for authentication UI | + +**Example:** +```kotlin +SecureKeystore.encryptData( + alias = "my_key", + data = "Sensitive information", + onSuccess = { encryptedText -> + println("Encrypted: $encryptedText") + }, + onFailure = { code, message -> + println("Encryption failed: $code - $message") + }, + context = this +) +``` + +--- + +#### decryptData + +Decrypts the given encrypted text using the key assigned to the alias. + +**Signature:** +```kotlin +fun decryptData( + alias: String, + encryptedText: String, + onSuccess: (data: String) -> Unit, + onFailure: (code: Int, message: String) -> Unit, + context: Context +) +``` + +**Parameters:** -`generateKeyPair(type: String, alias: String, isAuthRequired: boolean, authTimeout?: number) => String` +| Name | Type | Required | Description | +|------|------|----------|-------------| +| alias | String | Yes | Key identifier for decryption | +| encryptedText | String | Yes | Encrypted data to decrypt | +| onSuccess | (String) -> Unit | Yes | Callback with decrypted text | +| onFailure | (Int, String) -> Unit | Yes | Callback with error code and message | +| context | Context | Yes | Android context for authentication UI | + +**Example:** +```kotlin +SecureKeystore.decryptData( + alias = "my_key", + encryptedText = encryptedData, + onSuccess = { decryptedText -> + println("Decrypted: $decryptedText") + }, + onFailure = { code, message -> + println("Decryption failed: $code - $message") + }, + context = this +) +``` + +--- + +### Asymmetric Key Operations + +#### generateKeyPair Generates an asymmetric RSA or EC (P-256) key pair for signing. -### removeKey +**Signature:** +```kotlin +fun generateKeyPair( + type: String, + alias: String, + isAuthRequired: Boolean, + authTimeout: Int? = null +): String +``` -`removeKey(alias: String) => void` +**Parameters:** -Removes a key associated with the alias from the keystore. +| Name | Type | Required | Default | Description | +|------|------|----------|---------|------------------------------------------------| +| type | String | Yes | N/A | Key type: `"RS256"` or `"ES256"` | +| alias | String | Yes | N/A | Unique identifier for the key pair | +| isAuthRequired | Boolean | Yes | N/A | Whether authentication is required for signing | +| authTimeout | Int? | No | null | Authentication timeout in seconds | + +**Returns:** -### encryptData +| Type | Description | +|------|-------------| +| String | Public key in PEM format | +**Example:** ```kotlin -encryptData( - alias: String, - data: String, - onSuccess: (encryptedText: String) -> Unit, - onFailure: (code: number, message: String) -> Unit, - context: Context, -) => void +// Generate RSA key pair +val rsaPublicKey = SecureKeystore.generateKeyPair( + type = "RS256", + alias = "rsa_key", + isAuthRequired = false +) + +// Generate EC key pair with authentication +val ecPublicKey = SecureKeystore.generateKeyPair( + type = "ES256", + alias = "ec_key", + isAuthRequired = true, + authTimeout = 60 +) ``` -Encrypts the given data (encoded in Base64) using the key assigned to the alias. Returns the encrypted data as a String through the `onSuccess` callback. +--- -### decryptData +#### sign +Creates a signature for the given data and signing algorithm using the key assigned to the alias. + +**Signature:** ```kotlin -decryptData( - alias: String, - encryptedText: String, - onSuccess: (data: String) -> Unit, - onFailure: (code: number, message: String) -> Unit, - context: Context, -) => void +fun sign( + signAlgorithm: String, + alias: String, + data: String, + onSuccess: (signature: String) -> Unit, + onFailure: (code: Int, message: String) -> Unit, + context: Context +) ``` -Decrypts the given `encryptedText` using the key assigned to the alias. Returns the decrypted data as a String through the `onSuccess` callback. +**Parameters:** -### sign +| Name | Type | Required | Description | +|------|------|----------|-------------| +| signAlgorithm | String | Yes | Signing algorithm: `"SHA256withRSA"` or `"SHA256withECDSA"` | +| alias | String | Yes | Key pair identifier for signing | +| data | String | Yes | Data to sign (Base64 encoded) | +| onSuccess | (String) -> Unit | Yes | Callback with signature (Base64 encoded) | +| onFailure | (Int, String) -> Unit | Yes | Callback with error code and message | +| context | Context | Yes | Android context for authentication UI | +**Example:** ```kotlin -sign( - signAlgorithm: String, - alias: String, - data: String, - onSuccess: (signature: String) -> Unit, - onFailure: (code: number, message: String) -> Unit, - context: Context, -) => void +SecureKeystore.sign( + signAlgorithm = "SHA256withRSA", + alias = "rsa_key", + data = "data to sign", + onSuccess = { signature -> + println("Signature: $signature") + }, + onFailure = { code, message -> + println("Signing failed: $code - $message") + }, + context = this +) ``` -Creates a signature for the given data and signing algorithm using the key assigned to the alias. Returns the signature as a String through the `onSuccess` callback. +**Note on ECDSA Signatures:** -> For `SHA256withECDSA` as `signAlgorithm`, the output is in standard ASN.1 format. In the case of certain verifiers like jwt.io, conversion to RS format is necessary. +For `SHA256withECDSA` as `signAlgorithm`, the output is in standard ASN.1 format. In the case of certain verifiers like jwt.io, conversion to RS format is necessary. ```kotlin private fun convertDerToRsFormat(derSignature: ByteArray): ByteArray { @@ -123,8 +371,13 @@ private fun convertDerToRsFormat(derSignature: ByteArray): ByteArray { val rPadded = ByteArray(32) val sPadded = ByteArray(32) - val rTrimmed = if (rBytes.size > 32) rBytes.copyOfRange(rBytes.size - 32, rBytes.size) else rBytes - val sTrimmed = if (sBytes.size > 32) sBytes.copyOfRange(sBytes.size - 32, sBytes.size) else sBytes + val rTrimmed = if (rBytes.size > 32) { + rBytes.copyOfRange(rBytes.size - 32, rBytes.size) + } else rBytes + + val sTrimmed = if (sBytes.size > 32) { + sBytes.copyOfRange(sBytes.size - 32, sBytes.size) + } else sBytes System.arraycopy(rTrimmed, 0, rPadded, 32 - rTrimmed.size, rTrimmed.size) System.arraycopy(sTrimmed, 0, sPadded, 32 - sTrimmed.size, sTrimmed.size) @@ -133,59 +386,173 @@ private fun convertDerToRsFormat(derSignature: ByteArray): ByteArray { } ``` -### generateHmacSha +--- + +### HMAC Operations + +#### generateHmacSha256Key + +Generates a symmetric key specifically for HMAC-SHA256 operations. + +**Signature:** +```kotlin +fun generateHmacSha256Key(alias: String) +``` + +**Parameters:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| alias | String | Yes | Unique identifier for the HMAC key | + +**Example:** ```kotlin -generateHmacSha( +SecureKeystore.generateHmacSha256Key("hmac_key") +``` + +--- + +#### generateHmacSha + +Generates an HMAC signature for the given data using the key assigned to the alias. + +**Signature:** +```kotlin +fun generateHmacSha( alias: String, data: String, onSuccess: (signature: String) -> Unit, - onFailure: (code: number, message: String) -> Unit, -) => void + onFailure: (code: Int, message: String) -> Unit +) ``` -Generates an HMAC signature for the given data using the key assigned to the alias. Returns the signature as a String through the `onSuccess` callback. +**Parameters:** -### generateHmacSha256Key +| Name | Type | Required | Description | +|------|------|----------|-------------| +| alias | String | Yes | HMAC key identifier | +| data | String | Yes | Data to generate HMAC for | +| onSuccess | (String) -> Unit | Yes | Callback with HMAC signature (Base64 encoded) | +| onFailure | (Int, String) -> Unit | Yes | Callback with error code and message | -`generateHmacSha256Key(alias: String) => void` +**Example:** +```kotlin +SecureKeystore.generateHmacSha( + alias = "hmac_key", + data = "message to authenticate", + onSuccess = { signature -> + println("HMAC: $signature") + }, + onFailure = { code, message -> + println("HMAC generation failed: $code - $message") + } +) +``` -Generates a symmetric key specifically for HMAC-SHA256 operations. +--- -### retrieveGenericKey +### Generic Key Storage -`retrieveGenericKey(account: String) => String[]` +#### storeKeyPair -Retrieves a list of keys associated with the specified account. +Stores the specified public and private key pair associated with the account. + +**Signature:** +```kotlin +fun storeKeyPair( + publicKey: String, + privateKey: String, + account: String +) +``` -### storeGenericKey +**Parameters:** +| Name | Type | Required | Description | +|------------|------|----------|----------------------| +| publicKey | String | Yes | Public key to store | +| privateKey | String | Yes | Private key to store | +| alias | String | Yes | Key pair identifier | + +**Example:** ```kotlin -storeGenericKey( - publicKey: String, - privateKey: String, - account: String, -) => void +SecureKeystore.storeKeyPair( + publicKey = "-----BEGIN PUBLIC KEY-----...", + privateKey = "-----BEGIN PRIVATE KEY-----...", + alias = "user@example.com" +) ``` -Stores the specified public and private key pair associated with the account. +--- + +#### retrieveKeyPair + +Retrieves a list of keys associated with the specified account. + +**Signature:** +```kotlin +fun retrieveKeyPair(alias: String): Array +``` + +**Parameters:** -### retrieveKey +| Name | Type | Required | Description | +|-------|------|----------|---------------------| +| alias | String | Yes | Key pair identifier | -`retrieveKey(alias: String) => String` +**Returns:** + +| Type | Description | +|------|-------------| +| Array | Array of keys associated with the account | + +**Example:** +```kotlin +val keys = SecureKeystore.retrieveKeyPair("user@example.com") +keys.forEach { key -> + println("Key: $key") +} +``` + +--- + +#### retrieveKey Retrieves the key associated with the alias. -### removeAllKeys +**Signature:** +```kotlin +fun retrieveKey(alias: String): String +``` + +**Parameters:** -`removeAllKeys() => void` +| Name | Type | Required | Description | +|------|------|----------|-------------| +| alias | String | Yes | Key identifier | -Removes all keys stored in the keystore. +**Returns:** + +| Type | Description | +|------|-------------| +| String | The key associated with the alias | -## Contributing +**Example:** +```kotlin +val publicKey = SecureKeystore.retrieveKey("my_key") +println("Public Key: $publicKey") +``` + +--- + +## 🀝 Contributing See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. -## License +--- + +## πŸ“„ License MPL-2.0 + +--- \ No newline at end of file From 05417eefd87f883e53fc83b3e3a3f020fc178abe Mon Sep 17 00:00:00 2001 From: abhip2565 Date: Wed, 4 Mar 2026 18:35:08 +0530 Subject: [PATCH 2/3] [INJIMOB-3779] add V2 interface Signed-off-by: abhip2565 --- .../SecureKeystore.kt | 10 ++ .../SecureKeystoreImpl.kt | 131 +++++++++++------- .../SecureKeystoreV2.kt | 24 ++++ .../common/Constants.kt | 7 + 4 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt index 27f45954..9255d1d8 100644 --- a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt @@ -41,10 +41,20 @@ interface SecureKeystore { fun generateHmacSha256Key(alias: String) + @Deprecated( + message = "Use retrieveKeyPair instead", + replaceWith = ReplaceWith("retrieveKeyPair(account, context)") + ) fun retrieveGenericKey(account: String, context: Any): List fun removeAllKeys() + + @Deprecated( + message = "Use storeKeyPair instead", + replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, account)") + ) fun storeGenericKey(publicKey: String,privateKey: String,account: String) + fun retrieveKey(alias: String):String } diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt index a825755f..51b65585 100644 --- a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt @@ -7,23 +7,19 @@ import androidx.biometric.BiometricPrompt.CryptoObject import androidx.fragment.app.FragmentActivity import com.reactnativesecurekeystore.biometrics.Biometrics import com.reactnativesecurekeystore.common.PemConverter +import com.reactnativesecurekeystore.common.Util import com.reactnativesecurekeystore.common.Util.Companion.getLogTag import com.reactnativesecurekeystore.dto.EncryptedOutput import com.reactnativesecurekeystore.exception.InvalidEncryptionText import com.reactnativesecurekeystore.exception.KeyNotFound -import com.reactnativesecurekeystore.common.Util; import kotlinx.coroutines.runBlocking import java.security.Key import java.security.KeyPair import java.security.KeyStore import java.security.PrivateKey -import java.util.concurrent.CountDownLatch; -import java.security.PublicKey -import javax.crypto.SecretKey -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine +import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors -import kotlin.coroutines.resume +import javax.crypto.SecretKey const val BIOMETRIC_AUTH_TITLE = "Unlock App" const val BIOMETRIC_AUTH_SUBTITLE = "Please use fingerprint to unlock the app" @@ -34,7 +30,7 @@ class SecureKeystoreImpl( private val cipherBox: CipherBox, private val biometrics: Biometrics, private val preferences: Preferences, -) : SecureKeystore { +) : SecureKeystoreV2 { private var ks: KeyStore = KeyStore.getInstance(KEYSTORE_TYPE) private val logTag = getLogTag(javaClass.simpleName) @@ -54,13 +50,19 @@ class SecureKeystoreImpl( isAuthRequired: Boolean, authTimeout: Int?, ): String { - val keyPair: KeyPair - keyPair = if (type == "RS256") { - keyGenerator.generateKeyPair(alias, isAuthRequired, authTimeout) - } else if (type == "ES256") - keyGenerator.generateKeyPairEC(alias, isAuthRequired, authTimeout) - else - throw KeyNotFound("Given key type $type is not supported") + val keyPair: KeyPair = when (type) { + SigningAlgorithm.RSA.value -> { + keyGenerator.generateKeyPair(alias, isAuthRequired, authTimeout) + } + + SigningAlgorithm.ES256.value -> keyGenerator.generateKeyPairEC( + alias, + isAuthRequired, + authTimeout + ) + + else -> throw KeyNotFound("Given key type $type is not supported") + } return PemConverter(keyPair.public).toPem() } @@ -227,45 +229,18 @@ class SecureKeystoreImpl( } } + @Deprecated( + "Use retrieveKeyPair instead", + replaceWith = ReplaceWith("retrieveKeyPair(account, context)") + ) override fun retrieveGenericKey(account: String, context: Any): List { - try { - val privateKeyAlias = Util.getPrivateKeyId(account) - val publicKeyAlias = Util.getPublicKeyId(account) - - val keyPair = ArrayList() - - val fragmentActivity = context as? FragmentActivity - ?: throw IllegalArgumentException("Context must be a FragmentActivity for biometric authentication") - if (account == "ES256K" || account == "ED25519") { - - val success = authenticateBiometricallyBlocking(fragmentActivity, privateKeyAlias) - - if (success) { - val privateKey = preferences.getPreference(privateKeyAlias, "") - val publicKey = preferences.getPreference(publicKeyAlias, "") - keyPair.add(privateKey) - keyPair.add(publicKey) - } else { - Log.e("SecureKeystore", "Biometric authentication failed") - } - } else { - val privateKey = preferences.getPreference(privateKeyAlias, "") - val publicKey = preferences.getPreference(publicKeyAlias, "") - keyPair.add(privateKey) - keyPair.add(publicKey) - } - return keyPair - } catch (e: Exception) { - Log.e( - "SecureKeystore", - "Error during biometric authentication or retrieving key-data: ${e.message}" - ) - throw Exception(e.message) - } + return retrieveKeyPair(account, context) } - - private fun authenticateBiometricallyBlocking(activity: FragmentActivity, keyAlias: String): Boolean { + private fun authenticateBiometricallyBlocking( + activity: FragmentActivity, + keyAlias: String + ): Boolean { val latch = CountDownLatch(1) var success = false @@ -306,12 +281,62 @@ class SecureKeystoreImpl( } + @Deprecated( + "Use storeKeyPair instead", + replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, account)") + ) override fun storeGenericKey( publicKey: String, privateKey: String, account: String, ) { - preferences.savePreference(Util.getPublicKeyId(account), publicKey) - preferences.savePreference(Util.getPrivateKeyId(account), privateKey) + storeKeyPair(publicKey, privateKey, account) + } + + override fun storeKeyPair( + publicKey: String, + privateKey: String, + alias: String + ) { + preferences.savePreference(Util.getPublicKeyId(alias), publicKey) + preferences.savePreference(Util.getPrivateKeyId(alias), privateKey) + + } + + override fun retrieveKeyPair(alias: String, context: Any): List { + try { + val privateKeyAlias = Util.getPrivateKeyId(alias) + val publicKeyAlias = Util.getPublicKeyId(alias) + + val keyPair = ArrayList() + + val fragmentActivity = context as? FragmentActivity + ?: throw IllegalArgumentException("Context must be a FragmentActivity for biometric authentication") + if (alias == SigningAlgorithm.ES256K.value || alias == SigningAlgorithm.EDDSA.value) { + + val success = authenticateBiometricallyBlocking(fragmentActivity, privateKeyAlias) + + if (success) { + val privateKey = preferences.getPreference(privateKeyAlias, "") + val publicKey = preferences.getPreference(publicKeyAlias, "") + keyPair.add(privateKey) + keyPair.add(publicKey) + } else { + Log.e("SecureKeystore", "Biometric authentication failed") + } + } else { + val privateKey = preferences.getPreference(privateKeyAlias, "") + val publicKey = preferences.getPreference(publicKeyAlias, "") + keyPair.add(privateKey) + keyPair.add(publicKey) + } + return keyPair + } catch (e: Exception) { + Log.e( + "SecureKeystore", + "Error during biometric authentication or retrieving key-data: ${e.message}" + ) + throw Exception(e.message) + } } } diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt new file mode 100644 index 00000000..123e40e1 --- /dev/null +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt @@ -0,0 +1,24 @@ +package com.reactnativesecurekeystore + +interface SecureKeystoreV2 : SecureKeystore { + + fun storeKeyPair(publicKey: String, privateKey: String, alias: String) + + fun retrieveKeyPair(alias: String, context: Any): List + + @Deprecated( + "Use storeKeyPair instead", + replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, account)") + ) + override fun storeGenericKey(publicKey: String, privateKey: String, account: String) { + storeKeyPair(publicKey, privateKey, account) + } + + @Deprecated( + "Use retrieveKeyPair instead", + replaceWith = ReplaceWith("retrieveKeyPair(account, context)") + ) + override fun retrieveGenericKey(account: String, context: Any): List { + return retrieveKeyPair(account, context) + } +} \ No newline at end of file diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt index f86b6ecc..ee8a1376 100644 --- a/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt @@ -12,3 +12,10 @@ const val ENCRYPTION_KEY_SIZE = 256 const val PUBLIC_KEY_STORING_ID = "_publicKey" const val PRIVATE_KEY_STORING_ID = "_privateKey" + +enum class SigningAlgorithm(val value: String) { + RSA("RS256"), + ES256("ES256"), + EDDSA("EdDSA"), + ES256K("ES256K"), +} From cb8a691622b41ecafc1be6a24882ca9f72d6a988 Mon Sep 17 00:00:00 2001 From: abhip2565 Date: Wed, 4 Mar 2026 19:05:54 +0530 Subject: [PATCH 3/3] [INJIMOB-3779] enhance biometric logic retrievekeypair method Signed-off-by: abhip2565 --- README.md | 134 +++++++++++++----- .../SecureKeystore.kt | 4 +- .../SecureKeystoreImpl.kt | 12 +- .../SecureKeystoreV2.kt | 6 +- .../common/Constants.kt | 9 +- 5 files changed, 121 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 2ed86d09..c238d1e3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,19 @@ A Kotlin/Android library for secure key management using Android's hardware-back --- +## πŸ†• What's New in v2 + +The `SecureKeystoreV2` interface was introduced to improve the Generic Key Storage API. The following changes were made: + +| Change | v1 (Deprecated) | v2 (Current) | +|--------|----------------|--------------| +| Store a key pair | `storeGenericKey(publicKey, privateKey, account)` | `storeKeyPair(publicKey, privateKey, alias)` | +| Retrieve a key pair | `retrieveGenericKey(account, context)` | `retrieveKeyPair(alias, context, isBiometricRequiredToFetch)` | + +> **Note:** `SecureKeystoreV2` extends `SecureKeystore`. The deprecated v1 methods (`storeGenericKey`, `retrieveGenericKey`) remain available for backwards compatibility but delegate to their v2 equivalents. They will be removed in a future release. Migrate to the new methods as soon as possible. + +--- + ## Feature Support The Secure-Keystore library provides hardware-backed key management for symmetric, asymmetric, and HMAC keys using Android’s Keystore system. @@ -135,6 +148,35 @@ fun removeAllKeys() --- +#### retrieveKey + +Retrieves the public key in PEM format for the given alias from the Android Keystore. + +**Signature:** +```kotlin +fun retrieveKey(alias: String): String +``` + +**Parameters:** + +| Name | Type | Required | Description | +|-------|--------|----------|---------------------| +| alias | String | Yes | The key identifier | + +**Returns:** + +| Type | Description | +|--------|--------------------------| +| String | Public key in PEM format | + +**Example:** +```kotlin +val publicKeyPem = SecureKeystore.retrieveKey("rsa_key") +println(publicKeyPem) // -----BEGIN PUBLIC KEY-----... +``` + +--- + ### Symmetric Key Operations #### generateKey @@ -453,26 +495,28 @@ SecureKeystore.generateHmacSha( ### Generic Key Storage +> ⚠️ **Deprecation Notice:** `storeGenericKey` and `retrieveGenericKey` are deprecated as of v2. Use [`storeKeyPair`](#storekeypair) and [`retrieveKeyPair`](#retrievekeypair) instead. + #### storeKeyPair -Stores the specified public and private key pair associated with the account. +Stores the specified public and private key pair associated with the alias. This method replaces the deprecated `storeGenericKey`. **Signature:** ```kotlin fun storeKeyPair( publicKey: String, privateKey: String, - account: String + alias: String ) ``` **Parameters:** -| Name | Type | Required | Description | -|------------|------|----------|----------------------| -| publicKey | String | Yes | Public key to store | -| privateKey | String | Yes | Private key to store | -| alias | String | Yes | Key pair identifier | +| Name | Type | Required | Description | +|------------|--------|----------|----------------------| +| publicKey | String | Yes | Public key to store | +| privateKey | String | Yes | Private key to store | +| alias | String | Yes | Key pair identifier | **Example:** ```kotlin @@ -487,60 +531,82 @@ SecureKeystore.storeKeyPair( #### retrieveKeyPair -Retrieves a list of keys associated with the specified account. +Retrieves the public and private keys associated with the specified alias. For sensitive key types (`ES256K`, `EdDSA`), biometric authentication is triggered automatically. This method replaces the deprecated `retrieveGenericKey`. **Signature:** ```kotlin -fun retrieveKeyPair(alias: String): Array +fun retrieveKeyPair(alias: String, context: Any, isBiometricRequiredToFetch: Boolean = false): List ``` **Parameters:** -| Name | Type | Required | Description | -|-------|------|----------|---------------------| -| alias | String | Yes | Key pair identifier | +| Name | Type | Required | Default | Description | +|---------------------------|---------|----------|---------|----------------------------------------------------------------------------------------------| +| alias | String | Yes | N/A | Key pair identifier | +| context | Any | Yes | N/A | `FragmentActivity` context, required for biometric authentication prompts | +| isBiometricRequiredToFetch | Boolean | No | `false` | When `true`, biometric authentication is enforced before the key pair is returned regardless of key type | **Returns:** -| Type | Description | -|------|-------------| -| Array | Array of keys associated with the account | +| Type | Description | +|---------------|----------------------------------------------------------| +| List\ | List containing `[privateKey, publicKey]` for the alias | **Example:** ```kotlin -val keys = SecureKeystore.retrieveKeyPair("user@example.com") -keys.forEach { key -> - println("Key: $key") -} +val keys = SecureKeystore.retrieveKeyPair("user@example.com", this) +val privateKey = keys[0] +val publicKey = keys[1] ``` --- -#### retrieveKey +#### ~~storeGenericKey~~ *(Deprecated)* -Retrieves the key associated with the alias. +> ⚠️ **Deprecated.** Use [`storeKeyPair`](#storekeypair) instead. + +Stores a public and private key pair associated with the account. This method now delegates to `storeKeyPair` internally. **Signature:** ```kotlin -fun retrieveKey(alias: String): String +@Deprecated("Use storeKeyPair instead") +fun storeGenericKey( + publicKey: String, + privateKey: String, + account: String +) ``` -**Parameters:** +**Migration:** +```kotlin +// Before (v1 - deprecated) +SecureKeystore.storeGenericKey(publicKey, privateKey, account) -| Name | Type | Required | Description | -|------|------|----------|-------------| -| alias | String | Yes | Key identifier | +// After (v2) +SecureKeystore.storeKeyPair(publicKey, privateKey, alias = account) +``` -**Returns:** +--- -| Type | Description | -|------|-------------| -| String | The key associated with the alias | +#### ~~retrieveGenericKey~~ *(Deprecated)* -**Example:** +> ⚠️ **Deprecated.** Use [`retrieveKeyPair`](#retrievekeypair) instead. + +Retrieves keys associated with the specified account. This method now delegates to `retrieveKeyPair` internally. + +**Signature:** ```kotlin -val publicKey = SecureKeystore.retrieveKey("my_key") -println("Public Key: $publicKey") +@Deprecated("Use retrieveKeyPair instead") +fun retrieveGenericKey(account: String, context: Any): List +``` + +**Migration:** +```kotlin +// Before (v1 - deprecated) +val keys = SecureKeystore.retrieveGenericKey(account, context) + +// After (v2) +val keys = SecureKeystore.retrieveKeyPair(alias = account, context = this) ``` --- @@ -555,4 +621,4 @@ See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the MPL-2.0 ---- \ No newline at end of file +--- diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt index 9255d1d8..feb76e33 100644 --- a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystore.kt @@ -43,7 +43,7 @@ interface SecureKeystore { @Deprecated( message = "Use retrieveKeyPair instead", - replaceWith = ReplaceWith("retrieveKeyPair(account, context)") + replaceWith = ReplaceWith("retrieveKeyPair(account, context, isBiometricRequiredToFetch)"), ) fun retrieveGenericKey(account: String, context: Any): List @@ -51,7 +51,7 @@ interface SecureKeystore { @Deprecated( message = "Use storeKeyPair instead", - replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, account)") + replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, alias)") ) fun storeGenericKey(publicKey: String,privateKey: String,account: String) diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt index 51b65585..8b407000 100644 --- a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreImpl.kt @@ -231,7 +231,7 @@ class SecureKeystoreImpl( @Deprecated( "Use retrieveKeyPair instead", - replaceWith = ReplaceWith("retrieveKeyPair(account, context)") + replaceWith = ReplaceWith("retrieveKeyPair(account, context, isBiometricRequiredToFetch)"), ) override fun retrieveGenericKey(account: String, context: Any): List { return retrieveKeyPair(account, context) @@ -283,7 +283,7 @@ class SecureKeystoreImpl( @Deprecated( "Use storeKeyPair instead", - replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, account)") + replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, alias)") ) override fun storeGenericKey( publicKey: String, @@ -303,7 +303,11 @@ class SecureKeystoreImpl( } - override fun retrieveKeyPair(alias: String, context: Any): List { + override fun retrieveKeyPair( + alias: String, + context: Any, + isBiometricRequiredToFetch: Boolean + ): List { try { val privateKeyAlias = Util.getPrivateKeyId(alias) val publicKeyAlias = Util.getPublicKeyId(alias) @@ -312,7 +316,7 @@ class SecureKeystoreImpl( val fragmentActivity = context as? FragmentActivity ?: throw IllegalArgumentException("Context must be a FragmentActivity for biometric authentication") - if (alias == SigningAlgorithm.ES256K.value || alias == SigningAlgorithm.EDDSA.value) { + if (SigningAlgorithm.contains(alias) || isBiometricRequiredToFetch) { val success = authenticateBiometricallyBlocking(fragmentActivity, privateKeyAlias) diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt index 123e40e1..0024c121 100644 --- a/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/SecureKeystoreV2.kt @@ -4,11 +4,11 @@ interface SecureKeystoreV2 : SecureKeystore { fun storeKeyPair(publicKey: String, privateKey: String, alias: String) - fun retrieveKeyPair(alias: String, context: Any): List + fun retrieveKeyPair(alias: String, context: Any, isBiometricRequiredToFetch: Boolean = false): List @Deprecated( "Use storeKeyPair instead", - replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, account)") + replaceWith = ReplaceWith("storeKeyPair(publicKey, privateKey, alias)") ) override fun storeGenericKey(publicKey: String, privateKey: String, account: String) { storeKeyPair(publicKey, privateKey, account) @@ -16,7 +16,7 @@ interface SecureKeystoreV2 : SecureKeystore { @Deprecated( "Use retrieveKeyPair instead", - replaceWith = ReplaceWith("retrieveKeyPair(account, context)") + replaceWith = ReplaceWith("retrieveKeyPair(account, context, isBiometricRequiredToFetch)"), ) override fun retrieveGenericKey(account: String, context: Any): List { return retrieveKeyPair(account, context) diff --git a/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt b/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt index ee8a1376..c8607dd1 100644 --- a/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt +++ b/kotlin/android/src/main/java/com/reactnativesecurekeystore/common/Constants.kt @@ -17,5 +17,12 @@ enum class SigningAlgorithm(val value: String) { RSA("RS256"), ES256("ES256"), EDDSA("EdDSA"), - ES256K("ES256K"), + ES256K("ES256K"); + + companion object { + fun contains(value: String): Boolean { + return values().any { it.value.equals(value, ignoreCase = true) } + } + } } +