Skip to content

Commit

Permalink
chore: use mutex instead of experimental limitedParallelism (#692)
Browse files Browse the repository at this point in the history
* chore: use mutex instead of experimental limitedParallelism

* chore: revert SupervisorJob removal
  • Loading branch information
DorianMazur authored Nov 21, 2024
1 parent eeb63a4 commit 6e3f4ce
Showing 1 changed file with 73 additions and 69 deletions.
142 changes: 73 additions & 69 deletions android/src/main/java/com/oblador/keychain/KeychainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ import com.oblador.keychain.exceptions.EmptyParameterException
import com.oblador.keychain.exceptions.KeyStoreAccessException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

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

/** Limit parallelism for coroutineScope */
@OptIn(ExperimentalCoroutinesApi::class)
private val serialDispatcher = Dispatchers.Default.limitedParallelism(1)
/** Mutex to prevent concurrent calls to Cipher, which doesn't support multi-threading */
private val mutex = Mutex()

// endregion
// region Initialization
Expand Down Expand Up @@ -217,28 +217,30 @@ class KeychainModule(reactContext: ReactApplicationContext) :
options: ReadableMap?,
promise: Promise
) {
coroutineScope.launch(serialDispatcher) {
try {
throwIfEmptyLoginPassword(username, password)
val level = getSecurityLevelOrDefault(options)
val storage = getSelectedStorage(options)
throwIfInsufficientLevel(storage, level)
val promptInfo = getPromptInfo(options)
val result = encryptToResult(alias, storage, username, password, level, promptInfo)
prefsStorage.storeEncryptedEntry(alias, result)
val results = Arguments.createMap()
results.putString(Maps.SERVICE, alias)
results.putString(Maps.STORAGE, storage.getCipherStorageName())
promise.resolve(results)
} catch (e: EmptyParameterException) {
Log.e(KEYCHAIN_MODULE, e.message, e)
promise.reject(Errors.E_EMPTY_PARAMETERS, e)
} catch (e: CryptoFailedException) {
Log.e(KEYCHAIN_MODULE, e.message, e)
promise.reject(Errors.E_CRYPTO_FAILED, e)
} catch (fail: Throwable) {
Log.e(KEYCHAIN_MODULE, fail.message, fail)
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
coroutineScope.launch {
mutex.withLock {
try {
throwIfEmptyLoginPassword(username, password)
val level = getSecurityLevelOrDefault(options)
val storage = getSelectedStorage(options)
throwIfInsufficientLevel(storage, level)
val promptInfo = getPromptInfo(options)
val result = encryptToResult(alias, storage, username, password, level, promptInfo)
prefsStorage.storeEncryptedEntry(alias, result)
val results = Arguments.createMap()
results.putString(Maps.SERVICE, alias)
results.putString(Maps.STORAGE, storage.getCipherStorageName())
promise.resolve(results)
} catch (e: EmptyParameterException) {
Log.e(KEYCHAIN_MODULE, e.message, e)
promise.reject(Errors.E_EMPTY_PARAMETERS, e)
} catch (e: CryptoFailedException) {
Log.e(KEYCHAIN_MODULE, e.message, e)
promise.reject(Errors.E_CRYPTO_FAILED, e)
} catch (fail: Throwable) {
Log.e(KEYCHAIN_MODULE, fail.message, fail)
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
}
}
}
}
Expand Down Expand Up @@ -273,46 +275,48 @@ class KeychainModule(reactContext: ReactApplicationContext) :
}

private fun getGenericPassword(alias: String, options: ReadableMap?, promise: Promise) {
coroutineScope.launch(serialDispatcher) {
try {
val resultSet = prefsStorage.getEncryptedEntry(alias)
if (resultSet == null) {
Log.e(KEYCHAIN_MODULE, "No entry found for service: $alias")
promise.resolve(false)
return@launch
}
val storageName = resultSet.cipherStorageName
val rules = getSecurityRulesOrDefault(options)
val promptInfo = getPromptInfo(options)
var cipher: CipherStorage? = null

// Only check for upgradable ciphers for FacebookConseal as that
// is the only cipher that can be upgraded
cipher =
if (rules == Rules.AUTOMATIC_UPGRADE && storageName == KnownCiphers.FB) {
// get the best storage
val accessControl = getAccessControlOrDefault(options)
val useBiometry = getUseBiometry(accessControl)
getCipherStorageForCurrentAPILevel(useBiometry)
} else {
getCipherStorageByName(storageName)
coroutineScope.launch {
mutex.withLock {
try {
val resultSet = prefsStorage.getEncryptedEntry(alias)
if (resultSet == null) {
Log.e(KEYCHAIN_MODULE, "No entry found for service: $alias")
promise.resolve(false)
return@launch
}
val decryptionResult = decryptCredentials(alias, cipher!!, resultSet, rules, promptInfo)
val credentials = Arguments.createMap()
credentials.putString(Maps.SERVICE, alias)
credentials.putString(Maps.USERNAME, decryptionResult.username)
credentials.putString(Maps.PASSWORD, decryptionResult.password)
credentials.putString(Maps.STORAGE, cipher?.getCipherStorageName())
promise.resolve(credentials)
} catch (e: KeyStoreAccessException) {
Log.e(KEYCHAIN_MODULE, e.message!!)
promise.reject(Errors.E_KEYSTORE_ACCESS_ERROR, e)
} catch (e: CryptoFailedException) {
Log.e(KEYCHAIN_MODULE, e.message!!)
promise.reject(Errors.E_CRYPTO_FAILED, e)
} catch (fail: Throwable) {
Log.e(KEYCHAIN_MODULE, fail.message, fail)
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
val storageName = resultSet.cipherStorageName
val rules = getSecurityRulesOrDefault(options)
val promptInfo = getPromptInfo(options)
var cipher: CipherStorage? = null

// Only check for upgradable ciphers for FacebookConseal as that
// is the only cipher that can be upgraded
cipher =
if (rules == Rules.AUTOMATIC_UPGRADE && storageName == KnownCiphers.FB) {
// get the best storage
val accessControl = getAccessControlOrDefault(options)
val useBiometry = getUseBiometry(accessControl)
getCipherStorageForCurrentAPILevel(useBiometry)
} else {
getCipherStorageByName(storageName)
}
val decryptionResult = decryptCredentials(alias, cipher!!, resultSet, rules, promptInfo)
val credentials = Arguments.createMap()
credentials.putString(Maps.SERVICE, alias)
credentials.putString(Maps.USERNAME, decryptionResult.username)
credentials.putString(Maps.PASSWORD, decryptionResult.password)
credentials.putString(Maps.STORAGE, cipher?.getCipherStorageName())
promise.resolve(credentials)
} catch (e: KeyStoreAccessException) {
Log.e(KEYCHAIN_MODULE, e.message!!)
promise.reject(Errors.E_KEYSTORE_ACCESS_ERROR, e)
} catch (e: CryptoFailedException) {
Log.e(KEYCHAIN_MODULE, e.message!!)
promise.reject(Errors.E_CRYPTO_FAILED, e)
} catch (fail: Throwable) {
Log.e(KEYCHAIN_MODULE, fail.message, fail)
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
}
}
}
}
Expand Down Expand Up @@ -469,7 +473,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
* set then executed migration.
*/
@Throws(CryptoFailedException::class, KeyStoreAccessException::class)
private fun decryptCredentials(
private suspend fun decryptCredentials(
alias: String,
current: CipherStorage,
resultSet: PrefsStorageBase.ResultSet,
Expand Down Expand Up @@ -510,7 +514,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :

/** Try to decrypt with provided storage. */
@Throws(CryptoFailedException::class)
private fun decryptToResult(
private suspend fun decryptToResult(
alias: String,
storage: CipherStorage,
resultSet: PrefsStorageBase.ResultSet,
Expand All @@ -527,7 +531,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :

/** Try to encrypt with provided storage. */
@Throws(CryptoFailedException::class)
private fun encryptToResult(
private suspend fun encryptToResult(
alias: String,
storage: CipherStorage,
username: String,
Expand Down Expand Up @@ -558,7 +562,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
@Throws(
KeyStoreAccessException::class, CryptoFailedException::class, IllegalArgumentException::class
)
fun migrateCipherStorage(
private suspend fun migrateCipherStorage(
service: String,
newCipherStorage: CipherStorage,
oldCipherStorage: CipherStorage,
Expand Down

0 comments on commit 6e3f4ce

Please sign in to comment.