@@ -2,16 +2,21 @@ package com.margelo.nitro.sensitiveinfo
22
33import android.content.Context
44import android.os.Build
5+ import android.security.keystore.KeyGenParameterSpec
6+ import android.security.keystore.KeyProperties
7+ import androidx.biometric.BiometricManager
58import androidx.security.crypto.EncryptedSharedPreferences
69import androidx.security.crypto.MasterKeys
710import com.facebook.proguard.annotations.DoNotStrip
811import com.margelo.nitro.core.Promise
912import 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
1722class 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}
0 commit comments