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…