1- package com.sensitiveinfo
1+ package com.margelo.nitro. sensitiveinfo
22
3+ import androidx.annotation.Keep
34import com.margelo.nitro.core.Promise
45import com.margelo.nitro.sensitiveinfo.AccessControl
56import com.margelo.nitro.sensitiveinfo.AuthenticationPrompt
@@ -47,6 +48,7 @@ import kotlin.text.Charsets
4748 * The class resolves the appropriate storage backend, encrypts values with the Android Keystore,
4849 * and keeps metadata so JavaScript consumers always know which security tier saved an entry.
4950 */
51+ @Keep
5052class HybridSensitiveInfo : HybridSensitiveInfoSpec () {
5153 private val applicationContext get() = ReactContextHolder .requireContext()
5254
@@ -57,21 +59,29 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
5759 private val authenticator by lazy { BiometricAuthenticator () }
5860 private val cryptoManager by lazy { CryptoManager (authenticator) }
5961
62+ private fun resolveService (service : String? ): String = serviceResolver.resolve(service)
63+
64+ /* * Dispatches a block to the IO dispatcher while keeping call-sites succinct. */
65+ private suspend fun <T > io (block : suspend () -> T ): T = withContext(Dispatchers .IO ) { block() }
66+
67+ /* * Wrapper that fetches an entry from disk using the canonical dispatcher. */
68+ private suspend fun readEntry (service : String , key : String ): PersistedEntry ? = io {
69+ storage.read(service, key)
70+ }
71+
6072 /* *
6173 * Encrypts and stores a secret for the requested service/key pair.
6274 *
6375 * @return Metadata describing the security level used, mirroring the JS `MutationResult` type.
6476 */
6577 override fun setItem (request : SensitiveInfoSetRequest ): Promise <MutationResult > {
6678 return Promise .async {
67- val service = serviceResolver.resolve (request.service)
79+ val service = resolveService (request.service)
6880 val strongOnly = request.androidBiometricsStrongOnly == true
6981 val resolution = accessControlResolver.resolve(request.accessControl, strongOnly)
7082 val alias = AliasGenerator .create(service, resolution.signature)
7183
72- val previousEntry = withContext(Dispatchers .IO ) {
73- storage.read(service, request.key)
74- }
84+ val previousEntry = readEntry(service, request.key)
7585
7686 val metadata = StorageMetadata (
7787 securityLevel = resolution.securityLevel,
@@ -98,9 +108,7 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
98108 useStrongBox = resolution.useStrongBox
99109 )
100110
101- withContext(Dispatchers .IO ) {
102- storage.save(service, request.key, persisted)
103- }
111+ io { storage.save(service, request.key, persisted) }
104112
105113 if (previousEntry != null && previousEntry.alias != alias) {
106114 maybeDeleteAlias(service, previousEntry.alias)
@@ -116,9 +124,9 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
116124 override fun getItem (request : SensitiveInfoGetRequest ): Promise <SensitiveInfoItem ?> {
117125 return Promise .async {
118126 val includeValue = request.includeValue ? : true
119- val service = serviceResolver.resolve (request.service)
120- val entry = withContext( Dispatchers . IO ) { storage.read( service, request.key) }
121- ? : throw SensitiveInfoException .NotFound (request.key, service)
127+ val service = resolveService (request.service)
128+ val entry = readEntry( service, request.key)
129+ ? : throw SensitiveInfoException .NotFound (request.key, service)
122130
123131 val metadata = entry.metadata.toStorageMetadata() ? : fallbackMetadata(entry)
124132
@@ -142,9 +150,9 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
142150 */
143151 override fun deleteItem (request : SensitiveInfoDeleteRequest ): Promise <Boolean > {
144152 return Promise .async {
145- val service = serviceResolver.resolve (request.service)
146- val existing = withContext( Dispatchers . IO ) { storage.read( service, request.key) }
147- val removed = withContext( Dispatchers . IO ) { storage.delete(service, request.key) }
153+ val service = resolveService (request.service)
154+ val existing = readEntry( service, request.key)
155+ val removed = io { storage.delete(service, request.key) }
148156 if (removed && existing != null ) {
149157 maybeDeleteAlias(service, existing.alias)
150158 }
@@ -157,21 +165,23 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
157165 */
158166 override fun hasItem (request : SensitiveInfoHasRequest ): Promise <Boolean > {
159167 return Promise .async {
160- val service = serviceResolver.resolve(request.service)
161- withContext(Dispatchers .IO ) {
162- storage.contains(service, request.key)
163- }
168+ val service = resolveService(request.service)
169+ io { storage.contains(service, request.key) }
164170 }
165171 }
166172
167173 /* *
168- * Enumerates every entry in a service. When `includeValues` is false the secrets stay encrypted.
174+ * Enumerates every entry in a service. When `includeValues` is false the secrets stay encrypted.
175+ *
176+ * ```ts
177+ * const items = await SensitiveInfo.getAllItems({ service: 'vault', includeValues: true })
178+ * ```
169179 */
170180 override fun getAllItems (request : SensitiveInfoEnumerateRequest ? ): Promise <Array <SensitiveInfoItem >> {
171181 return Promise .async {
172182 val includeValues = request?.includeValues == true
173- val service = serviceResolver.resolve (request?.service)
174- val items = withContext( Dispatchers . IO ) { storage.readAll(service) }
183+ val service = resolveService (request?.service)
184+ val items = io { storage.readAll(service) }
175185
176186 val result = items.mapNotNull { (key, entry) ->
177187 val metadata = entry.metadata.toStorageMetadata() ? : fallbackMetadata(entry)
@@ -198,11 +208,9 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
198208 */
199209 override fun clearService (request : SensitiveInfoOptions ? ): Promise <Unit > {
200210 return Promise .async {
201- val service = serviceResolver.resolve(request?.service)
202- val existing = withContext(Dispatchers .IO ) { storage.readAll(service) }
203- withContext(Dispatchers .IO ) {
204- storage.clear(service)
205- }
211+ val service = resolveService(request?.service)
212+ val existing = io { storage.readAll(service) }
213+ io { storage.clear(service) }
206214 existing.map { it.second.alias }
207215 .distinct()
208216 .forEach { alias -> cryptoManager.deleteKey(alias) }
@@ -220,6 +228,10 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
220228 return Promise .resolved(snapshot)
221229 }
222230
231+ /* *
232+ * Rehydrates a ciphertext/IV pair using the alias captured in the persisted metadata. When the
233+ * item requires user presence the associated biometric/device-credential prompt is displayed.
234+ */
223235 private suspend fun decryptValue (
224236 entry : PersistedEntry ,
225237 metadata : StorageMetadata ,
@@ -247,6 +259,7 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
247259 return String (decrypted, Charsets .UTF_8 )
248260 }
249261
262+ /* * Fallback metadata path used when a legacy record predates the richer JSON payload. */
250263 private fun fallbackMetadata (entry : PersistedEntry ): StorageMetadata {
251264 val accessControl = accessControlFromPersisted(entry.metadata.accessControl) ? : AccessControl .NONE
252265 val backend = storageBackendFromPersisted(entry.metadata.backend) ? : StorageBackend .ANDROIDKEYSTORE
@@ -270,8 +283,13 @@ class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
270283
271284 private fun nowSeconds (): Double = System .currentTimeMillis() / 1000.0
272285
286+ /* *
287+ * Deletes the keystore alias once there are no persisted entries referencing it anymore. We
288+ * keep this work off the main thread as SharedPreferences iteration can be slow on older
289+ * devices.
290+ */
273291 private suspend fun maybeDeleteAlias (service : String , alias : String ) {
274- val remaining = withContext( Dispatchers . IO ) { storage.readAll(service) }
292+ val remaining = io { storage.readAll(service) }
275293 val stillReferenced = remaining.any { (_, entry) -> entry.alias == alias }
276294 if (! stillReferenced) {
277295 cryptoManager.deleteKey(alias)
0 commit comments