Add information about packages to backup metadata

This will be needed when backing up APKs.

ATTENTION: This is a breaking change, we only do because the app hasn't
been released.
This commit is contained in:
Torsten Grote 2019-12-17 13:45:36 -03:00
parent 01098a4d97
commit e1d55c8a4e
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
7 changed files with 220 additions and 37 deletions

View file

@ -1,25 +1,50 @@
package com.stevesoltys.seedvault.metadata
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import com.stevesoltys.seedvault.header.VERSION
import java.io.InputStream
data class BackupMetadata(
internal val version: Byte = VERSION,
internal val token: Long,
internal val androidVersion: Int = SDK_INT,
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}"
internal val time: Long = System.currentTimeMillis(),
internal val androidVersion: Int = Build.VERSION.SDK_INT,
internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
internal val packageMetadata: Map<String, PackageMetadata> = HashMap()
)
internal const val JSON_VERSION = "version"
internal const val JSON_TOKEN = "token"
internal const val JSON_ANDROID_VERSION = "androidVersion"
internal const val JSON_DEVICE_NAME = "deviceName"
internal const val JSON_METADATA = "@meta@"
internal const val JSON_METADATA_VERSION = "version"
internal const val JSON_METADATA_TOKEN = "token"
internal const val JSON_METADATA_TIME = "time"
internal const val JSON_METADATA_SDK_INT = "sdk_int"
internal const val JSON_METADATA_INCREMENTAL = "incremental"
internal const val JSON_METADATA_NAME = "name"
data class PackageMetadata(
internal val time: Long,
internal val version: Long? = null,
internal val installer: String? = null,
internal val signatures: List<String>? = null
) {
fun hasApk(): Boolean {
return version != null && signatures != null
}
}
internal const val JSON_PACKAGE_TIME = "time"
internal const val JSON_PACKAGE_VERSION = "version"
internal const val JSON_PACKAGE_INSTALLER = "installer"
internal const val JSON_PACKAGE_SIGNATURES = "signatures"
internal class DecryptionFailedException(cause: Throwable) : Exception(cause)
class EncryptedBackupMetadata private constructor(val token: Long, val inputStream: InputStream?, val error: Boolean) {
class EncryptedBackupMetadata private constructor(
val token: Long,
val inputStream: InputStream?,
val error: Boolean) {
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
/**
* Indicates that there was an error retrieving the encrypted backup metadata.

View file

@ -43,19 +43,45 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
// matches the authenticated version and token in the JSON.
try {
val json = JSONObject(bytes.toString(Utf8))
val version = json.getInt(JSON_VERSION).toByte()
// get backup metadata and check expectations
val meta = json.getJSONObject(JSON_METADATA)
val version = meta.getInt(JSON_METADATA_VERSION).toByte()
if (version != expectedVersion) {
throw SecurityException("Invalid version '${version.toInt()}' in metadata, expected '${expectedVersion.toInt()}'.")
}
val token = json.getLong(JSON_TOKEN)
val token = meta.getLong(JSON_METADATA_TOKEN)
if (token != expectedToken) {
throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.")
}
// get package metadata
val packageMetadata: HashMap<String, PackageMetadata> = HashMap()
for (packageName in json.keys()) {
if (packageName == JSON_METADATA) continue
val p = json.getJSONObject(packageName)
val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L)
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER, "")
val pSignatures = p.optJSONArray(JSON_PACKAGE_SIGNATURES)
val signatures = if (pSignatures == null) null else
ArrayList<String>(pSignatures.length()).apply {
for (i in (0 until pSignatures.length())) {
add(pSignatures.getString(i))
}
}
packageMetadata[packageName] = PackageMetadata(
time = p.getLong(JSON_PACKAGE_TIME),
version = if (pVersion == 0L) null else pVersion,
installer = if (pInstaller == "") null else pInstaller,
signatures = signatures
)
}
return BackupMetadata(
version = version,
token = token,
androidVersion = json.getInt(JSON_ANDROID_VERSION),
deviceName = json.getString(JSON_DEVICE_NAME)
time = meta.getLong(JSON_METADATA_TIME),
androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
deviceName = meta.getString(JSON_METADATA_NAME),
packageMetadata = packageMetadata
)
} catch (e: JSONException) {
throw SecurityException(e)

View file

@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.metadata
import androidx.annotation.VisibleForTesting
import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.io.OutputStream
@ -25,11 +26,24 @@ internal class MetadataWriterImpl(private val crypto: Crypto): MetadataWriter {
@VisibleForTesting
internal fun encode(metadata: BackupMetadata): ByteArray {
val json = JSONObject()
json.put(JSON_VERSION, metadata.version.toInt())
json.put(JSON_TOKEN, metadata.token)
json.put(JSON_ANDROID_VERSION, metadata.androidVersion)
json.put(JSON_DEVICE_NAME, metadata.deviceName)
val json = JSONObject().apply {
put(JSON_METADATA, JSONObject().apply {
put(JSON_METADATA_VERSION, metadata.version.toInt())
put(JSON_METADATA_TOKEN, metadata.token)
put(JSON_METADATA_TIME, metadata.time)
put(JSON_METADATA_SDK_INT, metadata.androidVersion)
put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental)
put(JSON_METADATA_NAME, metadata.deviceName)
})
}
for ((packageName, packageMetadata) in metadata.packageMetadata) {
json.put(packageName, JSONObject().apply {
put(JSON_PACKAGE_TIME, packageMetadata.time)
packageMetadata.version?.let { put(JSON_PACKAGE_VERSION, it) }
packageMetadata.installer?.let { put(JSON_PACKAGE_INSTALLER, it) }
packageMetadata.signatures?.let { put(JSON_PACKAGE_SIGNATURES, JSONArray(it)) }
})
}
return json.toString().toByteArray(Utf8)
}

View file

@ -17,7 +17,7 @@ import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.TimeUnit.MINUTES
const val DIRECTORY_ROOT = ".AndroidBackup"
const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
const val DIRECTORY_FULL_BACKUP = "full"
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
const val FILE_BACKUP_METADATA = ".backup.metadata"

View file

@ -4,8 +4,9 @@ import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.getRandomString
import io.mockk.mockk
import org.json.JSONArray
import org.json.JSONObject
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
@ -19,12 +20,7 @@ class MetadataReaderTest {
private val encoder = MetadataWriterImpl(crypto)
private val decoder = MetadataReaderImpl(crypto)
private val metadata = BackupMetadata(
version = 1.toByte(),
token = Random.nextLong(),
androidVersion = Random.nextInt(),
deviceName = getRandomString()
)
private val metadata = getMetadata()
private val metadataByteArray = encoder.encode(metadata)
@Test
@ -55,10 +51,13 @@ class MetadataReaderTest {
@Test
fun `missing fields throws SecurityException`() {
val json = JSONObject()
json.put(JSON_VERSION, metadata.version.toInt())
json.put(JSON_TOKEN, metadata.token)
json.put(JSON_ANDROID_VERSION, metadata.androidVersion)
val json = JSONObject().apply {
put(JSON_METADATA, JSONObject().apply {
put(JSON_METADATA_VERSION, metadata.version.toInt())
put(JSON_METADATA_TOKEN, metadata.token)
put(JSON_METADATA_SDK_INT, metadata.androidVersion)
})
}
val jsonBytes = json.toString().toByteArray(Utf8)
assertThrows(SecurityException::class.java) {
@ -66,4 +65,73 @@ class MetadataReaderTest {
}
}
@Test
fun `missing meta throws SecurityException`() {
val json = JSONObject().apply {
put("foo", "bat")
}
val jsonBytes = json.toString().toByteArray(Utf8)
assertThrows(SecurityException::class.java) {
decoder.decode(jsonBytes, metadata.version, metadata.token)
}
}
@Test
fun `package metadata gets read`() {
val packageMetadata = HashMap<String, PackageMetadata>().apply {
put("org.example", PackageMetadata(
time = Random.nextLong(),
version = Random.nextLong(),
installer = getRandomString(),
signatures = listOf(getRandomString(), getRandomString())
))
}
val metadata = getMetadata(packageMetadata)
val metadataByteArray = encoder.encode(metadata)
decoder.decode(metadataByteArray, metadata.version, metadata.token)
}
@Test
fun `package metadata with missing time throws`() {
val json = JSONObject(metadataByteArray.toString(Utf8))
json.put("org.example", JSONObject().apply {
put(JSON_PACKAGE_VERSION, Random.nextLong())
put(JSON_PACKAGE_INSTALLER, getRandomString())
put(JSON_PACKAGE_SIGNATURES, JSONArray(listOf(getRandomString(), getRandomString())))
})
val jsonBytes = json.toString().toByteArray(Utf8)
assertThrows(SecurityException::class.java) {
decoder.decode(jsonBytes, metadata.version, metadata.token)
}
}
@Test
fun `package metadata can only include time`() {
val json = JSONObject(metadataByteArray.toString(Utf8))
json.put("org.example", JSONObject().apply {
put(JSON_PACKAGE_TIME, Random.nextLong())
})
val jsonBytes = json.toString().toByteArray(Utf8)
val result = decoder.decode(jsonBytes, metadata.version, metadata.token)
assertEquals(1, result.packageMetadata.size)
val packageMetadata = result.packageMetadata.getOrElse("org.example") { fail() }
assertNull(packageMetadata.version)
assertNull(packageMetadata.installer)
assertNull(packageMetadata.signatures)
}
private fun getMetadata(packageMetadata: Map<String, PackageMetadata> = HashMap()): BackupMetadata {
return BackupMetadata(
version = 1.toByte(),
token = Random.nextLong(),
time = Random.nextLong(),
androidVersion = Random.nextInt(),
androidIncremental = getRandomString(),
deviceName = getRandomString(),
packageMetadata = packageMetadata
)
}
}

View file

@ -17,16 +17,65 @@ internal class MetadataWriterDecoderTest {
private val encoder = MetadataWriterImpl(crypto)
private val decoder = MetadataReaderImpl(crypto)
private val metadata = BackupMetadata(
version = Random.nextBytes(1)[0],
token = Random.nextLong(),
androidVersion = Random.nextInt(),
deviceName = getRandomString()
)
@Test
fun `encoded metadata matches decoded metadata`() {
fun `encoded metadata matches decoded metadata (no packages)`() {
val metadata = getMetadata()
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
}
@Test
fun `encoded metadata matches decoded metadata (with package, no apk info)`() {
val time = Random.nextLong()
val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(time))
}
val metadata = getMetadata(packages)
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
}
@Test
fun `encoded metadata matches decoded metadata (full package)`() {
val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(
time = Random.nextLong(),
version = Random.nextLong(),
installer = getRandomString(),
signatures = listOf(getRandomString(), getRandomString())))
}
val metadata = getMetadata(packages)
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
}
@Test
fun `encoded metadata matches decoded metadata (two full packages)`() {
val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(
time = Random.nextLong(),
version = Random.nextLong(),
installer = getRandomString(),
signatures = listOf(getRandomString())
))
put(getRandomString(), PackageMetadata(
time = Random.nextLong(),
version = Random.nextLong(),
installer = getRandomString(),
signatures = listOf(getRandomString(), getRandomString())
))
}
val metadata = getMetadata(packages)
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
}
private fun getMetadata(packageMetadata: Map<String, PackageMetadata> = HashMap()): BackupMetadata {
return BackupMetadata(
version = Random.nextBytes(1)[0],
token = Random.nextLong(),
time = Random.nextLong(),
androidVersion = Random.nextInt(),
androidIncremental = getRandomString(),
deviceName = getRandomString(),
packageMetadata = packageMetadata
)
}
}

View file

@ -41,6 +41,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
val metadata = BackupMetadata(
token = token,
androidVersion = Random.nextInt(),
androidIncremental = getRandomString(),
deviceName = getRandomString())
every { plugin.getAvailableBackups() } returns sequenceOf(encryptedMetadata, encryptedMetadata)