diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/KeystoreHelper.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/KeystoreHelper.kt index 9d20cd7ca..d6db64f0d 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/KeystoreHelper.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/KeystoreHelper.kt @@ -85,6 +85,33 @@ object KeystoreHelper { else -> throw IllegalArgumentException("unhandled key=$keyName") } + /** + * Get the encryption cipher for the key. If it fails once, delete the key, and try again. This should only be used by the seed fallback to check if the + * keystore is accessible. This seems to (rarely) happen after an OS update that fails to upgrade the key (raising a `upgrade_keyblob_if_required_with` + * error), and might be related to the secure element option? + */ + fun checkEncryptionCipherOrReset(keyName: String) = when (keyName) { + KEY_NO_AUTH -> { + try { + getEncryptionCipher(keyName) + } catch (e: Exception) { + log.error("could not get encryption cipher: ${e.localizedMessage}") + try { + log.error("deleting key=$keyName from keystore") + keyStore.deleteEntry(keyName) + getEncryptionCipher(keyName) + } catch (e: Exception) { + log.error("cannot delete $keyName entry from keystore: ${e.localizedMessage}") + throw e + } + } + } + + else -> { + throw IllegalArgumentException("unhandled key_name=$keyName") + } + } + /** Get encryption Cipher for given key. */ internal fun getEncryptionCipher(keyName: String): Cipher = Cipher.getInstance("$ENC_ALGO/$ENC_BLOCK_MODE/$ENC_PADDING").apply { init(Cipher.ENCRYPT_MODE, getKeyForName(keyName), parameters) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt index 283b9026e..f03d6c4cb 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt @@ -370,6 +370,9 @@ private fun StartupSeedFallback( icon = R.drawable.ic_check_circle ) } + is StartupDecryptionState.SeedInputFallback.Error.KeyStoreFailure -> { + Text(text = stringResource(id = R.string.startup_fallback_error_keystore_error)) + } } Spacer(modifier = Modifier.height(100.dp)) } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt index 58d7b499f..793f2fbaf 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt @@ -24,6 +24,7 @@ import fr.acinq.bitcoin.MnemonicCode import fr.acinq.bitcoin.byteVector import fr.acinq.lightning.crypto.LocalKeyManager import fr.acinq.phoenix.android.security.EncryptedSeed +import fr.acinq.phoenix.android.security.KeystoreHelper import fr.acinq.phoenix.android.security.SeedManager import fr.acinq.phoenix.android.services.NodeService import fr.acinq.phoenix.managers.NodeParamsManager @@ -39,23 +40,24 @@ import java.security.KeyStoreException sealed class StartupDecryptionState { - object Init : StartupDecryptionState() - object DecryptingSeed : StartupDecryptionState() - object DecryptionSuccess : StartupDecryptionState() + data object Init : StartupDecryptionState() + data object DecryptingSeed : StartupDecryptionState() + data object DecryptionSuccess : StartupDecryptionState() sealed class DecryptionError : StartupDecryptionState() { data class Other(val cause: Throwable): DecryptionError() data class KeystoreFailure(val cause: Throwable): DecryptionError() } sealed class SeedInputFallback : StartupDecryptionState() { - object Init: SeedInputFallback() - object CheckingSeed: SeedInputFallback() + data object Init: SeedInputFallback() + data object CheckingSeed: SeedInputFallback() sealed class Success: SeedInputFallback() { object MatchingData: Success() object WrittenToDisk: Success() } sealed class Error: SeedInputFallback() { data class Other(val cause: Throwable): Error() - object SeedDoesNotMatch: Error() + data object SeedDoesNotMatch: Error() + data class KeyStoreFailure(val cause: Throwable): Error() } } } @@ -103,6 +105,12 @@ class StartupViewModel : ViewModel() { if (channelsDbFile.exists()) { decryptionState.value = StartupDecryptionState.SeedInputFallback.Success.MatchingData val encodedSeed = EncryptedSeed.fromMnemonics(words) + try { + KeystoreHelper.checkEncryptionCipherOrReset(KeystoreHelper.KEY_NO_AUTH) + } catch (e: Exception) { + decryptionState.value = StartupDecryptionState.SeedInputFallback.Error.SeedDoesNotMatch + return@launch + } val encrypted = EncryptedSeed.V2.NoAuth.encrypt(encodedSeed) SeedManager.writeSeedToDisk(context, encrypted, overwrite = true) delay(1000) diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml index 21665045b..6c90e0a13 100644 --- a/phoenix-android/src/main/res/values/strings.xml +++ b/phoenix-android/src/main/res/values/strings.xml @@ -82,7 +82,8 @@ Unlock wallet Checking seed… An error occurred. Please try again. - This seed does not match your wallet. + This seed does not match your wallet data. + Failed to perform keystore operations. Seed matches.\nWriting to disk… Starting Phoenix…