Improve error message when no backups could be found for restore
This commit is contained in:
parent
54ad762eb1
commit
10ad6d6b2d
7 changed files with 20 additions and 22 deletions
|
@ -17,7 +17,7 @@ internal const val JSON_TOKEN = "token"
|
||||||
internal const val JSON_ANDROID_VERSION = "androidVersion"
|
internal const val JSON_ANDROID_VERSION = "androidVersion"
|
||||||
internal const val JSON_DEVICE_NAME = "deviceName"
|
internal const val JSON_DEVICE_NAME = "deviceName"
|
||||||
|
|
||||||
class FormatException(cause: Throwable) : Exception(cause)
|
class DecryptionFailedException(cause: Throwable) : Exception(cause)
|
||||||
|
|
||||||
class EncryptedBackupMetadata private constructor(val token: Long, val inputStream: InputStream?, val error: Boolean) {
|
class EncryptedBackupMetadata private constructor(val token: Long, val inputStream: InputStream?, val error: Boolean) {
|
||||||
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
|
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
|
||||||
|
|
|
@ -13,14 +13,14 @@ import javax.crypto.AEADBadTagException
|
||||||
|
|
||||||
interface MetadataReader {
|
interface MetadataReader {
|
||||||
|
|
||||||
@Throws(FormatException::class, SecurityException::class, UnsupportedVersionException::class, IOException::class)
|
@Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class)
|
||||||
fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata
|
fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
|
|
||||||
@Throws(FormatException::class, SecurityException::class, UnsupportedVersionException::class, IOException::class)
|
@Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class)
|
||||||
override fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata {
|
override fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata {
|
||||||
val version = inputStream.read().toByte()
|
val version = inputStream.read().toByte()
|
||||||
if (version < 0) throw IOException()
|
if (version < 0) throw IOException()
|
||||||
|
@ -28,14 +28,13 @@ class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
val metadataBytes = try {
|
val metadataBytes = try {
|
||||||
crypto.decryptSegment(inputStream)
|
crypto.decryptSegment(inputStream)
|
||||||
} catch (e: AEADBadTagException) {
|
} catch (e: AEADBadTagException) {
|
||||||
// TODO use yet another exception?
|
throw DecryptionFailedException(e)
|
||||||
throw SecurityException(e)
|
|
||||||
}
|
}
|
||||||
return decode(metadataBytes, version, expectedToken)
|
return decode(metadataBytes, version, expectedToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@Throws(FormatException::class, SecurityException::class)
|
@Throws(SecurityException::class)
|
||||||
internal fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata {
|
internal fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata {
|
||||||
// NOTE: We don't do extensive validation of the parsed input here,
|
// NOTE: We don't do extensive validation of the parsed input here,
|
||||||
// because it was encrypted with authentication, so we should be able to trust it.
|
// because it was encrypted with authentication, so we should be able to trust it.
|
||||||
|
@ -59,7 +58,7 @@ class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
deviceName = json.getString(JSON_DEVICE_NAME)
|
deviceName = json.getString(JSON_DEVICE_NAME)
|
||||||
)
|
)
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) {
|
||||||
throw FormatException(e)
|
throw SecurityException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.backup.header.UnsupportedVersionException
|
import com.stevesoltys.backup.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.backup.metadata.FormatException
|
import com.stevesoltys.backup.metadata.DecryptionFailedException
|
||||||
import com.stevesoltys.backup.metadata.MetadataReader
|
import com.stevesoltys.backup.metadata.MetadataReader
|
||||||
import com.stevesoltys.backup.settings.getBackupToken
|
import com.stevesoltys.backup.settings.getBackupToken
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
|
@ -50,12 +50,12 @@ internal class RestoreCoordinator(
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error while getting restore sets", e)
|
Log.e(TAG, "Error while getting restore sets", e)
|
||||||
return null
|
return null
|
||||||
} catch (e: FormatException) {
|
|
||||||
Log.e(TAG, "Error while getting restore sets", e)
|
|
||||||
return null
|
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "Error while getting restore sets", e)
|
Log.e(TAG, "Error while getting restore sets", e)
|
||||||
return null
|
return null
|
||||||
|
} catch (e: DecryptionFailedException) {
|
||||||
|
Log.e(TAG, "Error while decrypting restore set", e)
|
||||||
|
continue
|
||||||
} catch (e: UnsupportedVersionException) {
|
} catch (e: UnsupportedVersionException) {
|
||||||
Log.w(TAG, "Backup with unsupported version read", e)
|
Log.w(TAG, "Backup with unsupported version read", e)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -47,9 +47,6 @@ class RecoveryCodeViewModel(application: Application) : AndroidViewModel(applica
|
||||||
} catch (e: InvalidWordCountException) {
|
} catch (e: InvalidWordCountException) {
|
||||||
throw AssertionError(e)
|
throw AssertionError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO if (isRestore) check if we can decrypt a backup
|
|
||||||
|
|
||||||
val mnemonic = input.joinToString(" ")
|
val mnemonic = input.joinToString(" ")
|
||||||
val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "")
|
val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "")
|
||||||
Backup.keyManager.storeBackupKey(seed)
|
Backup.keyManager.storeBackupKey(seed)
|
||||||
|
|
|
@ -54,7 +54,8 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/errorView"
|
android:id="@+id/errorView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@android:color/holo_red_dark"
|
android:textColor="@android:color/holo_red_dark"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
|
@ -63,7 +64,8 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
||||||
tools:text="There was an error retrieving your backups." />
|
tools:text="@string/restore_set_empty_result"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/backView"
|
android:id="@+id/backView"
|
||||||
|
|
|
@ -90,8 +90,8 @@
|
||||||
<string name="restore_back">Don\'t restore</string>
|
<string name="restore_back">Don\'t restore</string>
|
||||||
<string name="restore_invalid_location_title">No backups found</string>
|
<string name="restore_invalid_location_title">No backups found</string>
|
||||||
<string name="restore_invalid_location_message">We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder.</string>
|
<string name="restore_invalid_location_message">We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder.</string>
|
||||||
<string name="restore_set_error">An error occurred loading the backups.</string>
|
<string name="restore_set_error">An error occurred while loading the backups.</string>
|
||||||
<string name="restore_set_empty_result">No backups found at given location.</string>
|
<string name="restore_set_empty_result">No suitable backups found at given location.\n\nThis is most likely due to a wrong recovery code or a storage error.</string>
|
||||||
<string name="restore_restoring">Restoring Backup</string>
|
<string name="restore_restoring">Restoring Backup</string>
|
||||||
<string name="restore_current_package">Restoring %s…</string>
|
<string name="restore_current_package">Restoring %s…</string>
|
||||||
<string name="restore_finished_success">Restore complete.</string>
|
<string name="restore_finished_success">Restore complete.</string>
|
||||||
|
|
|
@ -47,21 +47,21 @@ class MetadataReaderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `malformed JSON throws FormatException`() {
|
fun `malformed JSON throws SecurityException`() {
|
||||||
assertThrows(FormatException::class.java) {
|
assertThrows(SecurityException::class.java) {
|
||||||
decoder.decode("{".toByteArray(Utf8), metadata.version, metadata.token)
|
decoder.decode("{".toByteArray(Utf8), metadata.version, metadata.token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `missing fields throws FormatException`() {
|
fun `missing fields throws SecurityException`() {
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
json.put(JSON_VERSION, metadata.version.toInt())
|
json.put(JSON_VERSION, metadata.version.toInt())
|
||||||
json.put(JSON_TOKEN, metadata.token)
|
json.put(JSON_TOKEN, metadata.token)
|
||||||
json.put(JSON_ANDROID_VERSION, metadata.androidVersion)
|
json.put(JSON_ANDROID_VERSION, metadata.androidVersion)
|
||||||
val jsonBytes = json.toString().toByteArray(Utf8)
|
val jsonBytes = json.toString().toByteArray(Utf8)
|
||||||
|
|
||||||
assertThrows(FormatException::class.java) {
|
assertThrows(SecurityException::class.java) {
|
||||||
decoder.decode(jsonBytes, metadata.version, metadata.token)
|
decoder.decode(jsonBytes, metadata.version, metadata.token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue