From 23bb385190a5c4f27f31092a52853e68c7e13aa6 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 17 Sep 2021 09:56:41 +0200 Subject: [PATCH] Use new storage API for full restore --- .../transport/restore/FullRestore.kt | 25 ++++++--- .../transport/restore/RestoreCoordinator.kt | 5 +- .../transport/restore/RestoreModule.kt | 2 +- .../transport/CoordinatorIntegrationTest.kt | 11 ++-- .../transport/restore/FullRestoreTest.kt | 52 ++++++++++--------- .../restore/RestoreCoordinatorTest.kt | 11 ++-- .../restore/RestoreV0IntegrationTest.kt | 2 +- 7 files changed, 62 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt index 67690366..67fc1da3 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt @@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.header.HeaderReader import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.getADForFull +import com.stevesoltys.seedvault.transport.backup.BackupPlugin import libcore.io.IoUtils.closeQuietly import java.io.EOFException import java.io.IOException @@ -22,6 +23,7 @@ import java.security.GeneralSecurityException private class FullRestoreState( val version: Byte, val token: Long, + val name: String, val packageInfo: PackageInfo ) { var inputStream: InputStream? = null @@ -31,7 +33,8 @@ private val TAG = FullRestore::class.java.simpleName @Suppress("BlockingMethodInNonBlockingContext") internal class FullRestore( - private val plugin: FullRestorePlugin, + private val plugin: BackupPlugin, + private val legacyPlugin: FullRestorePlugin, private val outputFactory: OutputFactory, private val headerReader: HeaderReader, private val crypto: Crypto @@ -43,10 +46,13 @@ internal class FullRestore( /** * Return true if there is data stored for the given package. + * + * Deprecated. Use only for v0 backups. */ @Throws(IOException::class) + @Deprecated("Use BackupPlugin#hasData() instead") suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean { - return plugin.hasDataForPackage(token, packageInfo) + return legacyPlugin.hasDataForPackage(token, packageInfo) } /** @@ -55,8 +61,8 @@ internal class FullRestore( * It is possible that the system decides to not restore the package. * Then a new state will be initialized right away without calling other methods. */ - fun initializeState(version: Byte, token: Long, packageInfo: PackageInfo) { - state = FullRestoreState(version, token, packageInfo) + fun initializeState(version: Byte, token: Long, name: String, packageInfo: PackageInfo) { + state = FullRestoreState(version, token, name, packageInfo) } /** @@ -93,12 +99,16 @@ internal class FullRestore( if (state.inputStream == null) { Log.i(TAG, "First Chunk, initializing package input stream.") try { - val inputStream = plugin.getInputStreamForPackage(state.token, state.packageInfo) - val version = headerReader.readVersion(inputStream, state.version) - if (version == 0.toByte()) { + if (state.version == 0.toByte()) { + val inputStream = + legacyPlugin.getInputStreamForPackage(state.token, state.packageInfo) + val version = headerReader.readVersion(inputStream, state.version) + @Suppress("deprecation") crypto.decryptHeader(inputStream, version, packageName) state.inputStream = inputStream } else { + val inputStream = plugin.getInputStream(state.token, state.name) + val version = headerReader.readVersion(inputStream, state.version) val ad = getADForFull(version, packageName) state.inputStream = crypto.newDecryptingStream(inputStream, ad) } @@ -135,6 +145,7 @@ internal class FullRestore( if (state.version == 0.toByte()) { // read segment from input stream and decrypt it val decrypted = try { + @Suppress("deprecation") crypto.decryptSegment(inputStream) } catch (e: EOFException) { Log.i(TAG, " EOF") diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt index 59511853..0859cdac 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt @@ -216,7 +216,7 @@ internal class RestoreCoordinator( val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName) if (plugin.hasData(state.token, name)) { Log.i(TAG, "Found full backup data for $packageName.") - full.initializeState(version, state.token, packageInfo) + full.initializeState(version, state.token, name, packageInfo) state.currentPackage = packageName TYPE_FULL_STREAM } else throw IOException("No data found for $packageName. Skipping.") @@ -232,6 +232,7 @@ internal class RestoreCoordinator( return RestoreDescription(packageName, type) } + @Suppress("deprecation") private suspend fun nextRestorePackageV0( state: RestoreCoordinatorState, packageInfo: PackageInfo @@ -248,7 +249,7 @@ internal class RestoreCoordinator( } full.hasDataForPackage(state.token, packageInfo) -> { Log.i(TAG, "Found full backup data for $packageName.") - full.initializeState(0x00, state.token, packageInfo) + full.initializeState(0x00, state.token, "", packageInfo) state.currentPackage = packageName TYPE_FULL_STREAM } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt index 833b47da..62756a84 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt @@ -6,7 +6,7 @@ import org.koin.dsl.module val restoreModule = module { single { OutputFactory() } single { KVRestore(get().kvRestorePlugin, get(), get(), get()) } - single { FullRestore(get().fullRestorePlugin, get(), get(), get()) } + single { FullRestore(get(), get().fullRestorePlugin, get(), get(), get()) } single { RestoreCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get()) } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 50988512..7c52c3de 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -97,7 +97,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val fullRestorePlugin = mockk() private val fullRestore = - FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) + FullRestore(backupPlugin, fullRestorePlugin, outputFactory, headerReader, cryptoImpl) private val restore = RestoreCoordinator( context, crypto, @@ -122,7 +122,9 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val key264 = key2.encodeBase64() init { + @Suppress("deprecation") every { backupPlugin.kvBackupPlugin } returns kvBackupPlugin + @Suppress("deprecation") every { backupPlugin.fullBackupPlugin } returns fullBackupPlugin } @@ -349,12 +351,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { // reverse the backup streams into restore input val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray()) val rOutputStream = ByteArrayOutputStream() - coEvery { - fullRestorePlugin.getInputStreamForPackage( - token, - packageInfo - ) - } returns rInputStream + coEvery { backupPlugin.getInputStream(token, name) } returns rInputStream every { outputFactory.getOutputStream(fileDescriptor) } returns rOutputStream // restore data diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt index 9764377c..e6898050 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt @@ -11,6 +11,7 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VersionHeader import com.stevesoltys.seedvault.header.getADForFull +import com.stevesoltys.seedvault.transport.backup.BackupPlugin import io.mockk.CapturingSlot import io.mockk.Runs import io.mockk.coEvery @@ -32,8 +33,9 @@ import kotlin.random.Random @Suppress("BlockingMethodInNonBlockingContext") internal class FullRestoreTest : RestoreTest() { - private val plugin = mockk() - private val restore = FullRestore(plugin, outputFactory, headerReader, crypto) + private val plugin = mockk() + private val legacyPlugin = mockk() + private val restore = FullRestore(plugin, legacyPlugin, outputFactory, headerReader, crypto) private val encrypted = getRandomByteArray() private val outputStream = ByteArrayOutputStream() @@ -45,16 +47,17 @@ internal class FullRestoreTest : RestoreTest() { } @Test - fun `hasDataForPackage() delegates to plugin`() = runBlocking { + @Suppress("deprecation") + fun `v0 hasDataForPackage() delegates to plugin`() = runBlocking { val result = Random.nextBoolean() - coEvery { plugin.hasDataForPackage(token, packageInfo) } returns result + coEvery { legacyPlugin.hasDataForPackage(token, packageInfo) } returns result assertEquals(result, restore.hasDataForPackage(token, packageInfo)) } @Test fun `initializing state leaves a state`() { assertFalse(restore.hasState()) - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) assertTrue(restore.hasState()) } @@ -68,9 +71,9 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `getting InputStream for package when getting first chunk throws`() = runBlocking { - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } throws IOException() + coEvery { plugin.getInputStream(token, name) } throws IOException() every { fileDescriptor.close() } just Runs assertEquals( @@ -81,9 +84,9 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `reading version header when getting first chunk throws`() = runBlocking { - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { plugin.getInputStream(token, name) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } throws IOException() every { fileDescriptor.close() } just Runs @@ -95,9 +98,9 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `reading unsupported version when getting first chunk`() = runBlocking { - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { plugin.getInputStream(token, name) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } throws UnsupportedVersionException(unsupportedVersion) @@ -111,9 +114,9 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `getting decrypted stream when getting first chunk throws`() = runBlocking { - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { plugin.getInputStream(token, name) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION every { crypto.newDecryptingStream(inputStream, ad) } throws IOException() every { fileDescriptor.close() } just Runs @@ -127,9 +130,9 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `getting decrypted stream when getting first chunk throws general security exception`() = runBlocking { - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { plugin.getInputStream(token, name) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION every { crypto.newDecryptingStream(inputStream, ad) } throws GeneralSecurityException() every { fileDescriptor.close() } just Runs @@ -139,7 +142,7 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `full chunk gets decrypted`() = runBlocking { - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) initInputStream() readAndEncryptInputStream(encrypted) @@ -152,10 +155,11 @@ internal class FullRestoreTest : RestoreTest() { } @Test + @Suppress("deprecation") fun `full chunk gets decrypted from version 0`() = runBlocking { - restore.initializeState(0.toByte(), token, packageInfo) + restore.initializeState(0.toByte(), token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { legacyPlugin.getInputStreamForPackage(token, packageInfo) } returns inputStream every { headerReader.readVersion(inputStream, 0.toByte()) } returns 0.toByte() every { crypto.decryptHeader(inputStream, 0.toByte(), packageInfo.packageName) @@ -174,9 +178,9 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `unexpected version aborts with error`() = runBlocking { - restore.initializeState(Byte.MAX_VALUE, token, packageInfo) + restore.initializeState(Byte.MAX_VALUE, token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { plugin.getInputStream(token, name) } returns inputStream every { headerReader.readVersion(inputStream, Byte.MAX_VALUE) } throws GeneralSecurityException() @@ -192,9 +196,9 @@ internal class FullRestoreTest : RestoreTest() { fun `three full chunk get decrypted and then return no more data`() = runBlocking { val encryptedBytes = Random.nextBytes(MAX_SEGMENT_LENGTH * 2 + 1) val decryptedInputStream = ByteArrayInputStream(encryptedBytes) - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { plugin.getInputStream(token, name) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION every { crypto.newDecryptingStream(inputStream, ad) } returns decryptedInputStream every { outputFactory.getOutputStream(fileDescriptor) } returns outputStream @@ -213,7 +217,7 @@ internal class FullRestoreTest : RestoreTest() { @Test fun `aborting full restore closes stream, resets state`() = runBlocking { - restore.initializeState(VERSION, token, packageInfo) + restore.initializeState(VERSION, token, name, packageInfo) initInputStream() readAndEncryptInputStream(encrypted) @@ -227,7 +231,7 @@ internal class FullRestoreTest : RestoreTest() { } private fun initInputStream() { - coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream + coEvery { plugin.getInputStream(token, name) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION every { crypto.newDecryptingStream(inputStream, ad) } returns decryptedInputStream } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt index 1bd51fea..72fe53a3 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt @@ -242,13 +242,14 @@ internal class RestoreCoordinatorTest : TransportTest() { } @Test + @Suppress("deprecation") fun `v0 nextRestorePackage() returns full description if no KV data found`() = runBlocking { restore.beforeStartRestore(metadata.copy(version = 0x00)) restore.startRestore(token, packageInfoArray) coEvery { kv.hasDataForPackage(token, packageInfo) } returns false coEvery { full.hasDataForPackage(token, packageInfo) } returns true - every { full.initializeState(0x00, token, packageInfo) } just Runs + every { full.initializeState(0x00, token, "", packageInfo) } just Runs val expected = RestoreDescription(packageInfo.packageName, TYPE_FULL_STREAM) assertEquals(expected, restore.nextRestorePackage()) @@ -276,7 +277,7 @@ internal class RestoreCoordinatorTest : TransportTest() { every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2 coEvery { plugin.hasData(token, name2) } returns true - every { full.initializeState(VERSION, token, packageInfo2) } just Runs + every { full.initializeState(VERSION, token, name2, packageInfo2) } just Runs val expected = RestoreDescription(packageInfo2.packageName, TYPE_FULL_STREAM) assertEquals(expected, restore.nextRestorePackage()) @@ -298,7 +299,7 @@ internal class RestoreCoordinatorTest : TransportTest() { every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2 coEvery { plugin.hasData(token, name2) } returns true - every { full.initializeState(VERSION, token, packageInfo2) } just Runs + every { full.initializeState(VERSION, token, name2, packageInfo2) } just Runs val expected2 = RestoreDescription(packageInfo2.packageName, TYPE_FULL_STREAM) @@ -308,6 +309,7 @@ internal class RestoreCoordinatorTest : TransportTest() { } @Test + @Suppress("deprecation") fun `v0 nextRestorePackage() returns all packages from startRestore()`() = runBlocking { restore.beforeStartRestore(metadata.copy(version = 0x00)) restore.startRestore(token, packageInfoArray2) @@ -320,7 +322,7 @@ internal class RestoreCoordinatorTest : TransportTest() { coEvery { kv.hasDataForPackage(token, packageInfo2) } returns false coEvery { full.hasDataForPackage(token, packageInfo2) } returns true - every { full.initializeState(0.toByte(), token, packageInfo2) } just Runs + every { full.initializeState(0.toByte(), token, "", packageInfo2) } just Runs val expected2 = RestoreDescription(packageInfo2.packageName, TYPE_FULL_STREAM) assertEquals(expected2, restore.nextRestorePackage()) @@ -352,6 +354,7 @@ internal class RestoreCoordinatorTest : TransportTest() { } @Test + @Suppress("deprecation") fun `v0 when full#hasDataForPackage() throws, it tries next package`() = runBlocking { restore.beforeStartRestore(metadata.copy(version = 0x00)) restore.startRestore(token, packageInfoArray) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt index fd1fe72b..1b06ed76 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt @@ -52,7 +52,7 @@ internal class RestoreV0IntegrationTest : TransportTest() { private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val fullRestorePlugin = mockk() private val fullRestore = - FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) + FullRestore(backupPlugin, fullRestorePlugin, outputFactory, headerReader, cryptoImpl) private val restore = RestoreCoordinator( context, crypto,