@@ -27,12 +27,13 @@ import com.oblador.keychain.exceptions.EmptyParameterException
27
27
import com.oblador.keychain.exceptions.KeyStoreAccessException
28
28
import kotlinx.coroutines.CoroutineScope
29
29
import kotlinx.coroutines.Dispatchers
30
- import kotlinx.coroutines.ExperimentalCoroutinesApi
31
30
import kotlinx.coroutines.SupervisorJob
32
31
import kotlinx.coroutines.cancel
33
32
import kotlinx.coroutines.isActive
34
33
import java.util.concurrent.TimeUnit
35
34
import kotlinx.coroutines.launch
35
+ import kotlinx.coroutines.sync.Mutex
36
+ import kotlinx.coroutines.sync.withLock
36
37
37
38
@ReactModule(name = KeychainModule .KEYCHAIN_MODULE )
38
39
@Suppress(" unused" )
@@ -142,9 +143,8 @@ class KeychainModule(reactContext: ReactApplicationContext) :
142
143
/* * Launches a coroutine to perform non-blocking UI operations */
143
144
private val coroutineScope = CoroutineScope (Dispatchers .Default + SupervisorJob ())
144
145
145
- /* * Limit parallelism for coroutineScope */
146
- @OptIn(ExperimentalCoroutinesApi ::class )
147
- private val serialDispatcher = Dispatchers .Default .limitedParallelism(1 )
146
+ /* * Mutex to prevent concurrent calls to Cipher, which doesn't support multi-threading */
147
+ private val mutex = Mutex ()
148
148
149
149
// endregion
150
150
// region Initialization
@@ -217,28 +217,30 @@ class KeychainModule(reactContext: ReactApplicationContext) :
217
217
options : ReadableMap ? ,
218
218
promise : Promise
219
219
) {
220
- coroutineScope.launch(serialDispatcher) {
221
- try {
222
- throwIfEmptyLoginPassword(username, password)
223
- val level = getSecurityLevelOrDefault(options)
224
- val storage = getSelectedStorage(options)
225
- throwIfInsufficientLevel(storage, level)
226
- val promptInfo = getPromptInfo(options)
227
- val result = encryptToResult(alias, storage, username, password, level, promptInfo)
228
- prefsStorage.storeEncryptedEntry(alias, result)
229
- val results = Arguments .createMap()
230
- results.putString(Maps .SERVICE , alias)
231
- results.putString(Maps .STORAGE , storage.getCipherStorageName())
232
- promise.resolve(results)
233
- } catch (e: EmptyParameterException ) {
234
- Log .e(KEYCHAIN_MODULE , e.message, e)
235
- promise.reject(Errors .E_EMPTY_PARAMETERS , e)
236
- } catch (e: CryptoFailedException ) {
237
- Log .e(KEYCHAIN_MODULE , e.message, e)
238
- promise.reject(Errors .E_CRYPTO_FAILED , e)
239
- } catch (fail: Throwable ) {
240
- Log .e(KEYCHAIN_MODULE , fail.message, fail)
241
- promise.reject(Errors .E_UNKNOWN_ERROR , fail)
220
+ coroutineScope.launch {
221
+ mutex.withLock {
222
+ try {
223
+ throwIfEmptyLoginPassword(username, password)
224
+ val level = getSecurityLevelOrDefault(options)
225
+ val storage = getSelectedStorage(options)
226
+ throwIfInsufficientLevel(storage, level)
227
+ val promptInfo = getPromptInfo(options)
228
+ val result = encryptToResult(alias, storage, username, password, level, promptInfo)
229
+ prefsStorage.storeEncryptedEntry(alias, result)
230
+ val results = Arguments .createMap()
231
+ results.putString(Maps .SERVICE , alias)
232
+ results.putString(Maps .STORAGE , storage.getCipherStorageName())
233
+ promise.resolve(results)
234
+ } catch (e: EmptyParameterException ) {
235
+ Log .e(KEYCHAIN_MODULE , e.message, e)
236
+ promise.reject(Errors .E_EMPTY_PARAMETERS , e)
237
+ } catch (e: CryptoFailedException ) {
238
+ Log .e(KEYCHAIN_MODULE , e.message, e)
239
+ promise.reject(Errors .E_CRYPTO_FAILED , e)
240
+ } catch (fail: Throwable ) {
241
+ Log .e(KEYCHAIN_MODULE , fail.message, fail)
242
+ promise.reject(Errors .E_UNKNOWN_ERROR , fail)
243
+ }
242
244
}
243
245
}
244
246
}
@@ -273,46 +275,48 @@ class KeychainModule(reactContext: ReactApplicationContext) :
273
275
}
274
276
275
277
private fun getGenericPassword (alias : String , options : ReadableMap ? , promise : Promise ) {
276
- coroutineScope.launch(serialDispatcher) {
277
- try {
278
- val resultSet = prefsStorage.getEncryptedEntry(alias)
279
- if (resultSet == null ) {
280
- Log .e(KEYCHAIN_MODULE , " No entry found for service: $alias " )
281
- promise.resolve(false )
282
- return @launch
283
- }
284
- val storageName = resultSet.cipherStorageName
285
- val rules = getSecurityRulesOrDefault(options)
286
- val promptInfo = getPromptInfo(options)
287
- var cipher: CipherStorage ? = null
288
-
289
- // Only check for upgradable ciphers for FacebookConseal as that
290
- // is the only cipher that can be upgraded
291
- cipher =
292
- if (rules == Rules .AUTOMATIC_UPGRADE && storageName == KnownCiphers .FB ) {
293
- // get the best storage
294
- val accessControl = getAccessControlOrDefault(options)
295
- val useBiometry = getUseBiometry(accessControl)
296
- getCipherStorageForCurrentAPILevel(useBiometry)
297
- } else {
298
- getCipherStorageByName(storageName)
278
+ coroutineScope.launch {
279
+ mutex.withLock {
280
+ try {
281
+ val resultSet = prefsStorage.getEncryptedEntry(alias)
282
+ if (resultSet == null ) {
283
+ Log .e(KEYCHAIN_MODULE , " No entry found for service: $alias " )
284
+ promise.resolve(false )
285
+ return @launch
299
286
}
300
- val decryptionResult = decryptCredentials(alias, cipher!! , resultSet, rules, promptInfo)
301
- val credentials = Arguments .createMap()
302
- credentials.putString(Maps .SERVICE , alias)
303
- credentials.putString(Maps .USERNAME , decryptionResult.username)
304
- credentials.putString(Maps .PASSWORD , decryptionResult.password)
305
- credentials.putString(Maps .STORAGE , cipher?.getCipherStorageName())
306
- promise.resolve(credentials)
307
- } catch (e: KeyStoreAccessException ) {
308
- Log .e(KEYCHAIN_MODULE , e.message!! )
309
- promise.reject(Errors .E_KEYSTORE_ACCESS_ERROR , e)
310
- } catch (e: CryptoFailedException ) {
311
- Log .e(KEYCHAIN_MODULE , e.message!! )
312
- promise.reject(Errors .E_CRYPTO_FAILED , e)
313
- } catch (fail: Throwable ) {
314
- Log .e(KEYCHAIN_MODULE , fail.message, fail)
315
- promise.reject(Errors .E_UNKNOWN_ERROR , fail)
287
+ val storageName = resultSet.cipherStorageName
288
+ val rules = getSecurityRulesOrDefault(options)
289
+ val promptInfo = getPromptInfo(options)
290
+ var cipher: CipherStorage ? = null
291
+
292
+ // Only check for upgradable ciphers for FacebookConseal as that
293
+ // is the only cipher that can be upgraded
294
+ cipher =
295
+ if (rules == Rules .AUTOMATIC_UPGRADE && storageName == KnownCiphers .FB ) {
296
+ // get the best storage
297
+ val accessControl = getAccessControlOrDefault(options)
298
+ val useBiometry = getUseBiometry(accessControl)
299
+ getCipherStorageForCurrentAPILevel(useBiometry)
300
+ } else {
301
+ getCipherStorageByName(storageName)
302
+ }
303
+ val decryptionResult = decryptCredentials(alias, cipher!! , resultSet, rules, promptInfo)
304
+ val credentials = Arguments .createMap()
305
+ credentials.putString(Maps .SERVICE , alias)
306
+ credentials.putString(Maps .USERNAME , decryptionResult.username)
307
+ credentials.putString(Maps .PASSWORD , decryptionResult.password)
308
+ credentials.putString(Maps .STORAGE , cipher?.getCipherStorageName())
309
+ promise.resolve(credentials)
310
+ } catch (e: KeyStoreAccessException ) {
311
+ Log .e(KEYCHAIN_MODULE , e.message!! )
312
+ promise.reject(Errors .E_KEYSTORE_ACCESS_ERROR , e)
313
+ } catch (e: CryptoFailedException ) {
314
+ Log .e(KEYCHAIN_MODULE , e.message!! )
315
+ promise.reject(Errors .E_CRYPTO_FAILED , e)
316
+ } catch (fail: Throwable ) {
317
+ Log .e(KEYCHAIN_MODULE , fail.message, fail)
318
+ promise.reject(Errors .E_UNKNOWN_ERROR , fail)
319
+ }
316
320
}
317
321
}
318
322
}
@@ -469,7 +473,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
469
473
* set then executed migration.
470
474
*/
471
475
@Throws(CryptoFailedException ::class , KeyStoreAccessException ::class )
472
- private fun decryptCredentials (
476
+ private suspend fun decryptCredentials (
473
477
alias : String ,
474
478
current : CipherStorage ,
475
479
resultSet : PrefsStorageBase .ResultSet ,
@@ -510,7 +514,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
510
514
511
515
/* * Try to decrypt with provided storage. */
512
516
@Throws(CryptoFailedException ::class )
513
- private fun decryptToResult (
517
+ private suspend fun decryptToResult (
514
518
alias : String ,
515
519
storage : CipherStorage ,
516
520
resultSet : PrefsStorageBase .ResultSet ,
@@ -527,7 +531,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
527
531
528
532
/* * Try to encrypt with provided storage. */
529
533
@Throws(CryptoFailedException ::class )
530
- private fun encryptToResult (
534
+ private suspend fun encryptToResult (
531
535
alias : String ,
532
536
storage : CipherStorage ,
533
537
username : String ,
@@ -558,7 +562,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
558
562
@Throws(
559
563
KeyStoreAccessException ::class , CryptoFailedException ::class , IllegalArgumentException ::class
560
564
)
561
- fun migrateCipherStorage (
565
+ private suspend fun migrateCipherStorage (
562
566
service : String ,
563
567
newCipherStorage : CipherStorage ,
564
568
oldCipherStorage : CipherStorage ,
0 commit comments