Skip to content

Commit 68abcff

Browse files
committed
feat: enhance secure storage hooks with error handling and option validation
- Introduced a centralized error factory for consistent error handling across hooks. - Added option validation and normalization utilities to ensure consistent configurations. - Updated `useSecret`, `useSecretItem`, and `useSecureStorage` hooks to utilize new error handling and option extraction methods. - Improved documentation for hooks, including examples and detailed descriptions of parameters and return values. - Enhanced error messages to provide clearer guidance for common issues.
1 parent 3fadd68 commit 68abcff

19 files changed

Lines changed: 2147 additions & 235 deletions

android/src/main/java/com/sensitiveinfo/HybridSensitiveInfo.kt

Lines changed: 187 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ import com.sensitiveinfo.internal.auth.BiometricAuthenticator
77
import com.sensitiveinfo.internal.crypto.AccessControlResolver
88
import com.sensitiveinfo.internal.crypto.CryptoManager
99
import com.sensitiveinfo.internal.crypto.SecurityAvailabilityResolver
10+
import com.sensitiveinfo.internal.response.ResponseBuilder
11+
import com.sensitiveinfo.internal.response.StandardResponseBuilder
1012
import com.sensitiveinfo.internal.storage.PersistedEntry
1113
import com.sensitiveinfo.internal.storage.PersistedMetadata
1214
import com.sensitiveinfo.internal.storage.SecureStorage
1315
import com.sensitiveinfo.internal.util.AliasGenerator
1416
import com.sensitiveinfo.internal.util.ReactContextHolder
1517
import com.sensitiveinfo.internal.util.ServiceNameResolver
18+
import com.sensitiveinfo.internal.validation.AndroidStorageValidator
19+
import com.sensitiveinfo.internal.validation.StorageValidator
1620
import kotlinx.coroutines.CoroutineScope
1721
import kotlinx.coroutines.Dispatchers
1822
import kotlinx.coroutines.SupervisorJob
@@ -21,8 +25,16 @@ import kotlin.jvm.Volatile
2125
/**
2226
* Android Keystore implementation of the SensitiveInfo Nitro module.
2327
*
24-
* This class provides secure storage for sensitive data on Android using the Android Keystore
28+
* Provides secure storage for sensitive data on Android using the Android Keystore
2529
* for key management and SharedPreferences for encrypted data persistence.
30+
*
31+
* The implementation follows a consistent pattern across all methods:
32+
* 1. Validate inputs using [StorageValidator]
33+
* 2. Resolve access control and security parameters
34+
* 3. Perform cryptographic or storage operations
35+
* 4. Build responses using [ResponseBuilder] for type conversion
36+
*
37+
* @since 6.0.0
2638
*/
2739
class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
2840
private data class Dependencies(
@@ -31,7 +43,9 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
3143
val cryptoManager: CryptoManager,
3244
val accessControlResolver: AccessControlResolver,
3345
val securityAvailabilityResolver: SecurityAvailabilityResolver,
34-
val serviceNameResolver: ServiceNameResolver
46+
val serviceNameResolver: ServiceNameResolver,
47+
val validator: StorageValidator,
48+
val responseBuilder: ResponseBuilder
3549
)
3650

3751
@Volatile
@@ -56,31 +70,63 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
5670
cryptoManager = cryptoManager,
5771
accessControlResolver = accessControlResolver,
5872
securityAvailabilityResolver = securityAvailabilityResolver,
59-
serviceNameResolver = serviceNameResolver
73+
serviceNameResolver = serviceNameResolver,
74+
validator = AndroidStorageValidator(),
75+
responseBuilder = StandardResponseBuilder()
6076
).also { built ->
6177
dependencies = built
6278
}
6379
}
6480
}
6581
}
6682

