Skip to content

Commit 1f0fb62

Browse files
committed
Refactor crypto and storage for modern access control
Introduces AccessResolution and KeyAuthenticationStrategy for unified key management and authentication across Android API levels. Updates SecureStorage and PersistedEntry to store and handle richer metadata, supports biometric/device credential configuration, and adds LegacyCryptoManager for migration compatibility. Refactors encryption/decryption flows to use new abstractions and improves error handling and metadata persistence.
1 parent 00b412d commit 1f0fb62

12 files changed

Lines changed: 1436 additions & 369 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ class SensitiveInfoModule(reactContext: ReactApplicationContext) :
155155
key = key,
156156
value = value,
157157
service = service,
158-
accessControl = accessControl
158+
accessControl = accessControl,
159+
authenticationPrompt = authenticationPrompt
159160
)
160161

161162
// Return metadata to JavaScript

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import com.sensitiveinfo.internal.storage.SecureStorage
55
import com.sensitiveinfo.internal.storage.StorageResult
66
import com.sensitiveinfo.internal.storage.StorageMetadata
77
import com.sensitiveinfo.internal.util.SensitiveInfoException
8+
import com.sensitiveinfo.internal.util.ActivityContextHolder
9+
import com.sensitiveinfo.internal.auth.AuthenticationPrompt
810

911
/**
1012
* HybridSensitiveInfo.kt
@@ -57,7 +59,10 @@ import com.sensitiveinfo.internal.util.SensitiveInfoException
5759
*/
5860
class HybridSensitiveInfo(private val context: Context) {
5961

60-
private val storage = SecureStorage(context)
62+
private val storage = SecureStorage(
63+
context = context,
64+
activity = ActivityContextHolder.getActivity()
65+
)
6166

6267
/**
6368
* Stores a secret in secure storage with optional biometric protection.
@@ -125,7 +130,8 @@ class HybridSensitiveInfo(private val context: Context) {
125130
key: String,
126131
value: String,
127132
service: String? = null,
128-
accessControl: String? = null
133+
accessControl: String? = null,
134+
authenticationPrompt: AuthenticationPrompt? = null
129135
): StorageResult {
130136
// Validate inputs
131137
if (key.isEmpty()) {
@@ -136,14 +142,13 @@ class HybridSensitiveInfo(private val context: Context) {
136142
}
137143

138144
try {
139-
// Simply delegate to storage
140-
// The biometric prompt will be shown automatically by AndroidKeyStore
141-
// when the key is used during encryption
145+
// Delegate to storage and await the suspend function
142146
val storageMetadata = storage.setItem(
143147
key = key,
144148
value = value,
145149
service = service,
146-
accessControl = accessControl
150+
accessControl = accessControl,
151+
prompt = authenticationPrompt
147152
)
148153

149154
return StorageResult(
@@ -197,7 +202,7 @@ class HybridSensitiveInfo(private val context: Context) {
197202
* @throws SensitiveInfoException.UserCancelled if user cancels biometric prompt
198203
* @throws SensitiveInfoException.KeystoreUnavailable if key not accessible
199204
*/
200-
fun getItem(key: String, service: String? = null): StorageResult? {
205+
suspend fun getItem(key: String, service: String? = null): StorageResult? {
201206
if (key.isEmpty()) {
202207
throw SensitiveInfoException.InvalidConfiguration("key", "Cannot be empty")
203208
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.sensitiveinfo.internal.crypto
2+
3+
import com.sensitiveinfo.internal.util.AccessControl
4+
import com.sensitiveinfo.internal.util.SecurityLevel
5+
6+
/**
7+
* Encapsulates all security configuration for a single key.
8+
*
9+
* **Purpose:**
10+
* This is the SINGLE SOURCE OF TRUTH for how a key should be created and used.
11+
* It includes:
12+
* - What authentication is required
13+
* - Which authenticators are allowed
14+
* - Whether to use StrongBox
15+
* - Whether to invalidate on biometric enrollment change
16+
*
17+
* **Determinism:**
18+
* When decrypting an entry, AccessResolution is reconstructed from persisted
19+
* metadata. Using the SAME resolution ensures decryption works identically
20+
* to encryption.
21+
*
22+
* **API Abstraction:**
23+
* The resolution describes WHAT we want (e.g., "biometric + StrongBox").
24+
* KeyAuthenticationStrategy describes HOW to achieve it for each API level.
25+
*/
26+
internal data class AccessResolution(
27+
val accessControl: AccessControl,
28+
val securityLevel: SecurityLevel,
29+
val requiresAuthentication: Boolean,
30+
val allowedAuthenticators: Int,
31+
val useStrongBox: Boolean,
32+
val invalidateOnEnrollment: Boolean
33+
) {
34+
/**
35+
* Generates unique signature for this resolution.
36+
*
37+
* Used as part of the key alias to ensure different access controls
38+
* use different keys.
39+
*/
40+
val signature: String
41+
get() = buildString {
42+
append(accessControl.name)
43+
append('_')
44+
append(if (requiresAuthentication) '1' else '0')
45+
append('_')
46+
append(allowedAuthenticators)
47+
append('_')
48+
append(if (useStrongBox) '1' else '0')
49+
append('_')
50+
append(if (invalidateOnEnrollment) '1' else '0')
51+
}
52+
}

0 commit comments

Comments
 (0)