Skip to content

Commit d4dc54e

Browse files
authored
feat: improve error handling (#762)
* Added error code constants for both platforms. * Enhanced exception to support error codes - Added additional overloads to simplify usage. - Removed need for reThrowOnError (and "Wrapped error: " prefix). * Removed instances of overwriting the real error. * Applied correct error codes. * Added mapping of various BiometricPrompt error codes. * Added some consistency with Android when an unknown error occurs. * Removed E_SUPPORTED_BIOMETRY_ERROR error code. * Renamed CryptoFailedException to KeychainException. * Made default error code E_UNKNOWN_ERROR. * Removed CRYPTO_FAILED error code. * Removed INTERNAL_ERROR error code. * Improved error code mapping. * Removed need for E_KEYCHAIN_DUPLICATE_ITEM. * Removed E_INTERACTIVE_MODE_UNAVAILABLE error. * Removed KEYCHAIN_INTERACTION_NOT_ALLOWED to IOS_INTERACTION_NOT_ALLOWED. * Split up some biometric error codes. * Consolidated error info. * Removed unnecessary cases. * Fixed case where rejectWithError could be called with no error. * Fixed use of wrong error. * Simplified logic. * Added support for mapping LocalAuthentication errors. * Omit extra iOS cancel code until further investigation. * Moved to generic canceled error code. * Improved function name. * Avoid defaulting to unknown errors. * Correct error code and formatting. * Fixed mapping of unhandled LA error codes. * Consolidated E_AUTH_UNKNOWN_ERROR & E_AUTH_FAILED into E_AUTH_ERROR (renamed from E_BIOMETRIC_ERROR). * Aligned TS enums. * Default to E_INTERNAL_ERROR when custom errors are thrown. * More improvements to error codes. * Removed need for ANDROID_USER_NOT_AUTHENTICATED. * Refactored & aligned error codes. * Removed need for E_BIOMETRIC_INSUFFICIENT_SPACE and BIOMETRIC_UNABLE_TO_PROCESS. * Improved error code names. * Improved message. * Fixed iOS build errors. * Removed AUTH_PERMISSION_DENIED. * Aligned format of error messages. * Renamed enum to ERROR_CODE. * Removed reformatting of unknown errors. * Consolidated configuration errors. * Renamed KEY_PERMANENTLY_INVALIDATED to AUTH_INVALIDATED. * Minor refactor. * Consolidated E_UNKNOWN_ERROR into E_INTERNAL_ERROR. * Fixed logic when can't authenticate. * Wrapped error messages. * Added documentation alongside error codes. * Fixed build error.
1 parent a057429 commit d4dc54e

File tree

13 files changed

+442
-138
lines changed

13 files changed

+442
-138
lines changed

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

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAesGcm
2222
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb
2323
import com.oblador.keychain.resultHandler.ResultHandler
2424
import com.oblador.keychain.resultHandler.ResultHandlerProvider
25-
import com.oblador.keychain.exceptions.CryptoFailedException
25+
import com.oblador.keychain.exceptions.KeychainException
2626
import com.oblador.keychain.exceptions.EmptyParameterException
2727
import com.oblador.keychain.exceptions.KeyStoreAccessException
2828
import kotlinx.coroutines.CoroutineScope
@@ -92,13 +92,23 @@ class KeychainModule(reactContext: ReactApplicationContext) :
9292
/** Known error codes. */
9393
internal annotation class Errors {
9494
companion object {
95-
const val E_EMPTY_PARAMETERS = "E_EMPTY_PARAMETERS"
96-
const val E_CRYPTO_FAILED = "E_CRYPTO_FAILED"
97-
const val E_KEYSTORE_ACCESS_ERROR = "E_KEYSTORE_ACCESS_ERROR"
98-
const val E_SUPPORTED_BIOMETRY_ERROR = "E_SUPPORTED_BIOMETRY_ERROR"
99-
100-
/** Raised for unexpected errors. */
101-
const val E_UNKNOWN_ERROR = "E_UNKNOWN_ERROR"
95+
// Authentication errors
96+
const val E_PASSCODE_NOT_SET = "E_PASSCODE_NOT_SET"
97+
const val E_BIOMETRIC_NOT_ENROLLED = "E_BIOMETRIC_NOT_ENROLLED"
98+
const val E_BIOMETRIC_TIMEOUT = "E_BIOMETRIC_TIMEOUT"
99+
const val E_BIOMETRIC_LOCKOUT = "E_BIOMETRIC_LOCKOUT"
100+
const val E_BIOMETRIC_LOCKOUT_PERMANENT = "E_BIOMETRIC_LOCKOUT_PERMANENT"
101+
const val E_BIOMETRIC_TEMPORARILY_UNAVAILABLE = "E_BIOMETRIC_TEMPORARILY_UNAVAILABLE"
102+
const val E_BIOMETRIC_UNAVAILABLE = "E_BIOMETRIC_UNAVAILABLE"
103+
const val E_BIOMETRIC_VENDOR_ERROR = "E_BIOMETRIC_VENDOR_ERROR"
104+
const val E_AUTH_INVALIDATED = "E_AUTH_INVALIDATED"
105+
const val E_AUTH_CANCELED = "E_AUTH_CANCELED"
106+
const val E_AUTH_ERROR = "E_AUTH_ERROR"
107+
108+
// Misc errors
109+
const val E_INVALID_PARAMETERS = "E_INVALID_PARAMETERS"
110+
const val E_STORAGE_ACCESS_ERROR = "E_STORAGE_ACCESS_ERROR"
111+
const val E_INTERNAL_ERROR = "E_INTERNAL_ERROR"
102112
}
103113
}
104114

@@ -200,13 +210,13 @@ class KeychainModule(reactContext: ReactApplicationContext) :
200210
promise.resolve(results)
201211
} catch (e: EmptyParameterException) {
202212
Log.e(KEYCHAIN_MODULE, e.message, e)
203-
promise.reject(Errors.E_EMPTY_PARAMETERS, e)
204-
} catch (e: CryptoFailedException) {
213+
promise.reject(Errors.E_INVALID_PARAMETERS, e)
214+
} catch (e: KeychainException) {
205215
Log.e(KEYCHAIN_MODULE, e.message, e)
206-
promise.reject(Errors.E_CRYPTO_FAILED, e)
216+
promise.reject(e.errorCode, e)
207217
} catch (fail: Throwable) {
208218
Log.e(KEYCHAIN_MODULE, fail.message, fail)
209-
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
219+
promise.reject(Errors.E_INTERNAL_ERROR, fail)
210220
}
211221
}
212222
}
@@ -224,7 +234,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
224234
}
225235

226236
/** Get Cipher storage instance based on user provided options. */
227-
@Throws(CryptoFailedException::class)
237+
@Throws(KeychainException::class)
228238
private fun getSelectedStorage(options: ReadableMap?): CipherStorage {
229239
val accessControl = getAccessControlOrDefault(options)
230240
val useBiometry = getUseBiometry(accessControl)
@@ -269,13 +279,13 @@ class KeychainModule(reactContext: ReactApplicationContext) :
269279
promise.resolve(credentials)
270280
} catch (e: KeyStoreAccessException) {
271281
Log.e(KEYCHAIN_MODULE, e.message!!)
272-
promise.reject(Errors.E_KEYSTORE_ACCESS_ERROR, e)
273-
} catch (e: CryptoFailedException) {
282+
promise.reject(Errors.E_STORAGE_ACCESS_ERROR, e)
283+
} catch (e: KeychainException) {
274284
Log.e(KEYCHAIN_MODULE, e.message!!)
275-
promise.reject(Errors.E_CRYPTO_FAILED, e)
285+
promise.reject(e.errorCode, e)
276286
} catch (fail: Throwable) {
277287
Log.e(KEYCHAIN_MODULE, fail.message, fail)
278-
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
288+
promise.reject(Errors.E_INTERNAL_ERROR, fail)
279289
}
280290
}
281291
}
@@ -287,7 +297,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
287297
val services = doGetAllGenericPasswordServices()
288298
promise.resolve(Arguments.makeNativeArray<Any>(services.toTypedArray()))
289299
} catch (e: KeyStoreAccessException) {
290-
promise.reject(Errors.E_KEYSTORE_ACCESS_ERROR, e)
300+
promise.reject(Errors.E_STORAGE_ACCESS_ERROR, e)
291301
}
292302
}
293303

