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:
parent
01098a4d97
commit
e1d55c8a4e
7 changed files with 220 additions and 37 deletions
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue