Skip to content

Commit 6e3f4ce

Browse files
authored
chore: use mutex instead of experimental limitedParallelism (#692)
* chore: use mutex instead of experimental limitedParallelism * chore: revert SupervisorJob removal
1 parent eeb63a4 commit 6e3f4ce

File tree

1 file changed

+73
-69
lines changed

1 file changed

+73
-69
lines changed

android/src/main/java/com/oblador/keychain/KeychainModule.kt

Lines changed: 73 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ import com.oblador.keychain.exceptions.EmptyParameterException
2727
import com.oblador.keychain.exceptions.KeyStoreAccessException
2828
import kotlinx.coroutines.CoroutineScope
2929
import kotlinx.coroutines.Dispatchers
30-
import kotlinx.coroutines.ExperimentalCoroutinesApi
3130
import kotlinx.coroutines.SupervisorJob
3231
import kotlinx.coroutines.cancel
3332
import kotlinx.coroutines.isActive
3433
import java.util.concurrent.TimeUnit
3534
import kotlinx.coroutines.launch
35+
import kotlinx.coroutines.sync.Mutex
36+
import kotlinx.coroutines.sync.withLock
3637

3738
@ReactModule(name = KeychainModule.KEYCHAIN_MODULE)
3839
@Suppress("unused")
@@ -142,9 +143,8 @@ class KeychainModule(reactContext: ReactApplicationContext) :
142143
/** Launches a coroutine to perform non-blocking UI operations */
143144
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
144145

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()
148148

149149
// endregion
150150
// region Initialization
@@ -217,28 +217,30 @@ class KeychainModule(reactContext: ReactApplicationContext) :
217217
options: ReadableMap?,
218218
promise: Promise
219219
) {
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+
}
242244
}
243245
}
244246
}
@@ -273,46 +275,48 @@ class KeychainModule(reactContext: ReactApplicationContext) :
273275
}
274276

275277
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
299286
}
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+
}
316320
}
317321
}
318322
}
@@ -469,7 +473,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
469473
* set then executed migration.
470474
*/
471475
@Throws(CryptoFailedException::class, KeyStoreAccessException::class)
472-
private fun decryptCredentials(
476+
private suspend fun decryptCredentials(
473477
alias: String,
474478
current: CipherStorage,
475479
resultSet: PrefsStorageBase.ResultSet,
@@ -510,7 +514,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
510514

511515
/** Try to decrypt with provided storage. */
512516
@Throws(CryptoFailedException::class)
513-
private fun decryptToResult(
517+
private suspend fun decryptToResult(
514518
alias: String,
515519
storage: CipherStorage,
516520
resultSet: PrefsStorageBase.ResultSet,
@@ -527,7 +531,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
527531

528532
/** Try to encrypt with provided storage. */
529533
@Throws(CryptoFailedException::class)
530-
private fun encryptToResult(
534+
private suspend fun encryptToResult(
531535
alias: String,
532536
storage: CipherStorage,
533537
username: String,
@@ -558,7 +562,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
558562
@Throws(
559563
KeyStoreAccessException::class, CryptoFailedException::class, IllegalArgumentException::class
560564
)
561-
fun migrateCipherStorage(
565+
private suspend fun migrateCipherStorage(
562566
service: String,
563567
newCipherStorage: CipherStorage,
564568
oldCipherStorage: CipherStorage,

0 commit comments

Comments
 (0)