@@ -329,10 +339,10 @@ class KeychainModule(reactContext: ReactApplicationContext) :
329339
promise.resolve(true)
330340
} catch (e: KeyStoreAccessException) {
331341
Log.e(KEYCHAIN_MODULE, e.message!!)
332-
promise.reject(Errors.E_KEYSTORE_ACCESS_ERROR, e)
342+
promise.reject(Errors.E_STORAGE_ACCESS_ERROR, e)
333343
} catch (fail: Throwable) {
334344
Log.e(KEYCHAIN_MODULE, fail.message, fail)
335-
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
345+
promise.reject(Errors.E_INTERNAL_ERROR, fail)
336346
}
337347
}
338348

@@ -397,7 +407,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
397407
promise.resolve(reply)
398408
} catch (fail: Throwable) {
399409
Log.e(KEYCHAIN_MODULE, fail.message, fail)
400-
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
410+
promise.reject(Errors.E_INTERNAL_ERROR, fail)
401411
}
402412
}
403413

@@ -417,12 +427,9 @@ class KeychainModule(reactContext: ReactApplicationContext) :
417427
}
418428
}
419429
promise.resolve(reply)
420-
} catch (e: Exception) {
421-
Log.e(KEYCHAIN_MODULE, e.message, e)
422-
promise.reject(Errors.E_SUPPORTED_BIOMETRY_ERROR, e)
423430
} catch (fail: Throwable) {
424431
Log.e(KEYCHAIN_MODULE, fail.message, fail)
425-
promise.reject(Errors.E_UNKNOWN_ERROR, fail)
432+
promise.reject(Errors.E_INTERNAL_ERROR, fail)
426433
}
427434
}
428435

@@ -442,7 +449,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
442449
* Extract credentials from current storage. In case if current storage is not matching results
443450
* set then executed migration.
444451
*/
445-
@Throws(CryptoFailedException::class, KeyStoreAccessException::class)
452+
@Throws(KeychainException::class, KeyStoreAccessException::class)
446453
private suspend fun decryptCredentials(
447454
alias: String,
448455
current: CipherStorage,
@@ -472,7 +479,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
472479
}
473480

474481
/** Try to decrypt with provided storage. */
475-
@Throws(CryptoFailedException::class)
482+
@Throws(KeychainException::class)
476483
private suspend fun decryptToResult(
477484
alias: String,
478485
storage: CipherStorage,
@@ -487,15 +494,18 @@ class KeychainModule(reactContext: ReactApplicationContext) :
487494
resultSet.password!!,
488495
SecurityLevel.ANY
489496
)
490-
CryptoFailedException.reThrowOnError(handler.error)
491-
if (null == handler.decryptionResult) {
492-
throw CryptoFailedException("No decryption results and no error. Something deeply wrong!")
497+
val error = handler.error
498+
if (error != null) {
499+
throw KeychainException(error.message, error)
500+
}
501+
if (handler.decryptionResult == null) {
502+
throw KeychainException("No decryption results and no error. Something deeply wrong!")
493503
}
494504
return handler.decryptionResult!!
495505
}
496506

