@@ -19,8 +19,11 @@ import to.bitkit.ext.fromBase64
1919import to.bitkit.ext.toBase64
2020import to.bitkit.utils.AppError
2121import to.bitkit.utils.Logger
22+ import java.util.Collections
23+ import java.util.concurrent.ConcurrentHashMap
2224import javax.inject.Inject
2325import javax.inject.Singleton
26+ import kotlin.coroutines.cancellation.CancellationException
2427
2528private val Context .keychainDataStore: DataStore <Preferences > by preferencesDataStore(
2629 name = " keychain"
@@ -32,59 +35,76 @@ class Keychain @Inject constructor(
3235 @ApplicationContext private val context : Context ,
3336 @IoDispatcher private val dispatcher : CoroutineDispatcher ,
3437) : BaseCoroutineScope(dispatcher) {
38+ companion object {
39+ private const val TAG = " Keychain"
40+ private const val CAUSE_CHAIN_DEPTH = 4
41+ }
42+
3543 private val keyStore by lazy { AndroidKeyStore (alias = " keychain" ) }
3644
3745 @Suppress(" MemberNameEqualsClassName" )
3846 private val keychain = context.keychainDataStore
3947
48+ private val loadDiagnosticsEmitted: MutableSet <String > =
49+ Collections .newSetFromMap(ConcurrentHashMap <String , Boolean >())
50+
4051 val snapshot get() = runBlocking(this .coroutineContext) { keychain.data.first() }
4152
4253 fun loadString (key : String ): String? = load(key)?.decodeToString()
4354
44- @Suppress(" TooGenericExceptionCaught" , " SwallowedException " )
55+ @Suppress(" TooGenericExceptionCaught" )
4556 fun load (key : String ): ByteArray? {
4657 try {
4758 return snapshot[key.indexed]?.fromBase64()?.let {
4859 keyStore.decrypt(it)
4960 }
50- } catch (_: Exception ) {
51- throw KeychainError .FailedToLoad (key)
61+ } catch (c: CancellationException ) {
62+ throw c
63+ } catch (t: Throwable ) {
64+ emitLoadDiagnosticsOnce(key, t)
65+ throw KeychainError .FailedToLoad (key, cause = t)
5266 }
5367 }
5468
5569 suspend fun saveString (key : String , value : String ) = save(key, value.toByteArray())
5670
57- @Suppress(" TooGenericExceptionCaught" , " SwallowedException " )
71+ @Suppress(" TooGenericExceptionCaught" , " ThrowsCount " )
5872 suspend fun save (key : String , value : ByteArray ) {
5973 if (exists(key)) throw KeychainError .FailedToSaveAlreadyExists (key)
6074
6175 try {
6276 val encryptedValue = keyStore.encrypt(value)
6377 keychain.edit { it[key.indexed] = encryptedValue.toBase64() }
64- } catch (_: Exception ) {
65- throw KeychainError .FailedToSave (key)
78+ } catch (c: CancellationException ) {
79+ throw c
80+ } catch (t: Throwable ) {
81+ throw KeychainError .FailedToSave (key, cause = t)
6682 }
6783 Logger .info(" Saved to keychain: $key " )
6884 }
6985
7086 /* * Inserts or replaces a string value associated with a given key in the keychain. */
71- @Suppress(" TooGenericExceptionCaught" , " SwallowedException " )
87+ @Suppress(" TooGenericExceptionCaught" )
7288 suspend fun upsertString (key : String , value : String ) {
7389 try {
7490 val encryptedValue = keyStore.encrypt(value.toByteArray())
7591 keychain.edit { it[key.indexed] = encryptedValue.toBase64() }
76- } catch (_: Exception ) {
77- throw KeychainError .FailedToSave (key)
92+ } catch (c: CancellationException ) {
93+ throw c
94+ } catch (t: Throwable ) {
95+ throw KeychainError .FailedToSave (key, cause = t)
7896 }
7997 Logger .info(" Upsert in keychain: $key " )
8098 }
8199
82- @Suppress(" TooGenericExceptionCaught" , " SwallowedException " )
100+ @Suppress(" TooGenericExceptionCaught" )
83101 suspend fun delete (key : String ) {
84102 try {
85103 keychain.edit { it.remove(key.indexed) }
86- } catch (_: Exception ) {
87- throw KeychainError .FailedToDelete (key)
104+ } catch (c: CancellationException ) {
105+ throw c
106+ } catch (t: Throwable ) {
107+ throw KeychainError .FailedToDelete (key, cause = t)
88108 }
89109 Logger .debug(" Deleted from keychain: $key " )
90110 }
@@ -120,6 +140,32 @@ class Keychain @Inject constructor(
120140 .map { string -> string?.toIntOrNull() }
121141 }
122142
143+ private fun emitLoadDiagnosticsOnce (key : String , cause : Throwable ) {
144+ if (! loadDiagnosticsEmitted.add(key)) return
145+
146+ val aliasPresent = probe { keyStore.containsAlias() }
147+ val entryPresent = probe { snapshot.contains(key.indexed) }
148+ val walletIndex = probe {
149+ runBlocking { db.configDao().getAll().first() }.firstOrNull()?.walletIndex ? : 0L
150+ }
151+ val causeChain = generateSequence(cause) { it.cause }
152+ .take(CAUSE_CHAIN_DEPTH )
153+ .joinToString(separator = " <- " ) { it.javaClass.simpleName }
154+
155+ Logger .warn(
156+ " Decrypt failed for key='$key ' walletIndex='$walletIndex ' " +
157+ " aliasPresent='$aliasPresent ' entryPresent='$entryPresent ' " +
158+ " causeChain='$causeChain '" ,
159+ context = TAG ,
160+ )
161+ }
162+
163+ private inline fun <T > probe (block : () -> T ): String =
164+ runCatching(block).fold(
165+ onSuccess = { it.toString() },
166+ onFailure = { " error:${it.javaClass.simpleName} " },
167+ )
168+
123169 enum class Key {
124170 PUSH_NOTIFICATION_TOKEN ,
125171 PUSH_NOTIFICATION_PRIVATE_KEY ,
@@ -130,10 +176,19 @@ class Keychain @Inject constructor(
130176 }
131177}
132178
133- sealed class KeychainError (message : String ) : AppError(message) {
134- class FailedToDelete (key : String ) : KeychainError(" Failed to delete $key from keychain." )
135- class FailedToLoad (key : String ) : KeychainError(" Failed to load $key from keychain." )
136- class FailedToSave (key : String ) : KeychainError(" Failed to save to $key keychain." )
179+ sealed class KeychainError (message : String , cause : Throwable ? = null ) : AppError(message, cause) {
180+ class FailedToDelete (key : String , cause : Throwable ? = null ) :
181+ KeychainError (" Failed to delete $key from keychain${cause.causeSuffix()} " , cause)
182+
183+ class FailedToLoad (val key : String , cause : Throwable ? = null ) :
184+ KeychainError (" Failed to load $key from keychain${cause.causeSuffix()} " , cause)
185+
186+ class FailedToSave (key : String , cause : Throwable ? = null ) :
187+ KeychainError (" Failed to save to $key keychain${cause.causeSuffix()} " , cause)
188+
137189 class FailedToSaveAlreadyExists (key : String ) :
138190 KeychainError (" Key $key already exists in keychain. Explicitly delete key before attempting to update value." )
139191}
192+
193+ private fun Throwable?.causeSuffix (): String =
194+ this ?.let { " (cause='${it.javaClass.simpleName} ')" } ? : " ."
0 commit comments