83+
/**
84+
* Sets an item in secure storage.
85+
*
86+
* Process:
87+
* 1. Validates the key, value, and options
88+
* 2. Resolves service name and access control
89+
* 3. Generates Keystore alias from service and key
90+
* 4. Encrypts plaintext using CryptoManager
91+
* 5. Creates metadata for tracking security properties
92+
* 6. Persists encrypted entry and metadata
93+
* 7. Returns mutation result with metadata
94+
*
95+
* @param request The set request containing key, value, and options
96+
* @return Promise resolving to MutationResult with metadata
97+
* @throws IllegalArgumentException if key or value is invalid
98+
* @throws java.security.KeyStoreException if Keystore operation fails
99+
*/
67100
override fun setItem(request: SensitiveInfoSetRequest): Promise<MutationResult> {
68101
return Promise.async(coroutineScope) {
69102
val deps = ensureInitialized()
103+
104+
// Step 1: Validate inputs
105+
deps.validator.validateKey(request.key)
106+
deps.validator.validateValue(request.value)
107+
108+
// Step 2: Resolve service name
70109
val service = deps.serviceNameResolver.resolve(request.service)
110+
111+
// Step 3: Resolve access control
71112
val resolved = deps.accessControlResolver.resolve(request.accessControl)
113+
114+
// Step 4: Generate alias for Keystore entry
72115
val alias = AliasGenerator.aliasFor(service, request.key)
73116

117+
// Step 5: Encrypt plaintext
74118
val plaintext = request.value.toByteArray(Charsets.UTF_8)
75119
val encryption = deps.cryptoManager.encrypt(alias, plaintext, resolved, request.authenticationPrompt)
76120

121+
// Step 6: Create metadata
77122
val metadata = StorageMetadata(
78123
securityLevel = resolved.securityLevel,
79124
backend = StorageBackend.ANDROIDKEYSTORE,
80125
accessControl = resolved.accessControl,
81126
timestamp = System.currentTimeMillis() / 1000.0
82127
)
83128

129+
// Step 7: Persist entry
84130
val entry = PersistedEntry(
85131
alias = alias,
86132
ciphertext = encryption.ciphertext,
@@ -94,21 +140,47 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
94140

95141
deps.storage.save(service, request.key, entry)
96142

97-
MutationResult(metadata = metadata)
143+
// Step 8: Build response using response builder
144+
deps.responseBuilder.buildMutationResult(metadata)
98145
}
99146
}
100147

148+
/**
149+
* Retrieves an item from secure storage.
150+
*
151+
* Process:
152+
* 1. Resolves service name
153+
* 2. Reads encrypted entry and metadata from storage
154+
* 3. If include_value is true, decrypts the ciphertext
155+
* 4. Rebuilds access control from persisted metadata
156+
* 5. Reconstructs item from decrypted value and metadata
157+
* 6. Builds response using response builder
158+
*
159+
* @param request The get request with key and authentication prompt
160+
* @return Promise resolving to SensitiveInfoItem or null if not found
161+
* @throws IllegalArgumentException if key is invalid
162+
* @throws java.security.KeyStoreException if decryption fails
163+
*/
101164
override fun getItem(request: SensitiveInfoGetRequest): Promise<SensitiveInfoItem?> {
102165
return Promise.async(coroutineScope) {
103166
val deps = ensureInitialized()
167+
168+
// Step 1: Validate key
169+
deps.validator.validateKey(request.key)
170+
171+
// Step 2: Resolve service name
104172
val service = deps.serviceNameResolver.resolve(request.service)
105173

174+
// Step 3: Read entry from storage
106175
val entry = deps.storage.read(service, request.key)
107176
if (entry == null) {
108177
return@async null
109178
}
110179

180+
// Step 4: Decode metadata
111181
val metadata = entry.metadata.toStorageMetadata()
182+
183+
// Step 5: Decrypt value if requested
112184
val value = if (request.includeValue == true && entry.ciphertext != null && entry.iv != null) {
113185
val resolution = deps.cryptoManager.buildResolutionForPersisted(
114186
accessControl = metadata?.accessControl ?: AccessControl.NONE,
@@ -131,9 +203,9 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
131203
null
132204
}
133205

134-
SensitiveInfoItem(
206+
// Step 6: Build response using response builder
207+
deps.responseBuilder.buildItem(
135208
key = request.key,
136-
service = service,
137209
value = value,
138210
metadata = metadata ?: StorageMetadata(
139211
securityLevel = SecurityLevel.SOFTWARE,
@@ -145,36 +217,103 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
145217
}
146218
}
147219

220+
/**
221+
* Deletes an item from secure storage.
222+
*
223+
* Process:
224+
* 1. Validates the key
225+
* 2. Resolves service name
226+
* 3. Reads the stored entry to get Keystore alias
227+
* 4. Deletes the Keystore key using the alias
228+
* 5. Deletes the encrypted data from storage
229+
* 6. Returns success boolean
230+
*
231+
* @param request The delete request containing key
232+
* @return Promise resolving to boolean (success)
233+
* @throws IllegalArgumentException if key is invalid
234+
* @throws java.security.KeyStoreException if key deletion fails
235+
*/
148236
override fun deleteItem(request: SensitiveInfoDeleteRequest): Promise<Boolean> {
149237
return Promise.async(coroutineScope) {
150238
val deps = ensureInitialized()
239+
240+
// Step 1: Validate key
241+
deps.validator.validateKey(request.key)
242+
243+
// Step 2: Resolve service name
151244
val service = deps.serviceNameResolver.resolve(request.service)
152245

246+
// Step 3: Read entry to get alias
153247
val entry = deps.storage.read(service, request.key)
154248
if (entry != null) {
249+
// Step 4: Delete Keystore key
155250
deps.cryptoManager.deleteKey(entry.alias)
156251
}
157252

253+
// Step 5: Delete storage entry
158254
deps.storage.delete(service, request.key)
255+
256+
// Step 6: Return success
257+
true
159258
}
160259
}
161260

261+
/**
262+
* Checks if an item exists in secure storage.
263+
*
264+
* Process:
265+
* 1. Validates the key
266+
* 2. Resolves service name
267+
* 3. Checks if entry exists in storage
268+
* 4. Returns boolean existence
269+
*
270+
* @param request The has request containing key
271+
* @return Promise resolving to boolean (exists)
272+
* @throws IllegalArgumentException if key is invalid
273+
*/
162274
override fun hasItem(request: SensitiveInfoHasRequest): Promise<Boolean> {
163275
return Promise.async(coroutineScope) {
164276
val deps = ensureInitialized()
277+
278+
// Step 1: Validate key
279+
deps.validator.validateKey(request.key)
280+
281+
// Step 2: Resolve service name
165282
val service = deps.serviceNameResolver.resolve(request.service)
283+
284+
// Step 3: Check storage
166285
deps.storage.contains(service, request.key)
167286
}
168287
}
169288

289+
/**
290+
* Retrieves all items for a service.
291+
*
292+
* Process:
293+
* 1. Resolves service name
294+
* 2. Reads all entries from storage for the service
295+
* 3. For each entry, decrypts if include_values is true
296+
* 4. Handles decryption failures gracefully (skips value if fails)
297+
* 5. Builds item using response builder
298+
* 6. Filters out items that fail to process
299+
* 7. Returns array of items
300+
*
301+
* @param request The enumerate request with optional include_values flag
302+
* @return Promise resolving to array of SensitiveInfoItem
303+
* @throws IllegalArgumentException if service is invalid
304+
*/
170305
override fun getAllItems(request: SensitiveInfoEnumerateRequest?): Promise<Array<SensitiveInfoItem>> {
171306
return Promise.async(coroutineScope) {
172307
val deps = ensureInitialized()
308+
309+
// Step 1: Resolve service name
173310
val service = deps.serviceNameResolver.resolve(request?.service)
174311

312+
// Step 2: Read all entries
175313
val entries = deps.storage.readAll(service)
176314
val includeValues = request?.includeValues ?: false
177315

316+
// Step 3: Map entries to items
178317
entries.mapNotNull { (key, entry) ->
179318
try {
180319
val metadata = entry.metadata.toStorageMetadata() ?: StorageMetadata(
@@ -204,50 +343,86 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
204343
)
205344
String(plaintext, Charsets.UTF_8)
206345
} catch (e: Throwable) {
207-
// If decryption fails, skip including the value
346+
// Gracefully handle decryption failures
208347
null
209348
}
210349
} else {
211350
null
212351
}
213352

214-
SensitiveInfoItem(
353+
// Step 5: Build item using response builder
354+
deps.responseBuilder.buildItem(
215355
key = key,
216-
service = service,
217356
value = value,
218357
metadata = metadata
219358
)
220359
} catch (e: Throwable) {
221-
// Skip items that fail to process
360+
// Step 6: Skip items that fail to process
222361
null
223362
}
224363
}.toTypedArray()
225364
}
226365
}
227366

367+
/**
368+
* Clears all items for a service.
369+
*
370+
* Process:
371+
* 1. Validates options
372+
* 2. Resolves service name
373+
* 3. Reads all entries to get all Keystore aliases
374+
* 4. Deletes all Keystore keys
375+
* 5. Clears all SharedPreferences entries for the service
376+
* 6. Returns success
377+
*
378+
* @param request Optional SensitiveInfoOptions containing service name
379+
* @return Promise resolving to Unit (void)
380+
* @throws IllegalArgumentException if service is invalid
381+
* @throws java.security.KeyStoreException if any key deletion fails
382+
*/
228383
override fun clearService(request: SensitiveInfoOptions?): Promise<Unit> {
229384
return Promise.async(coroutineScope) {
230385
val deps = ensureInitialized()
386+
387+
// Step 1: Validate options
388+
deps.validator.validateOptions(request)
389+
390+
// Step 2: Resolve service name
231391
val service = deps.serviceNameResolver.resolve(request?.service)
232392

233-
// Get all entries for the service and delete their keys
393+
// Step 3: Read all entries for this service
234394
val entries = deps.storage.readAll(service)
395+
396+
// Step 4: Delete all Keystore keys
235397
for ((_, entry) in entries) {
236398
deps.cryptoManager.deleteKey(entry.alias)
237399
}
238400

239-
// Clear SharedPreferences
401+
// Step 5: Clear SharedPreferences
240402
deps.storage.clear(service)
241403

242404
Unit
243405
}
244406
}
245407

408+
/**
409+
* Gets supported security levels for the platform.
410+
*
411+
* Process:
412+
* 1. Queries security availability resolver for capabilities
413+
* 2. Converts capabilities to SecurityAvailability object
414+
* 3. Returns structured response
415+
*
416+
* @return Promise resolving to SecurityAvailability with platform capabilities
417+
*/
246418
override fun getSupportedSecurityLevels(): Promise<SecurityAvailability> {
247419
return Promise.async(coroutineScope) {
248420
val deps = ensureInitialized()
421+
422+
// Step 1: Query capabilities
249423
val capabilities = deps.securityAvailabilityResolver.resolve()
250424

425+
// Step 2: Build and return response
251426
SecurityAvailability(
252427
secureEnclave = capabilities.secureEnclave,
253428
strongBox = capabilities.strongBox,

android/src/main/java/com/sensitiveinfo/KeyRotation.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ class AndroidKeyRotationManager(private val context: Context) {
180180
* Gets a key by version ID from the keystore.
181181
* Returns null if key doesn't exist or can't be accessed.
182182
*/
183-
fun getKey(byVersionId keyVersionId: String): java.security.Key? {
183+
fun getKey(keyVersionId: String): java.security.Key? {
184184
return try {
185185
keyStore.getKey(keyVersionId, null)
186186
} catch (exception: KeyPermanentlyInvalidatedException) {
@@ -199,7 +199,7 @@ class AndroidKeyRotationManager(private val context: Context) {
199199
* @param keyVersionId ID of key to delete
200200
* @return true if deleted, false if not found
201201
*/
202-
fun deleteKey(byVersionId keyVersionId: String): Boolean {
202+
fun deleteKey(keyVersionId: String): Boolean {
203203
return try {
204204
if (keyStore.containsAlias(keyVersionId)) {
205205
keyStore.deleteEntry(keyVersionId)

0 commit comments

Comments
 (0)