497507
/** Try to encrypt with provided storage. */
498-
@Throws(CryptoFailedException::class)
508+
@Throws(KeychainException::class)
499509
private suspend fun encryptToResult(
500510
alias: String,
501511
storage: CipherStorage,
@@ -506,9 +516,12 @@ class KeychainModule(reactContext: ReactApplicationContext) :
506516
): CipherStorage.EncryptionResult {
507517
val handler = getInteractiveHandler(storage, promptInfo)
508518
storage.encrypt(handler, alias, username, password, securityLevel)
509-
CryptoFailedException.reThrowOnError(handler.error)
510-
if (null == handler.encryptionResult) {
511-
throw CryptoFailedException("No decryption results and no error. Something deeply wrong!")
519+
val error = handler.error
520+
if (error != null) {
521+
throw KeychainException(error.message, error)
522+
}
523+
if (handler.encryptionResult == null) {
524+
throw KeychainException("No encryption results and no error. Something deeply wrong!")
512525
}
513526
return handler.encryptionResult!!
514527
}
@@ -526,7 +539,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
526539
/* package */
527540
@Throws(
528541
KeyStoreAccessException::class,
529-
CryptoFailedException::class,
542+
KeychainException::class,
530543
IllegalArgumentException::class
531544
)
532545
private suspend fun migrateCipherStorage(
@@ -563,7 +576,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
563576
* The "Current" CipherStorage is the cipherStorage with the highest API level that is lower than
564577
* or equal to the current API level. Parameter allow to reduce level.
565578
*/
566-
@Throws(CryptoFailedException::class)
579+
@Throws(KeychainException::class)
567580
fun getCipherStorageForCurrentAPILevel(
568581
useBiometry: Boolean,
569582
usePasscode: Boolean
@@ -594,7 +607,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
594607
foundCipher = variant
595608
}
596609
if (foundCipher == null) {
597-
throw CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT)
610+
throw KeychainException("Unsupported Android SDK " + Build.VERSION.SDK_INT, Errors.E_INVALID_PARAMETERS)
598611
}
599612
Log.d(KEYCHAIN_MODULE, "Selected storage: " + foundCipher.getCipherStorageName())
600613
return foundCipher
@@ -641,7 +654,7 @@ class KeychainModule(reactContext: ReactApplicationContext) :
641654
if (isSecureHardwareAvailable) {
642655
SecurityLevel.SECURE_HARDWARE
643656
} else SecurityLevel.SECURE_SOFTWARE
644-
} catch (e: CryptoFailedException) {
657+
} catch (e: KeychainException) {
645658
Log.w(KEYCHAIN_MODULE, "Security Level Exception: " + e.message, e)
646659
SecurityLevel.ANY
647660
}
@@ -799,17 +812,18 @@ class KeychainModule(reactContext: ReactApplicationContext) :
799812
/**
800813
* Throw exception if required security level does not match storage provided security level.
801814
*/
802-
@Throws(CryptoFailedException::class)
815+
@Throws(KeychainException::class)
803816
fun throwIfInsufficientLevel(storage: CipherStorage, level: SecurityLevel) {
804817
if (storage.securityLevel().satisfiesSafetyThreshold(level)) {
805818
return
806819
}
807-
throw CryptoFailedException(
820+
throw KeychainException(
808821
String.format(
809822
"Cipher Storage is too weak. Required security level is: %s, but only %s is provided",
810823
level.name,
811824
storage.securityLevel().name
812-
)
825+
),
826+
Errors.E_INVALID_PARAMETERS
813827
)
814828
}
815829

android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.oblador.keychain.cipherStorage
22

33
import com.oblador.keychain.SecurityLevel
44
import com.oblador.keychain.resultHandler.ResultHandler
5-
import com.oblador.keychain.exceptions.CryptoFailedException
5+
import com.oblador.keychain.exceptions.KeychainException
66
import com.oblador.keychain.exceptions.KeyStoreAccessException
77

