Skip to content

Commit 1f7e3ac

Browse files
committed
feat: Add biometric security demo component and integrate biometric storage options
- Introduced `BiometricSecurityDemo` component for demonstrating biometric storage and retrieval. - Updated `App.tsx` to include the new biometric security demo component. - Enhanced iOS `SensitiveInfo` implementation to support biometric and StrongBox security levels. - Updated `SensitiveInfo.nitro.ts` to define new security levels and options for biometric authentication. - Created `useSensitiveInfo` hook to manage sensitive data with biometric capabilities. - Implemented utility functions for biometric authentication in `BiometricAuthenticator.ts`. - Updated existing storage methods to accept security level options. - Added comprehensive error handling and user feedback for biometric operations.
1 parent 5671fcd commit 1f7e3ac

10 files changed

Lines changed: 1366 additions & 98 deletions

File tree

android/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,9 @@ dependencies {
128128

129129
// Secure storage support
130130
implementation "androidx.security:security-crypto:1.0.0"
131+
132+
// Biometric authentication support
133+
implementation "androidx.biometric:biometric:1.1.0"
134+
implementation "androidx.fragment:fragment-ktx:1.6.2"
131135
}
132136

android/src/main/java/com/margelo/nitro/sensitiveinfo/SensitiveInfo.kt

Lines changed: 127 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ package com.margelo.nitro.sensitiveinfo
22

33
import android.content.Context
44
import android.os.Build
5+
import android.security.keystore.KeyGenParameterSpec
6+
import android.security.keystore.KeyProperties
7+
import androidx.biometric.BiometricManager
58
import androidx.security.crypto.EncryptedSharedPreferences
69
import androidx.security.crypto.MasterKeys
710
import com.facebook.proguard.annotations.DoNotStrip
811
import com.margelo.nitro.core.Promise
912
import com.margelo.nitro.NitroModules
10-
import com.margelo.nitro.sensitiveinfo.HybridSensitiveInfoSpec
13+
import java.security.KeyStore
14+
import javax.crypto.KeyGenerator
1115

