Add salt and backup type to metadata

This commit is contained in:
Torsten Grote 2021-09-16 16:12:54 +02:00 committed by Chirayu Desai
parent 39cb0c6443
commit 793663acb5
15 changed files with 123 additions and 41 deletions

View file

@ -13,6 +13,7 @@ typealias PackageMetadataMap = HashMap<String, PackageMetadata>
data class BackupMetadata( data class BackupMetadata(
internal val version: Byte = VERSION, internal val version: Byte = VERSION,
internal val token: Long, internal val token: Long,
internal val salt: String,
internal var time: Long = 0L, internal var time: Long = 0L,
internal val androidVersion: Int = Build.VERSION.SDK_INT, internal val androidVersion: Int = Build.VERSION.SDK_INT,
internal val androidIncremental: String = Build.VERSION.INCREMENTAL, internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
@ -23,6 +24,7 @@ data class BackupMetadata(
internal const val JSON_METADATA = "@meta@" internal const val JSON_METADATA = "@meta@"
internal const val JSON_METADATA_VERSION = "version" internal const val JSON_METADATA_VERSION = "version"
internal const val JSON_METADATA_TOKEN = "token" internal const val JSON_METADATA_TOKEN = "token"
internal const val JSON_METADATA_SALT = "salt"
internal const val JSON_METADATA_TIME = "time" internal const val JSON_METADATA_TIME = "time"
internal const val JSON_METADATA_SDK_INT = "sdk_int" internal const val JSON_METADATA_SDK_INT = "sdk_int"
internal const val JSON_METADATA_INCREMENTAL = "incremental" internal const val JSON_METADATA_INCREMENTAL = "incremental"
@ -69,6 +71,7 @@ data class PackageMetadata(
*/ */
internal var time: Long = 0L, internal var time: Long = 0L,
internal var state: PackageState = UNKNOWN_ERROR, internal var state: PackageState = UNKNOWN_ERROR,
internal var backupType: BackupType? = null,
internal val system: Boolean = false, internal val system: Boolean = false,
internal val version: Long? = null, internal val version: Long? = null,
internal val installer: String? = null, internal val installer: String? = null,
@ -87,7 +90,10 @@ data class ApkSplit(
// There's also a revisionCode, but it doesn't seem to be used just yet // There's also a revisionCode, but it doesn't seem to be used just yet
) )
enum class BackupType { KV, FULL }
internal const val JSON_PACKAGE_TIME = "time" internal const val JSON_PACKAGE_TIME = "time"
internal const val JSON_PACKAGE_BACKUP_TYPE = "backupType"
internal const val JSON_PACKAGE_STATE = "state" internal const val JSON_PACKAGE_STATE = "state"
internal const val JSON_PACKAGE_SYSTEM = "system" internal const val JSON_PACKAGE_SYSTEM = "system"
internal const val JSON_PACKAGE_VERSION = "version" internal const val JSON_PACKAGE_VERSION = "version"

View file

@ -10,6 +10,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
@ -24,16 +26,18 @@ private val TAG = MetadataManager::class.java.simpleName
@VisibleForTesting @VisibleForTesting
internal const val METADATA_CACHE_FILE = "metadata.cache" internal const val METADATA_CACHE_FILE = "metadata.cache"
internal const val METADATA_SALT_SIZE = 32
@WorkerThread @WorkerThread
class MetadataManager( internal class MetadataManager(
private val context: Context, private val context: Context,
private val clock: Clock, private val clock: Clock,
private val crypto: Crypto,
private val metadataWriter: MetadataWriter, private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader private val metadataReader: MetadataReader
) { ) {
private val uninitializedMetadata = BackupMetadata(token = 0L) private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
private var metadata: BackupMetadata = uninitializedMetadata private var metadata: BackupMetadata = uninitializedMetadata
get() { get() {
if (field == uninitializedMetadata) { if (field == uninitializedMetadata) {
@ -57,8 +61,9 @@ class MetadataManager(
@Synchronized @Synchronized
@Throws(IOException::class) @Throws(IOException::class)
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) { fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
modifyMetadata(metadataOutputStream) { modifyMetadata(metadataOutputStream) {
metadata = BackupMetadata(token = token) metadata = BackupMetadata(token = token, salt = salt)
} }
} }
@ -121,7 +126,11 @@ class MetadataManager(
*/ */
@Synchronized @Synchronized
@Throws(IOException::class) @Throws(IOException::class)
fun onPackageBackedUp(packageInfo: PackageInfo, metadataOutputStream: OutputStream) { fun onPackageBackedUp(
packageInfo: PackageInfo,
type: BackupType,
metadataOutputStream: OutputStream
) {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
modifyMetadata(metadataOutputStream) { modifyMetadata(metadataOutputStream) {
val now = clock.time() val now = clock.time()
@ -129,10 +138,12 @@ class MetadataManager(
if (metadata.packageMetadataMap.containsKey(packageName)) { if (metadata.packageMetadataMap.containsKey(packageName)) {
metadata.packageMetadataMap[packageName]!!.time = now metadata.packageMetadataMap[packageName]!!.time = now
metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA
metadata.packageMetadataMap[packageName]!!.backupType = type
} else { } else {
metadata.packageMetadataMap[packageName] = PackageMetadata( metadata.packageMetadataMap[packageName] = PackageMetadata(
time = now, time = now,
state = APK_AND_DATA, state = APK_AND_DATA,
backupType = type,
system = packageInfo.isSystemApp() system = packageInfo.isSystemApp()
) )
} }
@ -150,7 +161,8 @@ class MetadataManager(
internal fun onPackageBackupError( internal fun onPackageBackupError(
packageInfo: PackageInfo, packageInfo: PackageInfo,
packageState: PackageState, packageState: PackageState,
metadataOutputStream: OutputStream metadataOutputStream: OutputStream,
backupType: BackupType? = null
) { ) {
check(packageState != APK_AND_DATA) { "Backup Error with non-error package state." } check(packageState != APK_AND_DATA) { "Backup Error with non-error package state." }
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
@ -161,6 +173,7 @@ class MetadataManager(
metadata.packageMetadataMap[packageName] = PackageMetadata( metadata.packageMetadataMap[packageName] = PackageMetadata(
time = 0L, time = 0L,
state = packageState, state = packageState,
backupType = backupType,
system = packageInfo.isSystemApp() system = packageInfo.isSystemApp()
) )
} }

View file

@ -4,7 +4,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val metadataModule = module { val metadataModule = module {
single { MetadataManager(androidContext(), get(), get(), get()) } single { MetadataManager(androidContext(), get(), get(), get(), get()) }
single<MetadataWriter> { MetadataWriterImpl(get()) } single<MetadataWriter> { MetadataWriterImpl(get()) }
single<MetadataReader> { MetadataReaderImpl(get()) } single<MetadataReader> { MetadataReaderImpl(get()) }
} }

View file

@ -112,6 +112,13 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
WAS_STOPPED.name -> WAS_STOPPED WAS_STOPPED.name -> WAS_STOPPED
else -> UNKNOWN_ERROR else -> UNKNOWN_ERROR
} }
val pBackupType = when (p.optString(JSON_PACKAGE_BACKUP_TYPE)) {
BackupType.KV.name -> BackupType.KV
BackupType.FULL.name -> BackupType.FULL
// we can't fail when format version is 0,
// because when only backing up the APK for example, there's no type
else -> null
}
val pSystem = p.optBoolean(JSON_PACKAGE_SYSTEM, false) val pSystem = p.optBoolean(JSON_PACKAGE_SYSTEM, false)
val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L) val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L)
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER) val pInstaller = p.optString(JSON_PACKAGE_INSTALLER)
@ -127,6 +134,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
packageMetadataMap[packageName] = PackageMetadata( packageMetadataMap[packageName] = PackageMetadata(
time = p.getLong(JSON_PACKAGE_TIME), time = p.getLong(JSON_PACKAGE_TIME),
state = pState, state = pState,
backupType = pBackupType,
system = pSystem, system = pSystem,
version = if (pVersion == 0L) null else pVersion, version = if (pVersion == 0L) null else pVersion,
installer = if (pInstaller == "") null else pInstaller, installer = if (pInstaller == "") null else pInstaller,
@ -138,6 +146,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
return BackupMetadata( return BackupMetadata(
version = version, version = version,
token = token, token = token,
salt = if (version == 0.toByte()) "" else meta.getString(JSON_METADATA_SALT),
time = meta.getLong(JSON_METADATA_TIME), time = meta.getLong(JSON_METADATA_TIME),
androidVersion = meta.getInt(JSON_METADATA_SDK_INT), androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL), androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),

View file

@ -30,6 +30,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
put(JSON_METADATA, JSONObject().apply { put(JSON_METADATA, JSONObject().apply {
put(JSON_METADATA_VERSION, metadata.version.toInt()) put(JSON_METADATA_VERSION, metadata.version.toInt())
put(JSON_METADATA_TOKEN, metadata.token) put(JSON_METADATA_TOKEN, metadata.token)
put(JSON_METADATA_SALT, metadata.salt)
put(JSON_METADATA_TIME, metadata.time) put(JSON_METADATA_TIME, metadata.time)
put(JSON_METADATA_SDK_INT, metadata.androidVersion) put(JSON_METADATA_SDK_INT, metadata.androidVersion)
put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental) put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental)
@ -42,6 +43,11 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
if (packageMetadata.state != APK_AND_DATA) { if (packageMetadata.state != APK_AND_DATA) {
put(JSON_PACKAGE_STATE, packageMetadata.state.name) put(JSON_PACKAGE_STATE, packageMetadata.state.name)
} }
// We can't require a backup type in metadata at this point,
// only when version > 0 and we have actual restore data
if (packageMetadata.backupType != null) {
put(JSON_PACKAGE_BACKUP_TYPE, packageMetadata.backupType!!.name)
}
if (packageMetadata.system) { if (packageMetadata.system) {
put(JSON_PACKAGE_SYSTEM, packageMetadata.system) put(JSON_PACKAGE_SYSTEM, packageMetadata.system)
} }

View file

@ -25,7 +25,7 @@ import java.security.MessageDigest
private val TAG = ApkBackup::class.java.simpleName private val TAG = ApkBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
class ApkBackup( internal class ApkBackup(
private val pm: PackageManager, private val pm: PackageManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager private val metadataManager: MetadataManager

View file

@ -21,6 +21,7 @@ import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
@ -334,7 +335,7 @@ internal class BackupCoordinator(
TAG, "Cancel full backup of ${packageInfo.packageName}" + TAG, "Cancel full backup of ${packageInfo.packageName}" +
" because of ${state.cancelReason}" " because of ${state.cancelReason}"
) )
onPackageBackupError(packageInfo) onPackageBackupError(packageInfo, BackupType.FULL)
full.cancelFullBackup() full.cancelFullBackup()
} }
@ -381,14 +382,16 @@ internal class BackupCoordinator(
check(!full.hasState()) { check(!full.hasState()) {
"K/V backup has state, but full backup has dangling state as well" "K/V backup has state, but full backup has dangling state as well"
} }
onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state // getCurrentPackage() not-null because we have state
onPackageBackedUp(kv.getCurrentPackage()!!, BackupType.KV)
kv.finishBackup() kv.finishBackup()
} }
full.hasState() -> { full.hasState() -> {
check(!kv.hasState()) { check(!kv.hasState()) {
"Full backup has state, but K/V backup has dangling state as well" "Full backup has state, but K/V backup has dangling state as well"
} }
onPackageBackedUp(full.getCurrentPackage()!!) // not-null because we have state // getCurrentPackage() not-null because we have state
onPackageBackedUp(full.getCurrentPackage()!!, BackupType.FULL)
full.finishBackup() full.finishBackup()
} }
state.expectFinish -> { state.expectFinish -> {
@ -456,10 +459,10 @@ internal class BackupCoordinator(
} }
} }
private suspend fun onPackageBackedUp(packageInfo: PackageInfo) { private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) {
try { try {
plugin.getMetadataOutputStream().use { plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackedUp(packageInfo, it) metadataManager.onPackageBackedUp(packageInfo, type, it)
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error while writing metadata for ${packageInfo.packageName}", e) Log.e(TAG, "Error while writing metadata for ${packageInfo.packageName}", e)
@ -468,13 +471,13 @@ internal class BackupCoordinator(
} }
} }
private suspend fun onPackageBackupError(packageInfo: PackageInfo) { private suspend fun onPackageBackupError(packageInfo: PackageInfo, type: BackupType) {
// don't bother with system apps that have no data // don't bother with system apps that have no data
if (state.cancelReason == NO_DATA && packageInfo.isSystemApp()) return if (state.cancelReason == NO_DATA && packageInfo.isSystemApp()) return
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
try { try {
plugin.getMetadataOutputStream().use { plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackupError(packageInfo, state.cancelReason, it) metadataManager.onPackageBackupError(packageInfo, state.cancelReason, it, type)
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error while writing metadata for $packageName", e) Log.e(TAG, "Error while writing metadata for $packageName", e)

View file

@ -9,6 +9,8 @@ import android.content.pm.PackageInfo
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.TestApp import com.stevesoltys.seedvault.TestApp
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
@ -46,10 +48,11 @@ class MetadataManagerTest {
private val context: Context = mockk() private val context: Context = mockk()
private val clock: Clock = mockk() private val clock: Clock = mockk()
private val crypto: Crypto = mockk()
private val metadataWriter: MetadataWriter = mockk() private val metadataWriter: MetadataWriter = mockk()
private val metadataReader: MetadataReader = mockk() private val metadataReader: MetadataReader = mockk()
private val manager = MetadataManager(context, clock, metadataWriter, metadataReader) private val manager = MetadataManager(context, clock, crypto, metadataWriter, metadataReader)
private val time = 42L private val time = 42L
private val token = Random.nextLong() private val token = Random.nextLong()
@ -58,7 +61,9 @@ class MetadataManagerTest {
packageName = this@MetadataManagerTest.packageName packageName = this@MetadataManagerTest.packageName
applicationInfo = ApplicationInfo().apply { flags = FLAG_ALLOW_BACKUP } applicationInfo = ApplicationInfo().apply { flags = FLAG_ALLOW_BACKUP }
} }
private val initialMetadata = BackupMetadata(token = token) private val saltBytes = Random.nextBytes(METADATA_SALT_SIZE)
private val salt = saltBytes.encodeBase64()
private val initialMetadata = BackupMetadata(token = token, salt = salt)
private val storageOutputStream = ByteArrayOutputStream() private val storageOutputStream = ByteArrayOutputStream()
private val cacheOutputStream: FileOutputStream = mockk() private val cacheOutputStream: FileOutputStream = mockk()
private val cacheInputStream: FileInputStream = mockk() private val cacheInputStream: FileInputStream = mockk()
@ -72,6 +77,7 @@ class MetadataManagerTest {
@Test @Test
fun `test onDeviceInitialization()`() { fun `test onDeviceInitialization()`() {
every { clock.time() } returns time every { clock.time() } returns time
every { crypto.getRandomBytes(METADATA_SALT_SIZE) } returns saltBytes
expectReadFromCache() expectReadFromCache()
expectModifyMetadata(initialMetadata) expectModifyMetadata(initialMetadata)
@ -233,10 +239,10 @@ class MetadataManagerTest {
every { clock.time() } returns time every { clock.time() } returns time
expectModifyMetadata(initialMetadata) expectModifyMetadata(initialMetadata)
manager.onPackageBackedUp(packageInfo, storageOutputStream) manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
assertEquals( assertEquals(
packageMetadata.copy(state = APK_AND_DATA, system = true), packageMetadata.copy(state = APK_AND_DATA, backupType = BackupType.FULL, system = true),
manager.getPackageMetadata(packageName) manager.getPackageMetadata(packageName)
) )
assertEquals(time, manager.getLastBackupTime()) assertEquals(time, manager.getLastBackupTime())
@ -254,14 +260,15 @@ class MetadataManagerTest {
time = updateTime, time = updateTime,
packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced
) )
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(updateTime, APK_AND_DATA) updatedMetadata.packageMetadataMap[packageName] =
PackageMetadata(updateTime, APK_AND_DATA, BackupType.KV)
expectReadFromCache() expectReadFromCache()
every { clock.time() } returns updateTime every { clock.time() } returns updateTime
every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException() every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException()
try { try {
manager.onPackageBackedUp(packageInfo, storageOutputStream) manager.onPackageBackedUp(packageInfo, BackupType.KV, storageOutputStream)
fail() fail()
} catch (e: IOException) { } catch (e: IOException) {
// expected // expected
@ -294,7 +301,7 @@ class MetadataManagerTest {
every { clock.time() } returns time every { clock.time() } returns time
expectModifyMetadata(updatedMetadata) expectModifyMetadata(updatedMetadata)
manager.onPackageBackedUp(packageInfo, storageOutputStream) manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
assertEquals(time, manager.getLastBackupTime()) assertEquals(time, manager.getLastBackupTime())
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName)) assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))

View file

@ -4,6 +4,7 @@ import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
@ -33,8 +34,8 @@ internal class MetadataReadWriteTest {
private val reader = MetadataReaderImpl(cryptoImpl) private val reader = MetadataReaderImpl(cryptoImpl)
private val packages = HashMap<String, PackageMetadata>().apply { private val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(Random.nextLong(), APK_AND_DATA)) put(getRandomString(), PackageMetadata(Random.nextLong(), APK_AND_DATA, BackupType.FULL))
put(getRandomString(), PackageMetadata(Random.nextLong(), WAS_STOPPED)) put(getRandomString(), PackageMetadata(Random.nextLong(), WAS_STOPPED, BackupType.KV))
} }
@Test @Test
@ -55,6 +56,7 @@ internal class MetadataReadWriteTest {
return BackupMetadata( return BackupMetadata(
version = VERSION, version = VERSION,
token = Random.nextLong(), token = Random.nextLong(),
salt = getRandomBase64(32),
time = Random.nextLong(), time = Random.nextLong(),
androidVersion = Random.nextInt(), androidVersion = Random.nextInt(),
androidIncremental = getRandomString(), androidIncremental = getRandomString(),

View file

@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.metadata
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
@ -90,6 +91,7 @@ class MetadataReaderTest {
"org.example", PackageMetadata( "org.example", PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = QUOTA_EXCEEDED, state = QUOTA_EXCEEDED,
backupType = BackupType.FULL,
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
sha256 = getRandomString(), sha256 = getRandomString(),
@ -123,6 +125,7 @@ class MetadataReaderTest {
json.put("org.example", JSONObject().apply { json.put("org.example", JSONObject().apply {
put(JSON_PACKAGE_TIME, Random.nextLong()) put(JSON_PACKAGE_TIME, Random.nextLong())
put(JSON_PACKAGE_STATE, getRandomString()) put(JSON_PACKAGE_STATE, getRandomString())
put(JSON_PACKAGE_BACKUP_TYPE, BackupType.FULL.name)
put(JSON_PACKAGE_VERSION, Random.nextLong()) put(JSON_PACKAGE_VERSION, Random.nextLong())
put(JSON_PACKAGE_INSTALLER, getRandomString()) put(JSON_PACKAGE_INSTALLER, getRandomString())
put(JSON_PACKAGE_SHA256, getRandomString()) put(JSON_PACKAGE_SHA256, getRandomString())
@ -130,7 +133,9 @@ class MetadataReaderTest {
}) })
val jsonBytes = json.toString().toByteArray(Utf8) val jsonBytes = json.toString().toByteArray(Utf8)
val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token) val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token)
assertEquals(this.metadata.salt, metadata.salt)
assertEquals(UNKNOWN_ERROR, metadata.packageMetadataMap["org.example"]!!.state) assertEquals(UNKNOWN_ERROR, metadata.packageMetadataMap["org.example"]!!.state)
assertEquals(BackupType.FULL, metadata.packageMetadataMap["org.example"]!!.backupType)
} }
@Test @Test
@ -142,6 +147,7 @@ class MetadataReaderTest {
val jsonBytes = json.toString().toByteArray(Utf8) val jsonBytes = json.toString().toByteArray(Utf8)
val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token) val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token)
assertFalse(metadata.packageMetadataMap["org.example"]!!.system) assertFalse(metadata.packageMetadataMap["org.example"]!!.system)
assertNull(metadata.packageMetadataMap["org.example"]!!.backupType)
} }
@Test @Test
@ -149,12 +155,14 @@ class MetadataReaderTest {
val json = JSONObject(metadataByteArray.toString(Utf8)) val json = JSONObject(metadataByteArray.toString(Utf8))
json.put("org.example", JSONObject().apply { json.put("org.example", JSONObject().apply {
put(JSON_PACKAGE_TIME, Random.nextLong()) put(JSON_PACKAGE_TIME, Random.nextLong())
put(JSON_PACKAGE_BACKUP_TYPE, BackupType.KV.name)
}) })
val jsonBytes = json.toString().toByteArray(Utf8) val jsonBytes = json.toString().toByteArray(Utf8)
val result = decoder.decode(jsonBytes, metadata.version, metadata.token) val result = decoder.decode(jsonBytes, metadata.version, metadata.token)
assertEquals(1, result.packageMetadataMap.size) assertEquals(1, result.packageMetadataMap.size)
val packageMetadata = result.packageMetadataMap.getOrElse("org.example") { fail() } val packageMetadata = result.packageMetadataMap.getOrElse("org.example") { fail() }
assertEquals(BackupType.KV, packageMetadata.backupType)
assertNull(packageMetadata.version) assertNull(packageMetadata.version)
assertNull(packageMetadata.installer) assertNull(packageMetadata.installer)
assertNull(packageMetadata.signatures) assertNull(packageMetadata.signatures)
@ -166,6 +174,7 @@ class MetadataReaderTest {
return BackupMetadata( return BackupMetadata(
version = 1.toByte(), version = 1.toByte(),
token = Random.nextLong(), token = Random.nextLong(),
salt = getRandomBase64(METADATA_SALT_SIZE),
time = Random.nextLong(), time = Random.nextLong(),
androidVersion = Random.nextInt(), androidVersion = Random.nextInt(),
androidIncremental = getRandomString(), androidIncremental = getRandomString(),

View file

@ -54,6 +54,7 @@ internal class MetadataV0ReadTest {
) = BackupMetadata( ) = BackupMetadata(
version = 0x00, version = 0x00,
token = 1337L, token = 1337L,
salt = "",
time = 2342L, time = 2342L,
androidVersion = 30, androidVersion = 30,
androidIncremental = "sdfqefpojlfj", androidIncremental = "sdfqefpojlfj",

View file

@ -1,6 +1,7 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
@ -35,8 +36,8 @@ internal class MetadataWriterDecoderTest {
fun `encoded metadata matches decoded metadata (with package, no apk info)`() { fun `encoded metadata matches decoded metadata (with package, no apk info)`() {
val time = Random.nextLong() val time = Random.nextLong()
val packages = HashMap<String, PackageMetadata>().apply { val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(time, APK_AND_DATA)) put(getRandomString(), PackageMetadata(time, APK_AND_DATA, BackupType.FULL))
put(getRandomString(), PackageMetadata(time, WAS_STOPPED)) put(getRandomString(), PackageMetadata(time, WAS_STOPPED, BackupType.KV))
} }
val metadata = getMetadata(packages) val metadata = getMetadata(packages)
assertEquals( assertEquals(
@ -52,6 +53,7 @@ internal class MetadataWriterDecoderTest {
getRandomString(), PackageMetadata( getRandomString(), PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = APK_AND_DATA, state = APK_AND_DATA,
backupType = BackupType.FULL,
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
splits = listOf( splits = listOf(
@ -78,6 +80,7 @@ internal class MetadataWriterDecoderTest {
getRandomString(), PackageMetadata( getRandomString(), PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = QUOTA_EXCEEDED, state = QUOTA_EXCEEDED,
backupType = BackupType.FULL,
system = Random.nextBoolean(), system = Random.nextBoolean(),
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
@ -89,6 +92,7 @@ internal class MetadataWriterDecoderTest {
getRandomString(), PackageMetadata( getRandomString(), PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = NO_DATA, state = NO_DATA,
backupType = BackupType.KV,
system = Random.nextBoolean(), system = Random.nextBoolean(),
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
@ -121,6 +125,7 @@ internal class MetadataWriterDecoderTest {
return BackupMetadata( return BackupMetadata(
version = Random.nextBytes(1)[0], version = Random.nextBytes(1)[0],
token = Random.nextLong(), token = Random.nextLong(),
salt = getRandomBase64(32),
time = Random.nextLong(), time = Random.nextLong(),
androidVersion = Random.nextInt(), androidVersion = Random.nextInt(),
androidIncremental = getRandomString(), androidIncremental = getRandomString(),

View file

@ -13,6 +13,7 @@ import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
@ -33,7 +34,6 @@ import com.stevesoltys.seedvault.transport.restore.KVRestore
import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
import com.stevesoltys.seedvault.transport.restore.OutputFactory import com.stevesoltys.seedvault.transport.restore.OutputFactory
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.CapturingSlot import io.mockk.CapturingSlot
import io.mockk.Runs import io.mockk.Runs
@ -93,7 +93,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
notificationManager notificationManager
) )
private val restorePlugin = mockk<RestorePlugin>()
private val kvRestorePlugin = mockk<KVRestorePlugin>() private val kvRestorePlugin = mockk<KVRestorePlugin>()
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val fullRestorePlugin = mockk<FullRestorePlugin>() private val fullRestorePlugin = mockk<FullRestorePlugin>()
@ -172,13 +171,11 @@ internal class CoordinatorIntegrationTest : TransportTest() {
backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA) backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
} returns metadataOutputStream } returns metadataOutputStream
every { every {
metadataManager.onApkBackedUp( metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream)
packageInfo, } just Runs
packageMetadata, every {
metadataOutputStream metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
)
} just Runs } just Runs
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
// start and finish K/V backup // start and finish K/V backup
assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)) assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
@ -252,7 +249,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
coEvery { coEvery {
backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA) backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
} returns metadataOutputStream } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs every {
metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
} just Runs
// start and finish K/V backup // start and finish K/V backup
assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)) assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
@ -314,7 +313,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
metadataOutputStream metadataOutputStream
) )
} just Runs } just Runs
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs every {
metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
} just Runs
// perform backup to output stream // perform backup to output stream
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0)) assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))

View file

@ -10,8 +10,10 @@ import android.util.Log
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.BackupMetadata import com.stevesoltys.seedvault.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.METADATA_SALT_SIZE
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.every import io.mockk.every
@ -45,6 +47,7 @@ internal abstract class TransportTest {
} }
protected val metadata = BackupMetadata( protected val metadata = BackupMetadata(
token = token, token = token,
salt = getRandomBase64(METADATA_SALT_SIZE),
androidVersion = Random.nextInt(), androidVersion = Random.nextInt(),
androidIncremental = getRandomString(), androidIncremental = getRandomString(),
deviceName = getRandomString() deviceName = getRandomString()

View file

@ -15,6 +15,7 @@ import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
@ -254,7 +255,9 @@ internal class BackupCoordinatorTest : BackupTest() {
every { kv.getCurrentPackage() } returns packageInfo every { kv.getCurrentPackage() } returns packageInfo
every { settingsManager.getToken() } returns token every { settingsManager.getToken() } returns token
coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs every {
metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
} just Runs
every { kv.finishBackup() } returns result every { kv.finishBackup() } returns result
every { metadataOutputStream.close() } just Runs every { metadataOutputStream.close() } just Runs
@ -272,7 +275,9 @@ internal class BackupCoordinatorTest : BackupTest() {
every { full.getCurrentPackage() } returns packageInfo every { full.getCurrentPackage() } returns packageInfo
every { settingsManager.getToken() } returns token every { settingsManager.getToken() } returns token
coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs every {
metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
} just Runs
every { full.finishBackup() } returns result every { full.finishBackup() } returns result
every { metadataOutputStream.close() } just Runs every { metadataOutputStream.close() } just Runs
@ -302,7 +307,8 @@ internal class BackupCoordinatorTest : BackupTest() {
metadataManager.onPackageBackupError( metadataManager.onPackageBackupError(
packageInfo, packageInfo,
QUOTA_EXCEEDED, QUOTA_EXCEEDED,
metadataOutputStream metadataOutputStream,
BackupType.FULL
) )
} just Runs } just Runs
coEvery { full.cancelFullBackup() } just Runs coEvery { full.cancelFullBackup() } just Runs
@ -325,7 +331,12 @@ internal class BackupCoordinatorTest : BackupTest() {
assertEquals(0L, backup.requestFullBackupTime()) assertEquals(0L, backup.requestFullBackupTime())
verify(exactly = 1) { verify(exactly = 1) {
metadataManager.onPackageBackupError(packageInfo, QUOTA_EXCEEDED, metadataOutputStream) metadataManager.onPackageBackupError(
packageInfo,
QUOTA_EXCEEDED,
metadataOutputStream,
BackupType.FULL
)
} }
verify { metadataOutputStream.close() } verify { metadataOutputStream.close() }
} }
@ -341,7 +352,8 @@ internal class BackupCoordinatorTest : BackupTest() {
metadataManager.onPackageBackupError( metadataManager.onPackageBackupError(
packageInfo, packageInfo,
NO_DATA, NO_DATA,
metadataOutputStream metadataOutputStream,
BackupType.FULL
) )
} just Runs } just Runs
coEvery { full.cancelFullBackup() } just Runs coEvery { full.cancelFullBackup() } just Runs
@ -361,7 +373,12 @@ internal class BackupCoordinatorTest : BackupTest() {
assertEquals(0L, backup.requestFullBackupTime()) assertEquals(0L, backup.requestFullBackupTime())
verify(exactly = 1) { verify(exactly = 1) {
metadataManager.onPackageBackupError(packageInfo, NO_DATA, metadataOutputStream) metadataManager.onPackageBackupError(
packageInfo,
NO_DATA,
metadataOutputStream,
BackupType.FULL
)
} }
verify { metadataOutputStream.close() } verify { metadataOutputStream.close() }
} }