88
@Suppress("unused", "MemberVisibilityCanBePrivate")
@@ -44,10 +44,10 @@ interface CipherStorage {
4444
/**
4545
* Encrypt credentials with provided key (by alias) and required security level.
4646
*
47-
* @throws CryptoFailedException If encryption fails.
47+
* @throws KeychainException If encryption fails.
4848
*/
4949

50-
@Throws(CryptoFailedException::class)
50+
@Throws(KeychainException::class)
5151
fun encrypt(
5252
handler: ResultHandler,
5353
alias: String,
@@ -60,9 +60,9 @@ interface CipherStorage {
6060
/**
6161
* Decrypt the credentials but redirect results of operation to handler.
6262
*
63-
* @throws CryptoFailedException If decryption fails.
63+
* @throws KeychainException If decryption fails.
6464
*/
65-
@Throws(CryptoFailedException::class)
65+
@Throws(KeychainException::class)
6666
fun decrypt(
6767
handler: ResultHandler,
6868
alias: String,

android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageBase.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import android.security.keystore.UserNotAuthenticatedException
99
import android.util.Log
1010
import androidx.annotation.VisibleForTesting
1111
import com.oblador.keychain.DeviceAvailability
12+
import com.oblador.keychain.KeychainModule.Errors
1213
import com.oblador.keychain.SecurityLevel
1314
import com.oblador.keychain.cipherStorage.CipherStorageBase.DecryptBytesHandler
1415
import com.oblador.keychain.cipherStorage.CipherStorageBase.EncryptStringHandler
15-
import com.oblador.keychain.exceptions.CryptoFailedException
16+
import com.oblador.keychain.exceptions.KeychainException
1617
import com.oblador.keychain.exceptions.KeyStoreAccessException
1718
import java.io.ByteArrayInputStream
1819
import java.io.ByteArrayOutputStream
@@ -164,11 +165,12 @@ abstract class CipherStorageBase(protected val applicationContext: Context) : Ci
164165
}
165166

166167
/** Check requirements to the security level. */
167-
@Throws(CryptoFailedException::class)
168+
@Throws(KeychainException::class)
168169
protected fun throwIfInsufficientLevel(level: SecurityLevel) {
169170
if (!securityLevel().satisfiesSafetyThreshold(level)) {
170-
throw CryptoFailedException(
171-
"Insufficient security level (wants $level; got ${securityLevel()})"
171+
throw KeychainException(
172+
"Insufficient security level (wants $level; got ${securityLevel()})",
173+
Errors.E_INVALID_PARAMETERS
172174
)
173175
}
174176
}
@@ -328,7 +330,7 @@ abstract class CipherStorageBase(protected val applicationContext: Context) : Ci
328330

329331
/** Decrypt provided bytes to a string. */
330332
@SuppressLint("NewApi")
331-
@Throws(GeneralSecurityException::class, IOException::class, CryptoFailedException::class)
333+
@Throws(GeneralSecurityException::class, IOException::class, KeychainException::class)
332334
protected open fun decryptBytes(
333335
key: Key,
334336
bytes: ByteArray,
@@ -351,7 +353,7 @@ abstract class CipherStorageBase(protected val applicationContext: Context) : Ci
351353
throw UserNotAuthenticatedException()
352354
}
353355
e is javax.crypto.AEADBadTagException -> {
354-
throw CryptoFailedException(
356+
throw KeychainException(
355357
"Decryption failed: Authentication tag verification failed. " +
356358
"This usually indicates that the encrypted data was modified, corrupted, " +
357359
"or is being decrypted with the wrong key.",
@@ -402,7 +404,7 @@ abstract class CipherStorageBase(protected val applicationContext: Context) : Ci
402404
}
403405

404406
if (!validateKeySecurityLevel(requiredLevel, secretKey!!)) {
405-
throw CryptoFailedException("Cannot generate keys with required security guarantees")
407+
throw KeychainException("Cannot generate keys with required security guarantees", Errors.E_INVALID_PARAMETERS)
406408
}
407409
}
408410

android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesCbc.kt

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.oblador.keychain.KeychainModule.KnownCiphers
1010
import com.oblador.keychain.SecurityLevel
1111
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAesCbc.IV.IV_LENGTH
1212
import com.oblador.keychain.resultHandler.ResultHandler
13-
import com.oblador.keychain.exceptions.CryptoFailedException
13+
import com.oblador.keychain.exceptions.KeychainException
1414
import java.io.IOException
1515
import java.security.GeneralSecurityException
1616
import java.security.Key
@@ -74,7 +74,7 @@ class CipherStorageKeystoreAesCbc(reactContext: ReactApplicationContext) :
7474

7575
// region Overrides
7676

77-
@Throws(CryptoFailedException::class)
77+
@Throws(KeychainException::class)
7878
override fun encrypt(
7979
handler: ResultHandler,
8080
alias: String,
@@ -95,19 +95,14 @@ class CipherStorageKeystoreAesCbc(reactContext: ReactApplicationContext) :
9595
encryptString(key, username), encryptString(key, password), this
9696
)
9797
handler.onEncrypt(result, null)
98-
} catch (e: GeneralSecurityException) {
99-
throw CryptoFailedException("Could not encrypt data with alias: $alias", e)
10098
} catch (fail: Throwable) {
101-
throw CryptoFailedException(
102-
"Unknown error with alias: $alias, error: ${fail.message}",
103-
fail
104-
)
99+
throw KeychainException("Could not encrypt data with alias: $alias, error: ${fail.message}", fail)
105100
}
106101
}
107102

108103

109104
/** Redirect call to [decrypt] method. */
110-
@Throws(CryptoFailedException::class)
105+
@Throws(KeychainException::class)
111106
override fun decrypt(
112107
handler: ResultHandler,
113108
alias: String,
@@ -129,7 +124,7 @@ class CipherStorageKeystoreAesCbc(reactContext: ReactApplicationContext) :
129124
)
130125
handler.onDecrypt(results, null)
131126
} catch (e: GeneralSecurityException) {
132-
throw CryptoFailedException("Could not decrypt data with alias: $alias", e)
127+
throw KeychainException("Could not decrypt data with alias: $alias, error: ${e.message}", e)
133128
} catch (fail: Throwable) {
134129
handler.onDecrypt(null, fail)
135130
}

0 commit comments

Comments
 (0)