diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt index cd905036..675607bc 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt @@ -171,11 +171,13 @@ internal const val TYPE_BACKUP_KV: Byte = 0x01 internal const val TYPE_BACKUP_FULL: Byte = 0x02 internal const val TYPE_ICONS: Byte = 0x03 +@SuppressLint("HardwareIds") internal class CryptoImpl( - private val context: Context, + context: Context, private val keyManager: KeyManager, private val cipherFactory: CipherFactory, private val headerReader: HeaderReader, + private val androidId: String = Settings.Secure.getString(context.contentResolver, ANDROID_ID), ) : Crypto { private val keyV1: ByteArray by lazy { @@ -198,8 +200,6 @@ internal class CryptoImpl( * so all lazy values that depend on that key or the [gearTableKey] get reinitialized. */ override val repoId: String by lazy { - @SuppressLint("HardwareIds") - val androidId = Settings.Secure.getString(context.contentResolver, ANDROID_ID) val repoIdKey = deriveKey(keyManager.getMainKey(), "app backup repoId key".toByteArray()) val hmacHasher: Mac = Mac.getInstance(ALGORITHM_HMAC).apply { diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt index c0c3e355..e11e46d0 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt @@ -29,8 +29,7 @@ class CryptoImplTest { private val cipherFactory = mockk() private val headerReader = HeaderReaderImpl() - private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader) - + private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader, "androidId") @Test fun `decrypting multiple segments on empty stream throws`() { val inputStream = ByteArrayInputStream(ByteArray(0)) diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt index 4862b6c6..6062d5a2 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt @@ -29,7 +29,7 @@ class CryptoIntegrationTest { private val keyManager = KeyManagerTestImpl() private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader) + private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader, "androidId") private val cleartext = Random.nextBytes(Random.nextInt(1, 422300)) diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt index 912247b5..f2946aba 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt @@ -42,7 +42,7 @@ class CryptoTest { private val cipherFactory = mockk() private val headerReader = mockk() - private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader) + private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader, "androidId") private val cipher = mockk() diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt index 19362f38..37111278 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt @@ -35,7 +35,8 @@ internal class MetadataV0ReadTest { private val keyManager = KeyManagerTestImpl(secretKey) private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) + private val cryptoImpl = + CryptoImpl(context, keyManager, cipherFactory, headerReader, "androidId") private val reader = MetadataReaderImpl(cryptoImpl) 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 f5da1039..f3d1f0f3 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -65,7 +65,8 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val keyManager = KeyManagerTestImpl() private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) + private val cryptoImpl = + CryptoImpl(context, keyManager, cipherFactory, headerReader, "androidId") private val metadataReader = MetadataReaderImpl(cryptoImpl) private val notificationManager = mockk() private val dbManager = TestKvDbManager() diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCreationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCreationTest.kt new file mode 100644 index 00000000..307c7acd --- /dev/null +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCreationTest.kt @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: 2020 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.transport.backup + +import android.app.backup.BackupDataInput +import android.app.backup.BackupTransport.FLAG_INCREMENTAL +import android.app.backup.BackupTransport.TRANSPORT_OK +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stevesoltys.seedvault.TestApp +import com.stevesoltys.seedvault.backend.BackendManager +import com.stevesoltys.seedvault.crypto.CipherFactoryImpl +import com.stevesoltys.seedvault.crypto.CryptoImpl +import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES +import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl +import com.stevesoltys.seedvault.header.HeaderReaderImpl +import com.stevesoltys.seedvault.repo.AppBackupManager +import com.stevesoltys.seedvault.repo.BackupReceiver +import com.stevesoltys.seedvault.repo.BlobCache +import com.stevesoltys.seedvault.repo.BlobCreator +import com.stevesoltys.seedvault.repo.SnapshotCreator +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.runBlocking +import org.calyxos.seedvault.core.backends.AppBackupFileType +import org.calyxos.seedvault.core.backends.Backend +import org.calyxos.seedvault.core.toHexString +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import javax.crypto.spec.SecretKeySpec + +/** + * Creates encrypted backups from known data that can be used for further tests. + */ +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [34], // TODO: Drop once robolectric supports 35 + application = TestApp::class +) +internal class BackupCreationTest : BackupTest() { + + private val secretKey = SecretKeySpec( + "This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES" + ) + private val keyManager = KeyManagerTestImpl(secretKey) + private val cipherFactory = CipherFactoryImpl(keyManager) + private val headerReader = HeaderReaderImpl() + private val cryptoImpl = + CryptoImpl(context, keyManager, cipherFactory, headerReader, "0123456789") + + private val blobCache = BlobCache(context) + private val backendManager = mockk() + private val blobCreator = BlobCreator(cryptoImpl, backendManager) + private val backupReceiver = BackupReceiver(blobCache, blobCreator, cryptoImpl) + private val appBackupManager = mockk() + private val packageService = mockk() + private val snapshotCreator = + SnapshotCreator(context, clock, packageService, settingsManager, mockk(relaxed = true)) + private val notificationManager = mockk() + private val db = TestKvDbManager() + + private val kvBackup = KVBackup(backupReceiver, inputFactory, db) + private val fullBackup = + FullBackup(settingsManager, notificationManager, backupReceiver, inputFactory) + + private val backup = BackupCoordinator( + context = context, + backendManager = backendManager, + appBackupManager = appBackupManager, + kv = kvBackup, + full = fullBackup, + clock = clock, + packageService = packageService, + metadataManager = metadataManager, + settingsManager = settingsManager, + nm = notificationManager, + ) + + private val backend = mockk() + private val backupDataInput = mockk() + private val newRepoId = "b575506bb32d4279128cb423d347384f1985822d11254218bcd3ae77d6cccd27" + + init { + every { backendManager.backend } returns backend + every { appBackupManager.snapshotCreator } returns snapshotCreator + every { clock.time() } returns token + every { packageInfo.applicationInfo?.loadLabel(any()) } returns packageName + } + + @Test + fun `KV backup`() = runBlocking { + // return data + every { inputFactory.getBackupDataInput(data) } returns backupDataInput + every { backupDataInput.readNextHeader() } returns true andThen true andThen false + every { backupDataInput.key } returns key andThen key2 + every { backupDataInput.dataSize } returns appData.size andThen appData2.size + + // read in data + val byteSlot1 = slot() + val byteSlot2 = slot() + every { backupDataInput.readEntityData(capture(byteSlot1), 0, appData.size) } answers { + appData.copyInto(byteSlot1.captured) // write the app data into the passed ByteArray + appData.size + } + every { backupDataInput.readEntityData(capture(byteSlot2), 0, appData2.size) } answers { + appData2.copyInto(byteSlot2.captured) // write the app data into the passed ByteArray + appData2.size + } + + every { data.close() } just Runs + + assertEquals( + TRANSPORT_OK, + backup.performIncrementalBackup(packageInfo, data, FLAG_INCREMENTAL), + ) + + val handleSlot = slot() + val outputStream = ByteArrayOutputStream() + coEvery { backend.save(capture(handleSlot)) } returns outputStream + + assertEquals(TRANSPORT_OK, backup.finishBackup()) + assertEquals(newRepoId, handleSlot.captured.repoId) + + println(handleSlot.captured) + println(outputStream.toByteArray().toHexString()) + } + + @Test + fun `full backup`() = runBlocking { + every { inputFactory.getInputStream(data) } returns ByteArrayInputStream(appData2) + assertEquals( + TRANSPORT_OK, + backup.performFullBackup(packageInfo, data, 0), + ) + + every { settingsManager.quota } returns quota + every { data.close() } just Runs + assertEquals(TRANSPORT_OK, backup.sendBackupData(appData2.size)) + + val handleSlot = slot() + val outputStream = ByteArrayOutputStream() + coEvery { backend.save(capture(handleSlot)) } returns outputStream + + assertEquals(TRANSPORT_OK, backup.finishBackup()) + assertEquals(newRepoId, handleSlot.captured.repoId) + + println(handleSlot.captured) + println(outputStream.toByteArray().toHexString()) + } + +} diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt index c42fd9b1..27fadc10 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt @@ -6,6 +6,7 @@ package com.stevesoltys.seedvault.transport.backup import android.os.ParcelFileDescriptor +import com.stevesoltys.seedvault.toByteArrayFromHex import com.stevesoltys.seedvault.transport.TransportTest import io.mockk.mockk import java.io.OutputStream @@ -17,4 +18,41 @@ internal abstract class BackupTest : TransportTest() { protected val outputStream = mockk() protected val encryptedOutputStream = mockk() + protected val appData = ("562AB665C3543120FC794D7CDA3AC18E5959235A4D" + + "3FD9A75BE521E99EF8D79EA4A98652A0F32AFFEC07").toByteArrayFromHex() + protected val appData2 = ("C530E675053D3D253B7F5BE864B44DCB4484C9660C" + + "FBDAEA3ECA56A91E5F6D4DF5B79F9B33AF0F8CA73AF3F208916BA96BB49F1B535B05BCCAC2398251E00B" + + "0EB22214BB6E863E8E6228383E8172526C1E0B5DE353EEB90A31D2CB9DAEFE806A88F0CD7551237B5D1C" + + "14FB6ED5546CB3B52EA74C1BEA068C99D23FFB3345EA77CEBBA28DB5FBD6AD104CAC4F30E4B7F299E2E1" + + "AEB93B7CC158322443B47527289123974BD94BD7FED5E0EB26A59CAFB3544B28431255EBAB074EF7E7BA" + + "37E8B847DFE30B3D3F4334BE248CB384B13C010F439E004E973990CC05062B0B2E090435DA8362B5EF89" + + "E9E8640E03BD5C179F1FCC23D55737E246F8E36F461D816378F898812750109CD9BA2F5F3A11BCAF53D9" + + "81CF3A40194FFD13E4343D7A46BB60C2605469F0BCFD4A7DD9273B4B7CDE97591443FDBBFBFF713B4AB9" + + "C5DC303FDC768AC5C39CBFEA65DB24EE7BF9DE7566D46C3008997EAAE0288CE3BE217AA9081608A0DD06" + + "222E505F241BCBFC0D2FB2BA2490A315566B1848319C13BF07411623DE201DC70FFF7FF2E834F4910508" + + "DC1626C1F5C9661F1890C5FB57414DB95E5D3A6651695546D7E0BB8C67C446914D9CD110DF1323FA41AB" + + "870DEE288E9A8EB79A5902EF19FD89A9E5B2670900227078FF5A18B860CDE01B1F57CA1D910B20836AC1" + + "8DF92CAB93AFEA235AE44B5141C3E49BE964FF5ADBC5C504CA270B2764B2420192D5B0C8356FDB48C01A" + + "E00BC0B77098AAD6CDAFC2259E114A6C1C673572EF1A295CC8D00FF0F62F5797A486553604F9EA243DE5" + + "3CD3CB125A27BFB6C1E07485B0200E792200980C32AE2B1AE10880744396BC8DB153965046EA800F1C57" + + "8E6978CF482E49112D97CD0498772EDD0A22032530BB2EE12CE2D6640C612CEB0512525AA1AAFBADF17F" + + "3328D03AC49279DFB4D88F9AD21CE36DB58A4A210267E0705DB243CA419A05DF3DF2C4C06E12E42B0B98" + + "20CADBB9D469BC266996CD8BB51E30BDDB23980EC1463FD9C5DB9CE31D7E5E4F2C0B8E9B70B02A773297" + + "0C37CE04083488FDCC0663B89F2F1AFBEB73279DEC7DF783A276FBE80019B56B8A1B9DE9B9730E2D3FAA" + + "DB3A2B2D14C930F36FA89945D8D0D07C7C5C049011612085835656C768556453EE2BBB6736FB106F81A2" + + "52EB174688827D03C521C7BE9C31106F3F739142CB0F82F5B0AC8287EE4D4F459BDCD9267CE08375335D" + + "62997EDCD4AAC7C01CA24328FE753E4D05D152D30C6486E9B77F38AAF8C78855ED90643C42F92AC286DD" + + "95477E4DD3288923D380E1823325D9D120D49F932B768FCA8CF5C68200918DE9569C5EA6017A213A886E" + + "01408922943060398FA1599DD22E37D4DCD63E14ECDB821564EC1FB9C30460C917BF9295A42BFF65D6AE" + + "F3420DA4CD1DA371190D171D6DBD59D58F02290537708E7FEBEFBA5A5E4E8544ECCC8302FA28E66E506E" + + "78ACEDA79246824591A513D5361E0506FF5C44435FC6BDA03B0D5010A75833B286C3D2C0FE652325AB51" + + "C7CDF6EA3B5D607BBAD7402936CE453037DECF6EB3AECCF825CC06BB79C7A924CA65C1CCB429DE619914" + + "90CD1DD35F992F88FC5E632DCE02CB669F813F70CA84E03DA66CE72D5C572D3C8C5974DCB43BFCA24D4A" + + "EA985D0765819BBE643CA424DEF68232E53EAF58AE31BA38FCB87E1A20B61E01DE29A89BA76098863FFC" + + "8B85EDCD934C0946E35199CBA77F621CDC46A3DAFE571ECF898E1157B96C2566F355AFD323A585A344FC" + + "0D660A155F2CEEAF7DAAB0EEDBD08ADBC78486E51D05A2C98672CE02A746A9CD79B3F8062B3143355D7A" + + "30B8790FBDD26955009867A24FF16FA310887E71CC9817B2495052CBB5CC19C6ADA9592EBF0477DE696D" + + "858A13A3D23955330BFAFB0915CC").toByteArrayFromHex() + protected val key = "RestoreKey" + protected val key2 = "RestoreKey2" } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt index 209553f7..27880823 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt @@ -13,8 +13,6 @@ import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED import android.app.backup.BackupTransport.TRANSPORT_OK import android.content.pm.PackageInfo -import com.stevesoltys.seedvault.getRandomString -import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE import com.stevesoltys.seedvault.repo.BackupReceiver import io.mockk.CapturingSlot import io.mockk.Runs @@ -46,7 +44,6 @@ internal class KVBackupTest : BackupTest() { ) private val db = mockk() - private val key = getRandomString(MAX_KEY_LENGTH_SIZE) private val dataValue = Random.nextBytes(23) private val dbBytes = Random.nextBytes(42) private val inputStream = ByteArrayInputStream(dbBytes) 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 38dbb75f..330cb162 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 @@ -23,7 +23,7 @@ import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.repo.Loader import com.stevesoltys.seedvault.repo.SnapshotManager import com.stevesoltys.seedvault.toByteArrayFromHex -import com.stevesoltys.seedvault.transport.TransportTest +import com.stevesoltys.seedvault.transport.backup.BackupTest import com.stevesoltys.seedvault.transport.backup.KvDbManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import io.mockk.coEvery @@ -43,7 +43,7 @@ import javax.crypto.spec.SecretKeySpec /** * Tests that we can still restore Version 0 backups with current code. */ -internal class RestoreV0IntegrationTest : TransportTest() { +internal class RestoreV0IntegrationTest : BackupTest() { private val outputFactory = mockk() private val secretKey = SecretKeySpec( @@ -52,7 +52,8 @@ internal class RestoreV0IntegrationTest : TransportTest() { private val keyManager = KeyManagerTestImpl(secretKey) private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) + private val cryptoImpl = + CryptoImpl(context, keyManager, cipherFactory, headerReader, "androidId") private val dbManager = mockk() private val metadataReader = MetadataReaderImpl(cryptoImpl) private val notificationManager = mockk() @@ -91,44 +92,7 @@ internal class RestoreV0IntegrationTest : TransportTest() { } private val fileDescriptor = mockk(relaxed = true) - private val appData = ("562AB665C3543120FC794D7CDA3AC18E5959235A4D" + - "3FD9A75BE521E99EF8D79EA4A98652A0F32AFFEC07").toByteArrayFromHex() - private val appData2 = ("C530E675053D3D253B7F5BE864B44DCB4484C9660C" + - "FBDAEA3ECA56A91E5F6D4DF5B79F9B33AF0F8CA73AF3F208916BA96BB49F1B535B05BCCAC2398251E00B" + - "0EB22214BB6E863E8E6228383E8172526C1E0B5DE353EEB90A31D2CB9DAEFE806A88F0CD7551237B5D1C" + - "14FB6ED5546CB3B52EA74C1BEA068C99D23FFB3345EA77CEBBA28DB5FBD6AD104CAC4F30E4B7F299E2E1" + - "AEB93B7CC158322443B47527289123974BD94BD7FED5E0EB26A59CAFB3544B28431255EBAB074EF7E7BA" + - "37E8B847DFE30B3D3F4334BE248CB384B13C010F439E004E973990CC05062B0B2E090435DA8362B5EF89" + - "E9E8640E03BD5C179F1FCC23D55737E246F8E36F461D816378F898812750109CD9BA2F5F3A11BCAF53D9" + - "81CF3A40194FFD13E4343D7A46BB60C2605469F0BCFD4A7DD9273B4B7CDE97591443FDBBFBFF713B4AB9" + - "C5DC303FDC768AC5C39CBFEA65DB24EE7BF9DE7566D46C3008997EAAE0288CE3BE217AA9081608A0DD06" + - "222E505F241BCBFC0D2FB2BA2490A315566B1848319C13BF07411623DE201DC70FFF7FF2E834F4910508" + - "DC1626C1F5C9661F1890C5FB57414DB95E5D3A6651695546D7E0BB8C67C446914D9CD110DF1323FA41AB" + - "870DEE288E9A8EB79A5902EF19FD89A9E5B2670900227078FF5A18B860CDE01B1F57CA1D910B20836AC1" + - "8DF92CAB93AFEA235AE44B5141C3E49BE964FF5ADBC5C504CA270B2764B2420192D5B0C8356FDB48C01A" + - "E00BC0B77098AAD6CDAFC2259E114A6C1C673572EF1A295CC8D00FF0F62F5797A486553604F9EA243DE5" + - "3CD3CB125A27BFB6C1E07485B0200E792200980C32AE2B1AE10880744396BC8DB153965046EA800F1C57" + - "8E6978CF482E49112D97CD0498772EDD0A22032530BB2EE12CE2D6640C612CEB0512525AA1AAFBADF17F" + - "3328D03AC49279DFB4D88F9AD21CE36DB58A4A210267E0705DB243CA419A05DF3DF2C4C06E12E42B0B98" + - "20CADBB9D469BC266996CD8BB51E30BDDB23980EC1463FD9C5DB9CE31D7E5E4F2C0B8E9B70B02A773297" + - "0C37CE04083488FDCC0663B89F2F1AFBEB73279DEC7DF783A276FBE80019B56B8A1B9DE9B9730E2D3FAA" + - "DB3A2B2D14C930F36FA89945D8D0D07C7C5C049011612085835656C768556453EE2BBB6736FB106F81A2" + - "52EB174688827D03C521C7BE9C31106F3F739142CB0F82F5B0AC8287EE4D4F459BDCD9267CE08375335D" + - "62997EDCD4AAC7C01CA24328FE753E4D05D152D30C6486E9B77F38AAF8C78855ED90643C42F92AC286DD" + - "95477E4DD3288923D380E1823325D9D120D49F932B768FCA8CF5C68200918DE9569C5EA6017A213A886E" + - "01408922943060398FA1599DD22E37D4DCD63E14ECDB821564EC1FB9C30460C917BF9295A42BFF65D6AE" + - "F3420DA4CD1DA371190D171D6DBD59D58F02290537708E7FEBEFBA5A5E4E8544ECCC8302FA28E66E506E" + - "78ACEDA79246824591A513D5361E0506FF5C44435FC6BDA03B0D5010A75833B286C3D2C0FE652325AB51" + - "C7CDF6EA3B5D607BBAD7402936CE453037DECF6EB3AECCF825CC06BB79C7A924CA65C1CCB429DE619914" + - "90CD1DD35F992F88FC5E632DCE02CB669F813F70CA84E03DA66CE72D5C572D3C8C5974DCB43BFCA24D4A" + - "EA985D0765819BBE643CA424DEF68232E53EAF58AE31BA38FCB87E1A20B61E01DE29A89BA76098863FFC" + - "8B85EDCD934C0946E35199CBA77F621CDC46A3DAFE571ECF898E1157B96C2566F355AFD323A585A344FC" + - "0D660A155F2CEEAF7DAAB0EEDBD08ADBC78486E51D05A2C98672CE02A746A9CD79B3F8062B3143355D7A" + - "30B8790FBDD26955009867A24FF16FA310887E71CC9817B2495052CBB5CC19C6ADA9592EBF0477DE696D" + - "858A13A3D23955330BFAFB0915CC").toByteArrayFromHex() - private val key = "RestoreKey" private val key64 = key.encodeBase64() - private val key2 = "RestoreKey2" private val key264 = key2.encodeBase64() init { diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV1IntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV1IntegrationTest.kt new file mode 100644 index 00000000..0ba3719a --- /dev/null +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV1IntegrationTest.kt @@ -0,0 +1,258 @@ +/* + * SPDX-FileCopyrightText: 2020 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.transport.restore + +import android.app.backup.BackupDataOutput +import android.app.backup.BackupTransport.NO_MORE_DATA +import android.app.backup.BackupTransport.TRANSPORT_OK +import android.app.backup.RestoreDescription +import android.app.backup.RestoreDescription.TYPE_FULL_STREAM +import android.os.ParcelFileDescriptor +import com.stevesoltys.seedvault.backend.BackendManager +import com.stevesoltys.seedvault.crypto.CipherFactoryImpl +import com.stevesoltys.seedvault.crypto.CryptoImpl +import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES +import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl +import com.stevesoltys.seedvault.header.HeaderReaderImpl +import com.stevesoltys.seedvault.metadata.BackupType +import com.stevesoltys.seedvault.metadata.MetadataReaderImpl +import com.stevesoltys.seedvault.repo.Loader +import com.stevesoltys.seedvault.repo.SnapshotManager +import com.stevesoltys.seedvault.toByteArrayFromHex +import com.stevesoltys.seedvault.transport.backup.BackupTest +import com.stevesoltys.seedvault.transport.backup.TestKvDbManager +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyOrder +import kotlinx.coroutines.runBlocking +import org.calyxos.seedvault.core.backends.Backend +import org.calyxos.seedvault.core.backends.LegacyAppBackupFile +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import javax.crypto.spec.SecretKeySpec + +/** + * Tests that we can still restore Version 1 backups with current code. + */ +internal class RestoreV1IntegrationTest : BackupTest() { + + private val outputFactory = mockk() + private val secretKey = SecretKeySpec( + "This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES" + ) + private val keyManager = KeyManagerTestImpl(secretKey) + private val cipherFactory = CipherFactoryImpl(keyManager) + private val headerReader = HeaderReaderImpl() + private val cryptoImpl = + CryptoImpl(context, keyManager, cipherFactory, headerReader, "androidId") + private val dbManager = TestKvDbManager() + private val metadataReader = MetadataReaderImpl(cryptoImpl) + private val notificationManager = mockk() + private val backendManager: BackendManager = mockk() + private val loader = mockk() + private val snapshotManager = mockk() + + private val backend = mockk() + private val kvRestore = KVRestore( + backendManager = backendManager, + loader = loader, + legacyPlugin = mockk(), + outputFactory = outputFactory, + headerReader = headerReader, + crypto = cryptoImpl, + dbManager = dbManager, + ) + private val fullRestore = + FullRestore(backendManager, loader, mockk(), outputFactory, headerReader, cryptoImpl) + private val oldSalt = "31v9rtLNFE485vxQ6VtzSGo7N36ZBc97" + private val restore = RestoreCoordinator( + context = context, + crypto = cryptoImpl, + settingsManager = settingsManager, + metadataManager = metadataManager, + notificationManager = notificationManager, + backendManager = backendManager, + snapshotManager = snapshotManager, + kv = kvRestore, + full = fullRestore, + metadataReader = metadataReader, + ).apply { + val backup = RestorableBackup(metadata.copy(version = 0x01, salt = oldSalt)) + beforeStartRestore(backup) + } + + private val fileDescriptor = mockk(relaxed = true) + private val oldHandle = LegacyAppBackupFile.Blob( + token = token, + name = "i0juqM8CuZzN9EHKASsEEDlVTfE-SC-uRuyrQWDTzEs", + ) + + init { + every { backendManager.backend } returns backend + } + + @Test + fun `test key-value backup and restore with 2 records`() = runBlocking { + val encryptedDb = ("0128eb2eb87c33e9377de0d986afeaf72263f8a515fc5b626b49cd40100ea8dc" + + "c5e421027c7280e9f3e0df5982736dc8f833256971826e93f438839840832e38" + + "f942643730099e33191d3366031cb54144d33e3ab13cd7f1d3d62e6cbe421982" + + "384f5570bae8d87e19c7d20f91a0642be531eaea46466c0ad1c9e382fc11839e" + + "3d24521ba47e00d78db5699eb27f912bad9801db4f52ce8ddc9984e22f2aeb5a" + + "07baae80951d6b4448a5821c2aa18ee89b7f99b9252d7f510b5031a3c1fc1dda" + + "6e451eb81f52a0e58bbba14c495629ad4c92b0a2e2262e64d0df7e55207fd5b6" + + "3041d35a67bf44dc006706bb1d261fb9a20461fd7517859e08fbecab75c81755" + + "f986356f98ccdb8d2326aae9b5576657fb0513074b4a37b0555483ebf9ee04bf" + + "7a53a35e3a8bf9de43dd19c146785b881fb3bd3dbc467625a0c47b96fb1f7ff7" + + "9584268057ba3d01c3af58bdcc5f9fa1a0a0cc34c241b271b5de48efd70dbd27" + + "faa8d04363a700ec3cd37adb95a1e361f965b7a757b51e242a4eb3bb9f3166b1" + + "50dc5e55f9eb70460f4ed2d4fb5c8975d10b5157822f05805070547bbb49754b" + + "89162b20a27a24dbfdb7b58c08bc9ee58c98324d0eccf49c857a0cd81f753eaa" + + "072b9a798d55a38bde580d5dfd1b7554745b58f2f13187046e5ce8e36cd3be96" + + "6ba231b95704d4b468803382f93267fc301c899fde1768e9ec8387bee7acd847" + + "9a7c760133f3bb67064de6c0b35c9614e4297dbc6f08efecd33e75edf3a043da" + + "7a4d0a8992a65d2641ba3e251de497e2b7da881cb84780d7db7210d2d52e13a0" + + "28779a3b87609c7a479db5e52e3210e256b7b584fe735e2e779c22a0885e320b" + + "47ec6c9d86b7d564783326f603beb5e098d57658a2faca57f92118a7f701777e" + + "6598eeddb16afae60a9ff2241c622df391483d66100a4f16ee17289a869e3453" + + "6ea1fff7a1210da46a3ee23576485c23eb0c97d84f1924ee0526382420f348d2" + + "5701efa78b481fb43c6c4b25eb0675e10908240f191e5afb3027e8a29a160c7e" + + "83a3cbe0b3083829ce67bc7c110e95dc3a17a86c4bfbcb3e1b9f76d4be944b6a" + + "15e8ad63948e7e034f38bce2d15a1e699fe008bb3d35cb9743d3a795bf7b8ebe" + + "ea97a3b9b546e501b1a2be502fce6067e116c627054187460ed2f3f6378fbe85" + + "985809a12ce9ffd2a5033d41a666516510a4d40da09a56bc881fd07cf839e427" + + "9bb4ebd96bd84cdd7ddc9cfe2e2b9d00d3c8001f40c1e771e0edd37362a4b21e" + + "60c7d5799971c7a89e61d514ab2b481bd1123d9f5b173cc5ed98726ca3af598b" + + "23e152e7e66352bc01917306fa1f786a598fa53e49ba38b5f506808d6f6ca3f1" + + "8a754fcf7e1b804b8c1dce60e8bbc77ecea8f78ce66faa27d5a13eabcb898d7b" + + "bc16c7629db5953b2884a12ce66198d93b0bbed07c8fdac0633096d29eb97f19" + + "e817e5bca5651d4324e6c2b284468e286a2516cf792a196d19a2b6a09675d6b3" + + "c9aa994bc237341e583bdc5b1ca5266567aa8af924f5571d9ec131be56912146" + + "8e9954c8f747d971ea179963d4beb817c9796f85c79fbabea134e4b7df5e9a1a" + + "eafa7d296d4e834177bb685d41afbd5d5618eeb079cba2f4cc1886d79c6faef6" + + "4189817a429d05dcc243558a699f426283f09db2906d206c10d4ea3565dd42d1" + + "f1a914c65646bd5a039e082281ae462c196e0aff9e7702de4128c367db5a4239" + + "353fe6214124dff50e356ffc605e8e85ab35ac63217ea48e8ebd572a11882e19" + + "206e474131dd546dfcfea726109f86eeb5edd0f90e20749ae2019f8868b1b2f9" + + "242921dd5d63c5621ab91ef91dcffa048d1825593dab7c9f84c3d0590e81491e" + + "623325883e637ee82401f402aa5ab251bf12393a9a0291ec4b5ffe299337938c" + + "39335f7ac27da4e62365b1a8baf144fbfbd2a0f77c71aade8897b96557065f0b" + + "e7401b02cca855f75948e907c6d91adaacdc9abbb3f58a0dc46d542514d58928" + + "ad5bdb6c6bfb5ebadb4898841d816968e77fefcd85d657dbbd7808e9423758b8" + + "d98f2874cd9feef2fcddc6ac8879115223c76d1b9b6974bb552caf2d9b165484" + + "6ea36f92aed98b9a82700bf55dd6cd4f4b86527966c5f86fcff621706f77f0a7" + + "c737ae1f0a8d854815dcc8aee9f9c15c4bc95d4e5be6fa11627ad1112efa9290" + + "70f7aeaa7327091526165082a83af27ebb717b8dfc620a4c2a31a1ebf930320b" + + "3792d10f1420580ec4eed48fcf60ca66f6d6669f931a5d6947ffe6e1b2af4f07" + + "4265a78640ed30ad7f939891ae03877cb8cbffb33e41d92514ec003a9839ecbb" + + "fbdd53ac012389d5ceab86414cd60b965a1e0ab520b5350b9375d0cf7226bd2d" + + "5b141b1d71c0456aa090e34fd4d067").toByteArrayFromHex() + + // start restore + assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo))) + + // get restore description + val restoreDescription = restore.nextRestorePackage() ?: fail() + assertEquals(packageInfo.packageName, restoreDescription.packageName) + assertEquals(RestoreDescription.TYPE_KEY_VALUE, restoreDescription.dataType) + + // restore finds the backed up key and writes the decrypted value + val backupDataOutput = mockk() + coEvery { backend.load(oldHandle) } returns ByteArrayInputStream(encryptedDb) + every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput + every { backupDataOutput.writeEntityHeader(key, appData.size) } returns 1137 + every { backupDataOutput.writeEntityData(appData, appData.size) } returns appData.size + every { backupDataOutput.writeEntityHeader(key2, appData2.size) } returns 1137 + every { backupDataOutput.writeEntityData(appData2, appData2.size) } returns appData2.size + assertEquals(TRANSPORT_OK, restore.getRestoreData(fileDescriptor)) + + verifyOrder { + backupDataOutput.writeEntityData(appData, appData.size) + backupDataOutput.writeEntityData(appData2, appData2.size) + } + } + + @Test + fun `test full backup and restore with two chunks`() = runBlocking { + val encryptedData = ("0128b5b75f24f2d57f8ab858b5eefe1836ed185999fa26adc1e8b5f8db238dbb" + + "b6b3aa4a2951c69cfa9bae6b58e4e379e727edf7c3128d3ad969ab75c2e4acef" + + "23beb22f29d1e064dab3d2d16430e0edf2d3ad1279befe4e75ff50209fc44e04" + + "b2bd1e14420bcfe9b1407724af98cf39eb4eee257147e346935f762d1f718cc9" + + "06ad2d86d413b061e0df719d62e6e95433ce0dd75e152a04f68afc0c479df785" + + "6591922ecd7c907d9b33db0352624a67c3d0c63e0fe2b7f57287d45013eb59ef" + + "bbb7bccc55577e5c7477b27f282eb73cb367fd5a9f3ded050e0d5da7648bf299" + + "31d396a6b08ad5b55cc9dea3d570726e17261501cda0f69b81fa7e4bc87de1f3" + + "4a353fa57e3ef1c24e7a0109bc3824f409df199cbb1f5bceccac488ba5e10857" + + "9e540d5fb98c482996b3a5db0381a19e295ad36277e37f4a69882313c22d5c4d" + + "6cbecc63645b039e07f591745345253536022d8c79de54e90478635dcb5182bb" + + "bf7bf17621c3aa979dcbb7231cf30a4fe942faf8816ba5701b132fb72477d5b6" + + "55a03c07d1978e3892da09c1e28d5a06a360cecb319450a5d4170b08d45504a8" + + "2726fc500315384d310eae2f17f5701b2a8929d66a20d0807ab67f61d76087d0" + + "2c5f7e06b8ce413df029eb376fcc72714d5c0588a1a91c3fba8e6b2464fe97c5" + + "de74d56b2bd884eb04d0a3897e8546be9caff567b9d7bcf6837f40240e0de3c5" + + "9a2ecdda6a881d1116a619221e33fc7b485880cd188bbff40f3a81687aa8fbd4" + + "657e0cd578768394533dd2ec9f010b3d74cf52734a55acb2e28d041d1aed60c7" + + "dc3e336299c9ac1f3dfcae10d9fd9ba3bf27ac17268b2c28a15893c3618a1e62" + + "dbbe4bb1ab2c6240ab16cd24b07e0cb0b33b99afc9278e46aa0a9f987ce9f67d" + + "92477ca931708001e43700f8d3391985fc62d9e0e744c58c2f4831be1e2fac56" + + "3848b67b427f8b589e390a4d53317c888de4e85226e9c36b220baf37793234a4" + + "35643cf95b3f9f08869afa0ddbc5fc7d5b63d657fe8acddde5fdbcb7bb9a900d" + + "486574ddaa446483f815685a83fe438ccecec67854a19194113a3ce27a7cffa5" + + "8831191ac1ff80a9203b9c2490631aa1e98ae30833be1099b79f332a03c3a47f" + + "b845674b987b3874ec3ff3ecdc98c545a700ac928d409f62a96991a43f3b02ae" + + "41e9062a7893417634e613d0af027c1fdc88085c5f00e93660b05012963a481d" + + "9ccc58df8a340789162092e16306a657a50d5ebe3037f32d9cc6af7580c35c3c" + + "1a7d9c44de9f78fa00f0cc725a5fff120f15b910bd26fec40f7dd046b0312ea3" + + "d174b8622f3940053993d32778804af8520c57c57d38da0a6b00aac8b860f7cc" + + "7e1ed0a5e5fdd7683fc56ee170e493f520bee887665068a285e45dbc803fa560" + + "00d1a58f373240582cbb96a7cb97a99663c55c2f4f040e799fef945d62f30cc8" + + "86ac67f350c013089eec5c619e9056704138139322de7d82cf5053c803254e36" + + "5e382f7a9ff0be9f52c554eab9f515069a6d71d6e96e9e55721a97fdfb0ed0b4" + + "5cb9af1625cd6aa29b2e0cace36990c9973dec7c6b940e4905674ff4e70170fe" + + "2ed56659c9b3923bac130c9b10ac95b4c6986f5bc68b7e5439d9436d3c95aa80" + + "0397f419b215dd8c591de5e4f4237b440719cb8803c1b9f4f064a5d88e88145a" + + "f592aec86080e60699dfa6284ca5aee9b799d14422997a499a4472f5ef1ee4f6" + + "8d41a00262d212bacc79cf9c82dfa24c4da3a1eade3be3ca3cdcf2cc00b8a2cb" + + "5c53aad407329275a9fc9bfbff9ceb24a1cfc2cedb330682ba8b5b6f03956959" + + "a102411fee64cdc2b2dca3bb8093815e754b2da823d9a08d15c83da7bfcfa378" + + "9ce4b22d54e290de3c22f3294323cadd736f79786e5752dd269cdd97827471dd" + + "af6c468d3d835443107b1f0edf775c4ad38d2a8a49d048fddd383c5ef7371049" + + "5496c78a72e04976eb702eac6065b2e912fd").toByteArrayFromHex() + + // set backup type to full + val packageMetadata = metadata.packageMetadataMap[packageName] ?: fail() + metadata.packageMetadataMap[packageName] = packageMetadata.copy( + backupType = BackupType.FULL, + ) + + // start restore + assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo))) + + val restoreDescription = restore.nextRestorePackage() ?: fail() + assertEquals(packageInfo.packageName, restoreDescription.packageName) + assertEquals(TYPE_FULL_STREAM, restoreDescription.dataType) + + // reverse the backup streams into restore input + val inputStream = ByteArrayInputStream(encryptedData) + val outputStream = ByteArrayOutputStream() + every { outputFactory.getOutputStream(fileDescriptor) } returns outputStream + coEvery { backend.load(oldHandle) } returns inputStream + + // restore data + assertEquals(appData2.size, restore.getNextFullRestoreDataChunk(fileDescriptor)) + assertEquals(NO_MORE_DATA, restore.getNextFullRestoreDataChunk(fileDescriptor)) + restore.finishRestore() + + // assert that restored data matches original app data + assertArrayEquals(appData2, outputStream.toByteArray()) + } + +}