1216
/**
13-
* Secure storage implementation using AndroidX EncryptedSharedPreferences.
14-
* Uses StrongBox if available (API 28+), otherwise falls back to Android Keystore.
17+
* Secure storage implementation with multiple security levels.
18+
* Uses AndroidX EncryptedSharedPreferences with StrongBox when available.
19+
* Note: Biometric authentication must be handled at the JavaScript layer.
1520
*/
1621
@DoNotStrip
1722
class SensitiveInfo : HybridSensitiveInfoSpec() {
@@ -20,41 +25,152 @@ class SensitiveInfo : HybridSensitiveInfoSpec() {
2025
get() = NitroModules.applicationContext
2126
?: throw IllegalStateException("ReactApplicationContext is null")
2227

23-
// Initialize EncryptedSharedPreferences with StrongBox when possible
24-
private val prefs by lazy {
28+
companion object {
29+
private const val STRONGBOX_KEYSTORE_ALIAS = "SensitiveInfoStrongBoxKey"
30+
}
31+
32+
// Standard EncryptedSharedPreferences
33+
private val standardPrefs by lazy {
2534
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
2635

2736
EncryptedSharedPreferences.create(
28-
"react_native_sensitive_info",
37+
"react_native_sensitive_info_standard",
2938
masterKeyAlias,
3039
context,
3140
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
3241
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
3342
)
3443
}
3544

45+
// StrongBox EncryptedSharedPreferences (API 28+)
46+
private val strongBoxPrefs by lazy {
47+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxAvailableInternal()) {
48+
try {
49+
val masterKeyAlias = MasterKeys.getOrCreate(
50+
KeyGenParameterSpec.Builder(
51+
STRONGBOX_KEYSTORE_ALIAS,
52+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
53+
)
54+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
55+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
56+
.setKeySize(256)
57+
.setIsStrongBoxBacked(true)
58+
.build()
59+
)
60+
61+
EncryptedSharedPreferences.create(
62+
"react_native_sensitive_info_strongbox",
63+
masterKeyAlias,
64+
context,
65+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
66+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
67+
)
68+
} catch (e: Exception) {
69+
// Fallback to standard if StrongBox fails
70+
standardPrefs
71+
}
72+
} else {
73+
standardPrefs
74+
}
75+
}
76+
77+
// Biometric storage uses standard encryption but requires JS-level authentication
78+
private val biometricPrefs by lazy {
79+
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
80+
81+
EncryptedSharedPreferences.create(
82+
"react_native_sensitive_info_biometric",
83+
masterKeyAlias,
84+
context,
85+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
86+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
87+
)
88+
}
89+
90+
private fun getPreferencesForSecurityLevel(securityLevel: SecurityLevel?): android.content.SharedPreferences {
91+
return when (securityLevel) {
92+
SecurityLevel.BIOMETRIC -> biometricPrefs
93+
SecurityLevel.STRONGBOX -> strongBoxPrefs
94+
else -> standardPrefs
95+
}
96+
}
97+
3698
@DoNotStrip
37-
override fun getItem(key: String): Promise<String?> = Promise.async {
99+
override fun getItem(key: String, options: StorageOptions?): Promise<String?> = Promise.async {
100+
val securityLevel = options?.securityLevel
101+
val prefs = getPreferencesForSecurityLevel(securityLevel)
38102
prefs.getString(key, null)
39103
}
40104

41105
@DoNotStrip
42-
override fun setItem(key: String, value: String): Promise<Unit> = Promise.async {
106+
override fun setItem(key: String, value: String, options: StorageOptions?): Promise<Unit> = Promise.async {
107+
val securityLevel = options?.securityLevel
108+
val prefs = getPreferencesForSecurityLevel(securityLevel)
43109
prefs.edit().putString(key, value).apply()
44110
}
45111

46112
@DoNotStrip
47-
override fun removeItem(key: String): Promise<Unit> = Promise.async {
113+
override fun removeItem(key: String, options: StorageOptions?): Promise<Unit> = Promise.async {
114+
val securityLevel = options?.securityLevel
115+
val prefs = getPreferencesForSecurityLevel(securityLevel)
48116
prefs.edit().remove(key).apply()
49117
}
50118

51119
@DoNotStrip
52-
override fun getAllItems(): Promise<Map<String, String>> = Promise.async {
120+
override fun getAllItems(options: StorageOptions?): Promise<Map<String, String>> = Promise.async {
121+
val securityLevel = options?.securityLevel
122+
val prefs = getPreferencesForSecurityLevel(securityLevel)
53123
prefs.all.mapValues { it.value as? String ?: "" }
54124
}
55125

56126
@DoNotStrip
57-
override fun clear(): Promise<Unit> = Promise.async {
127+
override fun clear(options: StorageOptions?): Promise<Unit> = Promise.async {
128+
val securityLevel = options?.securityLevel
129+
val prefs = getPreferencesForSecurityLevel(securityLevel)
58130
prefs.edit().clear().apply()
59131
}
132+
133+
@DoNotStrip
134+
override fun isBiometricAvailable(): Promise<Boolean> = Promise.async {
135+
val biometricManager = BiometricManager.from(context)
136+
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
137+
BiometricManager.BIOMETRIC_SUCCESS -> true
138+
else -> false
139+
}
140+
}
141+
142+
@DoNotStrip
143+
override fun isStrongBoxAvailable(): Promise<Boolean> = Promise.async {
144+
isStrongBoxAvailableInternal()
145+
}
146+
147+
private fun isStrongBoxAvailableInternal(): Boolean {
148+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
149+
try {
150+
val keyStore = KeyStore.getInstance("AndroidKeyStore")
151+
keyStore.load(null)
152+
153+
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
154+
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
155+
"test_strongbox_key",
156+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
157+
)
158+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
159+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
160+
.setIsStrongBoxBacked(true)
161+
.build()
162+
163+
keyGenerator.init(keyGenParameterSpec)
164+
keyGenerator.generateKey()
165+
166+
// Clean up test key
167+
keyStore.deleteEntry("test_strongbox_key")
168+
true
169+
} catch (e: Exception) {
170+
false
171+
}
172+
} else {
173+
false
174+
}
175+
}
60176
}

example/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
SearchItemForm,
1919
StoredItemsList,
2020
SecurityInfo,
21+
BiometricSecurityDemo,
2122
} from './components';
2223
import { useSensitiveInfo, useTheme } from './hooks';
2324
import { commonStyles } from './styles/commonStyles';
@@ -133,6 +134,8 @@ export default function App() {
133134
onRemoveItem={removeItemById}
134135
/>
135136

137+
<BiometricSecurityDemo />
138+
136139
<SecurityInfo theme={theme} />
137140

138141
<AppFooter theme={theme} />

0 commit comments

Comments
 (0)