Fix device initialization and generation of new backup tokens
This commit is contained in:
parent
81c2031ce7
commit
569e3db385
18 changed files with 266 additions and 149 deletions
|
@ -4,6 +4,8 @@ import android.os.Build
|
|||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import java.io.InputStream
|
||||
|
||||
typealias PackageMetadataMap = HashMap<String, PackageMetadata>
|
||||
|
||||
data class BackupMetadata(
|
||||
internal val version: Byte = VERSION,
|
||||
internal val token: Long,
|
||||
|
@ -11,7 +13,7 @@ data class BackupMetadata(
|
|||
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: HashMap<String, PackageMetadata> = HashMap()
|
||||
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap()
|
||||
)
|
||||
|
||||
internal const val JSON_METADATA = "@meta@"
|
||||
|
@ -26,16 +28,18 @@ data class PackageMetadata(
|
|||
internal var time: Long,
|
||||
internal val version: Long? = null,
|
||||
internal val installer: String? = null,
|
||||
internal val sha256: String? = null,
|
||||
internal val signatures: List<String>? = null
|
||||
) {
|
||||
fun hasApk(): Boolean {
|
||||
return version != null && signatures != null
|
||||
return version != null && sha256 != 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_SHA256 = "sha256"
|
||||
internal const val JSON_PACKAGE_SIGNATURES = "signatures"
|
||||
|
||||
internal class DecryptionFailedException(cause: Throwable) : Exception(cause)
|
||||
|
|
|
@ -28,10 +28,8 @@ class MetadataManager(
|
|||
field = try {
|
||||
getMetadataFromCache() ?: throw IOException()
|
||||
} catch (e: IOException) {
|
||||
// create new default metadata
|
||||
// Attention: If this happens due to a read error, we will overwrite remote metadata
|
||||
Log.w(TAG, "Creating new metadata...")
|
||||
BackupMetadata(token = clock.time())
|
||||
// If this happens, it is hard to recover from this. Let's hope it never does.
|
||||
throw AssertionError("Error reading metadata from cache", e)
|
||||
}
|
||||
}
|
||||
return field
|
||||
|
@ -40,14 +38,13 @@ class MetadataManager(
|
|||
/**
|
||||
* Call this when initializing a new device.
|
||||
*
|
||||
* A new backup token will be generated.
|
||||
* Existing [BackupMetadata] will be cleared
|
||||
* Existing [BackupMetadata] will be cleared, use the given new token,
|
||||
* and written encrypted to the given [OutputStream] as well as the internal cache.
|
||||
*/
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun onDeviceInitialization(metadataOutputStream: OutputStream) {
|
||||
metadata = BackupMetadata(token = clock.time())
|
||||
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
|
||||
metadata = BackupMetadata(token = token)
|
||||
metadataWriter.write(metadata, metadataOutputStream)
|
||||
writeMetadataToCache()
|
||||
}
|
||||
|
@ -59,7 +56,7 @@ class MetadataManager(
|
|||
*/
|
||||
@Synchronized
|
||||
fun onApkBackedUp(packageName: String, packageMetadata: PackageMetadata) {
|
||||
metadata.packageMetadata[packageName]?.let {
|
||||
metadata.packageMetadataMap[packageName]?.let {
|
||||
check(it.time <= packageMetadata.time) {
|
||||
"APK backup set time of $packageName backwards"
|
||||
}
|
||||
|
@ -70,7 +67,7 @@ class MetadataManager(
|
|||
"APK backup backed up the same or a smaller version: was ${it.version} is ${packageMetadata.version}"
|
||||
}
|
||||
}
|
||||
metadata.packageMetadata[packageName] = packageMetadata
|
||||
metadata.packageMetadataMap[packageName] = packageMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,22 +82,28 @@ class MetadataManager(
|
|||
val oldMetadata = metadata.copy()
|
||||
val now = clock.time()
|
||||
metadata.time = now
|
||||
if (metadata.packageMetadata.containsKey(packageName)) {
|
||||
metadata.packageMetadata[packageName]?.time = now
|
||||
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
||||
metadata.packageMetadataMap[packageName]?.time = now
|
||||
} else {
|
||||
metadata.packageMetadata[packageName] = PackageMetadata(time = now)
|
||||
metadata.packageMetadataMap[packageName] = PackageMetadata(time = now)
|
||||
}
|
||||
try {
|
||||
metadataWriter.write(metadata, metadataOutputStream)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Error writing metadata to storage", e)
|
||||
// revert metadata and do not write it to cache
|
||||
// TODO also revert changes made by last [onApkBackedUp]
|
||||
metadata = oldMetadata
|
||||
throw IOException(e)
|
||||
}
|
||||
writeMetadataToCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current backup token.
|
||||
*
|
||||
* If the token is 0L, it is not yet initialized and must not be used for anything.
|
||||
*/
|
||||
@Synchronized
|
||||
fun getBackupToken(): Long = metadata.token
|
||||
|
||||
|
@ -114,7 +117,7 @@ class MetadataManager(
|
|||
|
||||
@Synchronized
|
||||
fun getPackageMetadata(packageName: String): PackageMetadata? {
|
||||
return metadata.packageMetadata[packageName]?.copy()
|
||||
return metadata.packageMetadataMap[packageName]?.copy()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
@ -129,7 +132,7 @@ class MetadataManager(
|
|||
return null
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.d(TAG, "Cached metadata not found, creating...")
|
||||
return null
|
||||
return uninitializedMetadata
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,12 +55,13 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
|||
throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.")
|
||||
}
|
||||
// get package metadata
|
||||
val packageMetadata: HashMap<String, PackageMetadata> = HashMap()
|
||||
val packageMetadataMap = PackageMetadataMap()
|
||||
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 pSha256 = p.optString(JSON_PACKAGE_SHA256)
|
||||
val pSignatures = p.optJSONArray(JSON_PACKAGE_SIGNATURES)
|
||||
val signatures = if (pSignatures == null) null else
|
||||
ArrayList<String>(pSignatures.length()).apply {
|
||||
|
@ -68,10 +69,11 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
|||
add(pSignatures.getString(i))
|
||||
}
|
||||
}
|
||||
packageMetadata[packageName] = PackageMetadata(
|
||||
packageMetadataMap[packageName] = PackageMetadata(
|
||||
time = p.getLong(JSON_PACKAGE_TIME),
|
||||
version = if (pVersion == 0L) null else pVersion,
|
||||
installer = if (pInstaller == "") null else pInstaller,
|
||||
sha256 = if (pSha256 == "") null else pSha256,
|
||||
signatures = signatures
|
||||
)
|
||||
}
|
||||
|
@ -82,7 +84,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
|||
androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
|
||||
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
|
||||
deviceName = meta.getString(JSON_METADATA_NAME),
|
||||
packageMetadata = packageMetadata
|
||||
packageMetadataMap = packageMetadataMap
|
||||
)
|
||||
} catch (e: JSONException) {
|
||||
throw SecurityException(e)
|
||||
|
|
|
@ -33,11 +33,12 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
|
|||
put(JSON_METADATA_NAME, metadata.deviceName)
|
||||
})
|
||||
}
|
||||
for ((packageName, packageMetadata) in metadata.packageMetadata) {
|
||||
for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
|
||||
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.sha256?.let { put(JSON_PACKAGE_SHA256, it) }
|
||||
packageMetadata.signatures?.let { put(JSON_PACKAGE_SIGNATURES, JSONArray(it)) }
|
||||
})
|
||||
}
|
||||
|
|
|
@ -23,7 +23,13 @@ internal class DocumentsProviderBackupPlugin(
|
|||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun initializeDevice() {
|
||||
override fun initializeDevice(newToken: Long): Boolean {
|
||||
// check if storage is already initialized
|
||||
if (storage.isInitialized()) return false
|
||||
|
||||
// reset current storage
|
||||
storage.reset(newToken)
|
||||
|
||||
// get or create root backup dir
|
||||
storage.rootBackupDir ?: throw IOException()
|
||||
|
||||
|
@ -35,6 +41,8 @@ internal class DocumentsProviderBackupPlugin(
|
|||
storage.getSetDir()?.findFile(FILE_BACKUP_METADATA)?.delete()
|
||||
kvDir?.deleteContents()
|
||||
fullDir?.deleteContents()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
|
|
|
@ -30,53 +30,91 @@ private val TAG = DocumentsStorage::class.java.simpleName
|
|||
internal class DocumentsStorage(
|
||||
private val context: Context,
|
||||
private val metadataManager: MetadataManager,
|
||||
settingsManager: SettingsManager) {
|
||||
private val settingsManager: SettingsManager) {
|
||||
|
||||
private val storage: Storage? = settingsManager.getStorage()
|
||||
|
||||
internal val rootBackupDir: DocumentFile? by lazy {
|
||||
val parent = storage?.getDocumentFile(context) ?: return@lazy null
|
||||
try {
|
||||
val rootDir = parent.createOrGetDirectory(DIRECTORY_ROOT)
|
||||
// create .nomedia file to prevent Android's MediaScanner from trying to index the backup
|
||||
rootDir.createOrGetFile(FILE_NO_MEDIA)
|
||||
rootDir
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating root backup dir.", e)
|
||||
null
|
||||
internal var storage: Storage? = null
|
||||
get() {
|
||||
if (field == null) field = settingsManager.getStorage()
|
||||
return field
|
||||
}
|
||||
|
||||
internal var rootBackupDir: DocumentFile? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
val parent = storage?.getDocumentFile(context) ?: return null
|
||||
field = try {
|
||||
val rootDir = parent.createOrGetDirectory(DIRECTORY_ROOT)
|
||||
// create .nomedia file to prevent Android's MediaScanner from trying to index the backup
|
||||
rootDir.createOrGetFile(FILE_NO_MEDIA)
|
||||
rootDir
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating root backup dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
private var currentToken: Long = 0L
|
||||
get() {
|
||||
if (field == 0L) field = metadataManager.getBackupToken()
|
||||
return field
|
||||
}
|
||||
|
||||
private var currentSetDir: DocumentFile? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
if (currentToken == 0L) return null
|
||||
field = try {
|
||||
rootBackupDir?.createOrGetDirectory(currentToken.toString())
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating current restore set dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
var currentFullBackupDir: DocumentFile? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
field = try {
|
||||
currentSetDir?.createOrGetDirectory(DIRECTORY_FULL_BACKUP)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating full backup dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
var currentKvBackupDir: DocumentFile? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
field = try {
|
||||
currentSetDir?.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating K/V backup dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
fun isInitialized(): Boolean {
|
||||
if (settingsManager.getAndResetIsStorageChanging()) return false // storage location has changed
|
||||
val kvEmpty = currentKvBackupDir?.listFiles()?.isEmpty() ?: false
|
||||
val fullEmpty = currentFullBackupDir?.listFiles()?.isEmpty() ?: false
|
||||
return kvEmpty && fullEmpty
|
||||
}
|
||||
|
||||
private val currentToken: Long by lazy {
|
||||
metadataManager.getBackupToken()
|
||||
}
|
||||
|
||||
private val currentSetDir: DocumentFile? by lazy {
|
||||
val currentSetName = currentToken.toString()
|
||||
try {
|
||||
rootBackupDir?.createOrGetDirectory(currentSetName)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating current restore set dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val currentFullBackupDir: DocumentFile? by lazy {
|
||||
try {
|
||||
currentSetDir?.createOrGetDirectory(DIRECTORY_FULL_BACKUP)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating full backup dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val currentKvBackupDir: DocumentFile? by lazy {
|
||||
try {
|
||||
currentSetDir?.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating K/V backup dir.", e)
|
||||
null
|
||||
}
|
||||
fun reset(newToken: Long) {
|
||||
storage = null
|
||||
currentToken = newToken
|
||||
rootBackupDir = null
|
||||
currentSetDir = null
|
||||
currentKvBackupDir = null
|
||||
currentFullBackupDir = null
|
||||
}
|
||||
|
||||
fun getAuthority(): String? = storage?.uri?.authority
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.hardware.usb.UsbDevice
|
|||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
||||
private const val PREF_KEY_STORAGE_NAME = "storageName"
|
||||
|
@ -19,6 +20,8 @@ class SettingsManager(context: Context) {
|
|||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
private var isStorageChanging: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
// FIXME Storage is currently plugin specific and not generic
|
||||
fun setStorage(storage: Storage) {
|
||||
prefs.edit()
|
||||
|
@ -26,6 +29,7 @@ class SettingsManager(context: Context) {
|
|||
.putString(PREF_KEY_STORAGE_NAME, storage.name)
|
||||
.putBoolean(PREF_KEY_STORAGE_IS_USB, storage.isUsb)
|
||||
.apply()
|
||||
isStorageChanging.set(true)
|
||||
}
|
||||
|
||||
fun getStorage(): Storage? {
|
||||
|
@ -36,6 +40,10 @@ class SettingsManager(context: Context) {
|
|||
return Storage(uri, name, isUsb)
|
||||
}
|
||||
|
||||
fun getAndResetIsStorageChanging(): Boolean {
|
||||
return isStorageChanging.getAndSet(false)
|
||||
}
|
||||
|
||||
fun setFlashDrive(usb: FlashDrive?) {
|
||||
if (usb == null) {
|
||||
prefs.edit()
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.content.pm.PackageManager
|
|||
import android.content.pm.Signature
|
||||
import android.content.pm.SigningInfo
|
||||
import android.util.Log
|
||||
import android.util.PackageUtils.computeSha256DigestBytes
|
||||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.encodeBase64
|
||||
|
@ -18,7 +19,6 @@ import java.io.FileNotFoundException
|
|||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
private val TAG = ApkBackup::class.java.simpleName
|
||||
|
||||
|
@ -45,23 +45,23 @@ class ApkBackup(
|
|||
return false
|
||||
}
|
||||
|
||||
// get cached metadata about package
|
||||
val packageMetadata = metadataManager.getPackageMetadata(packageName)
|
||||
?: PackageMetadata(time = clock.time())
|
||||
|
||||
// TODO remove when adding support in [signaturesChanged]
|
||||
// TODO remove when adding support for packages with multiple signers
|
||||
if (packageInfo.signingInfo.hasMultipleSigners()) {
|
||||
Log.e(TAG, "Package $packageName has multiple signers. Not backing it up.")
|
||||
return false
|
||||
}
|
||||
|
||||
// get signatures
|
||||
val signatures = getSignatures(packageInfo.signingInfo)
|
||||
val signatures = packageInfo.signingInfo.getSignatures()
|
||||
if (signatures.isEmpty()) {
|
||||
Log.e(TAG, "Package $packageName has no signatures. Not backing it up.")
|
||||
return false
|
||||
}
|
||||
|
||||
// get cached metadata about package
|
||||
val packageMetadata = metadataManager.getPackageMetadata(packageName)
|
||||
?: PackageMetadata(time = clock.time())
|
||||
|
||||
// get version codes
|
||||
val version = packageInfo.longVersionCode
|
||||
val backedUpVersion = packageMetadata.version ?: 0L // no version will cause backup
|
||||
|
@ -84,12 +84,20 @@ class ApkBackup(
|
|||
throw IOException(e)
|
||||
}
|
||||
|
||||
// copy the APK to the storage's output
|
||||
// copy the APK to the storage's output and calculate SHA-256 hash while at it
|
||||
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
streamGetter.invoke().use { outputStream ->
|
||||
inputStream.use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var bytes = inputStream.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
outputStream.write(buffer, 0, bytes)
|
||||
messageDigest.update(buffer, 0, bytes)
|
||||
bytes = inputStream.read(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
val sha256 = messageDigest.digest().encodeBase64()
|
||||
Log.d(TAG, "Backed up new APK of $packageName with version $version.")
|
||||
|
||||
// update the metadata
|
||||
|
@ -98,44 +106,37 @@ class ApkBackup(
|
|||
time = clock.time(),
|
||||
version = version,
|
||||
installer = installer,
|
||||
sha256 = sha256,
|
||||
signatures = signatures
|
||||
)
|
||||
metadataManager.onApkBackedUp(packageName, updatedMetadata)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getSignatures(signingInfo: SigningInfo): List<String> {
|
||||
val signatures = ArrayList<String>()
|
||||
if (signingInfo.hasMultipleSigners()) {
|
||||
for (sig in signingInfo.apkContentsSigners) {
|
||||
signatures.add(hashSignature(sig).encodeBase64())
|
||||
}
|
||||
} else {
|
||||
for (sig in signingInfo.signingCertificateHistory) {
|
||||
signatures.add(hashSignature(sig).encodeBase64())
|
||||
}
|
||||
}
|
||||
return signatures
|
||||
}
|
||||
|
||||
private fun hashSignature(signature: Signature): ByteArray {
|
||||
try {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.update(signature.toByteArray())
|
||||
return digest.digest()
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Log.e(TAG, "No SHA-256 algorithm found!", e)
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun signaturesChanged(packageMetadata: PackageMetadata, signatures: List<String>): Boolean {
|
||||
// no signatures in package metadata counts as them not having changed
|
||||
if (packageMetadata.signatures == null) return false
|
||||
// TODO this is probably more complicated, need to verify
|
||||
// 1. multiple signers: need to match all signatures in list
|
||||
// 2. single signer (with or without history): the intersection of both lists must not be empty.
|
||||
// TODO to support multiple signers check if lists differ
|
||||
return packageMetadata.signatures.intersect(signatures).isEmpty()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Base64 encoded SHA-256 signature hashes.
|
||||
*/
|
||||
fun SigningInfo.getSignatures(): List<String> {
|
||||
return if (hasMultipleSigners()) {
|
||||
apkContentsSigners.map { signature ->
|
||||
hashSignature(signature).encodeBase64()
|
||||
}
|
||||
} else {
|
||||
signingCertificateHistory.map { signature ->
|
||||
hashSignature(signature).encodeBase64()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hashSignature(signature: Signature): ByteArray {
|
||||
return computeSha256DigestBytes(signature.toByteArray()) ?: throw AssertionError()
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.content.pm.PackageInfo
|
|||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
@ -24,6 +25,7 @@ internal class BackupCoordinator(
|
|||
private val kv: KVBackup,
|
||||
private val full: FullBackup,
|
||||
private val apkBackup: ApkBackup,
|
||||
private val clock: Clock,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val nm: BackupNotificationManager) {
|
||||
|
@ -48,7 +50,7 @@ internal class BackupCoordinator(
|
|||
* for example, if there is no current live data-set at all,
|
||||
* or there is no authenticated account under which to store the data remotely -
|
||||
* the transport should return [TRANSPORT_OK] here
|
||||
* and treat the initializeDevice() / finishBackup() pair as a graceful no-op.
|
||||
* and treat the [initializeDevice] / [finishBackup] pair as a graceful no-op.
|
||||
*
|
||||
* @return One of [TRANSPORT_OK] (OK so far) or
|
||||
* [TRANSPORT_ERROR] (to retry following network error or other failure).
|
||||
|
@ -56,8 +58,13 @@ internal class BackupCoordinator(
|
|||
fun initializeDevice(): Int {
|
||||
Log.i(TAG, "Initialize Device!")
|
||||
return try {
|
||||
plugin.initializeDevice()
|
||||
metadataManager.onDeviceInitialization(plugin.getMetadataOutputStream())
|
||||
val token = clock.time()
|
||||
if (plugin.initializeDevice(token)) {
|
||||
Log.d(TAG, "Resetting backup metadata...")
|
||||
metadataManager.onDeviceInitialization(token, plugin.getMetadataOutputStream())
|
||||
} else {
|
||||
Log.d(TAG, "Storage was already initialized, doing no-op")
|
||||
}
|
||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||
// so we remember that we initialized successfully
|
||||
calledInitialize = true
|
||||
|
|
|
@ -8,5 +8,5 @@ val backupModule = module {
|
|||
single { ApkBackup(androidContext().packageManager, get(), get(), get()) }
|
||||
single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get()) }
|
||||
single { FullBackup(get<BackupPlugin>().fullBackupPlugin, get(), get(), get()) }
|
||||
single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
}
|
||||
|
|
|
@ -12,9 +12,12 @@ interface BackupPlugin {
|
|||
|
||||
/**
|
||||
* Initialize the storage for this device, erasing all stored data.
|
||||
*
|
||||
* @return true if the device needs initialization or
|
||||
* false if the device was initialized already and initialization should be a no-op.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun initializeDevice()
|
||||
fun initializeDevice(newToken: Long): Boolean
|
||||
|
||||
/**
|
||||
* Returns an [OutputStream] for writing backup metadata.
|
||||
|
|
|
@ -18,7 +18,6 @@ import com.stevesoltys.seedvault.settings.BackupManagerSettings
|
|||
import com.stevesoltys.seedvault.settings.FlashDrive
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.settings.Storage
|
||||
import com.stevesoltys.seedvault.transport.ConfigurableBackupTransportService
|
||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||
|
||||
|
@ -107,9 +106,6 @@ internal abstract class StorageViewModel(
|
|||
BackupManagerSettings.enableAutomaticBackups(app.contentResolver)
|
||||
}
|
||||
|
||||
// stop backup service to be sure the old location will get updated
|
||||
app.stopService(Intent(app, ConfigurableBackupTransportService::class.java))
|
||||
|
||||
Log.d(TAG, "New storage location saved: $uri")
|
||||
|
||||
return storage.isUsb
|
||||
|
|
|
@ -30,8 +30,9 @@ class MetadataManagerTest {
|
|||
private val manager = MetadataManager(context, clock, metadataWriter, metadataReader)
|
||||
|
||||
private val time = 42L
|
||||
private val token = Random.nextLong()
|
||||
private val packageName = getRandomString()
|
||||
private val initialMetadata = BackupMetadata(token = time)
|
||||
private val initialMetadata = BackupMetadata(token = token)
|
||||
private val storageOutputStream = ByteArrayOutputStream()
|
||||
private val cacheOutputStream: FileOutputStream = mockk()
|
||||
private val cacheInputStream: FileInputStream = mockk()
|
||||
|
@ -48,9 +49,9 @@ class MetadataManagerTest {
|
|||
every { metadataWriter.write(initialMetadata, storageOutputStream) } just Runs
|
||||
expectWriteToCache(initialMetadata)
|
||||
|
||||
manager.onDeviceInitialization(storageOutputStream)
|
||||
manager.onDeviceInitialization(token, storageOutputStream)
|
||||
|
||||
assertEquals(time, manager.getBackupToken())
|
||||
assertEquals(token, manager.getBackupToken())
|
||||
assertEquals(0L, manager.getLastBackupTime())
|
||||
}
|
||||
|
||||
|
@ -63,8 +64,7 @@ class MetadataManagerTest {
|
|||
signatures = listOf("sig")
|
||||
)
|
||||
|
||||
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
|
||||
every { clock.time() } returns time
|
||||
expectReadFromCache()
|
||||
|
||||
manager.onApkBackedUp(packageName, packageMetadata)
|
||||
|
||||
|
@ -73,14 +73,13 @@ class MetadataManagerTest {
|
|||
|
||||
@Test
|
||||
fun `test onApkBackedUp() with existing package metadata`() {
|
||||
val cachedMetadata = initialMetadata.copy()
|
||||
val packageMetadata = PackageMetadata(
|
||||
time = time,
|
||||
version = Random.nextLong(Long.MAX_VALUE),
|
||||
installer = getRandomString(),
|
||||
signatures = listOf("sig")
|
||||
)
|
||||
cachedMetadata.packageMetadata[packageName] = packageMetadata
|
||||
initialMetadata.packageMetadataMap[packageName] = packageMetadata
|
||||
val updatedPackageMetadata = PackageMetadata(
|
||||
time = time + 1,
|
||||
version = packageMetadata.version!! + 1,
|
||||
|
@ -88,7 +87,7 @@ class MetadataManagerTest {
|
|||
signatures = listOf("sig foo")
|
||||
)
|
||||
|
||||
expectReadFromCache(cachedMetadata)
|
||||
expectReadFromCache()
|
||||
|
||||
manager.onApkBackedUp(packageName, updatedPackageMetadata)
|
||||
|
||||
|
@ -99,9 +98,9 @@ class MetadataManagerTest {
|
|||
fun `test onPackageBackedUp()`() {
|
||||
val updatedMetadata = initialMetadata.copy()
|
||||
updatedMetadata.time = time
|
||||
updatedMetadata.packageMetadata[packageName] = PackageMetadata(time)
|
||||
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(time)
|
||||
|
||||
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
|
||||
expectReadFromCache()
|
||||
every { clock.time() } returns time
|
||||
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs
|
||||
expectWriteToCache(updatedMetadata)
|
||||
|
@ -116,10 +115,10 @@ class MetadataManagerTest {
|
|||
val updateTime = time + 1
|
||||
val updatedMetadata = initialMetadata.copy()
|
||||
updatedMetadata.time = updateTime
|
||||
updatedMetadata.packageMetadata[packageName] = PackageMetadata(updateTime)
|
||||
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(updateTime)
|
||||
|
||||
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
|
||||
every { clock.time() } returns time andThen updateTime
|
||||
expectReadFromCache()
|
||||
every { clock.time() } returns updateTime
|
||||
every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException()
|
||||
|
||||
try {
|
||||
|
@ -130,7 +129,7 @@ class MetadataManagerTest {
|
|||
}
|
||||
|
||||
assertEquals(0L, manager.getLastBackupTime()) // time was reverted
|
||||
assertEquals(initialMetadata.packageMetadata[packageName], manager.getPackageMetadata(packageName))
|
||||
assertEquals(initialMetadata.packageMetadataMap[packageName], manager.getPackageMetadata(packageName))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -139,14 +138,14 @@ class MetadataManagerTest {
|
|||
|
||||
val cacheTime = time - 1
|
||||
val cachedMetadata = initialMetadata.copy(time = cacheTime)
|
||||
cachedMetadata.packageMetadata[cachedPackageName] = PackageMetadata(cacheTime)
|
||||
cachedMetadata.packageMetadata[packageName] = PackageMetadata(cacheTime)
|
||||
cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(cacheTime)
|
||||
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime)
|
||||
|
||||
val updatedMetadata = cachedMetadata.copy(time = time)
|
||||
cachedMetadata.packageMetadata[cachedPackageName] = PackageMetadata(time)
|
||||
cachedMetadata.packageMetadata[packageName] = PackageMetadata(time)
|
||||
cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
|
||||
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(time)
|
||||
|
||||
expectReadFromCache(cachedMetadata)
|
||||
expectReadFromCache()
|
||||
every { clock.time() } returns time
|
||||
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs
|
||||
expectWriteToCache(updatedMetadata)
|
||||
|
@ -158,18 +157,42 @@ class MetadataManagerTest {
|
|||
assertEquals(PackageMetadata(time), manager.getPackageMetadata(packageName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getBackupToken() on first run`() {
|
||||
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
|
||||
|
||||
assertEquals(0L, manager.getBackupToken())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getLastBackupTime() on first run`() {
|
||||
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
|
||||
|
||||
assertEquals(0L, manager.getLastBackupTime())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getLastBackupTime() and getBackupToken() with cached metadata`() {
|
||||
initialMetadata.time = Random.nextLong()
|
||||
|
||||
expectReadFromCache()
|
||||
|
||||
assertEquals(initialMetadata.time, manager.getLastBackupTime())
|
||||
assertEquals(initialMetadata.token, manager.getBackupToken())
|
||||
}
|
||||
|
||||
private fun expectWriteToCache(metadata: BackupMetadata) {
|
||||
every { metadataWriter.encode(metadata) } returns encodedMetadata
|
||||
every { context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE) } returns cacheOutputStream
|
||||
every { cacheOutputStream.write(encodedMetadata) } just Runs
|
||||
}
|
||||
|
||||
private fun expectReadFromCache(metadata: BackupMetadata) {
|
||||
private fun expectReadFromCache() {
|
||||
val byteArray = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream
|
||||
every { cacheInputStream.available() } returns byteArray.size andThen 0
|
||||
every { cacheInputStream.read(byteArray) } returns -1
|
||||
every { metadataReader.decode(ByteArray(0)) } returns metadata
|
||||
every { metadataReader.decode(ByteArray(0)) } returns initialMetadata
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -84,6 +84,7 @@ class MetadataReaderTest {
|
|||
time = Random.nextLong(),
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
signatures = listOf(getRandomString(), getRandomString())
|
||||
))
|
||||
}
|
||||
|
@ -98,6 +99,7 @@ class MetadataReaderTest {
|
|||
json.put("org.example", JSONObject().apply {
|
||||
put(JSON_PACKAGE_VERSION, Random.nextLong())
|
||||
put(JSON_PACKAGE_INSTALLER, getRandomString())
|
||||
put(JSON_PACKAGE_SHA256, getRandomString())
|
||||
put(JSON_PACKAGE_SIGNATURES, JSONArray(listOf(getRandomString(), getRandomString())))
|
||||
})
|
||||
val jsonBytes = json.toString().toByteArray(Utf8)
|
||||
|
@ -115,8 +117,8 @@ class MetadataReaderTest {
|
|||
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() }
|
||||
assertEquals(1, result.packageMetadataMap.size)
|
||||
val packageMetadata = result.packageMetadataMap.getOrElse("org.example") { fail() }
|
||||
assertNull(packageMetadata.version)
|
||||
assertNull(packageMetadata.installer)
|
||||
assertNull(packageMetadata.signatures)
|
||||
|
@ -130,7 +132,7 @@ class MetadataReaderTest {
|
|||
androidVersion = Random.nextInt(),
|
||||
androidIncremental = getRandomString(),
|
||||
deviceName = getRandomString(),
|
||||
packageMetadata = packageMetadata
|
||||
packageMetadataMap = packageMetadata
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ internal class MetadataWriterDecoderTest {
|
|||
time = Random.nextLong(),
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
signatures = listOf(getRandomString(), getRandomString())))
|
||||
}
|
||||
val metadata = getMetadata(packages)
|
||||
|
@ -53,12 +54,14 @@ internal class MetadataWriterDecoderTest {
|
|||
time = Random.nextLong(),
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
signatures = listOf(getRandomString())
|
||||
))
|
||||
put(getRandomString(), PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
signatures = listOf(getRandomString(), getRandomString())
|
||||
))
|
||||
}
|
||||
|
@ -74,7 +77,7 @@ internal class MetadataWriterDecoderTest {
|
|||
androidVersion = Random.nextInt(),
|
||||
androidIncremental = getRandomString(),
|
||||
deviceName = getRandomString(),
|
||||
packageMetadata = packageMetadata
|
||||
packageMetadataMap = packageMetadata
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
||||
private val apkBackup = mockk<ApkBackup>()
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, apkBackup, metadataManager, settingsManager, notificationManager)
|
||||
private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, apkBackup, clock, metadataManager, settingsManager, notificationManager)
|
||||
|
||||
private val restorePlugin = mockk<RestorePlugin>()
|
||||
private val kvRestorePlugin = mockk<KVRestorePlugin>()
|
||||
|
|
|
@ -5,14 +5,11 @@ import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
|
|||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.Signature
|
||||
import android.util.PackageUtils
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.getRandomByteArray
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
|
@ -32,13 +29,18 @@ internal class ApkBackupTest : BackupTest() {
|
|||
private val apkBackup = ApkBackup(pm, clock, settingsManager, metadataManager)
|
||||
|
||||
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
|
||||
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
|
||||
private val sigs = arrayOf(Signature(signatureBytes))
|
||||
private val packageMetadata = PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
version = packageInfo.longVersionCode - 1,
|
||||
signatures = listOf("A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E")
|
||||
signatures = listOf("AwIB")
|
||||
)
|
||||
|
||||
init {
|
||||
mockkStatic(PackageUtils::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not back up @pm@`() {
|
||||
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
|
||||
|
@ -96,7 +98,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
|
||||
@Test
|
||||
fun `test successful APK backup`(@TempDir tmpDir: Path) {
|
||||
val apkBytes = getRandomByteArray()
|
||||
val apkBytes = byteArrayOf(0x04, 0x05, 0x06)
|
||||
val tmpFile = File(tmpDir.toAbsolutePath().toString())
|
||||
packageInfo.applicationInfo.sourceDir = File(tmpFile, "test.apk").apply {
|
||||
assertTrue(createNewFile())
|
||||
|
@ -107,6 +109,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
time = Random.nextLong(),
|
||||
version = packageInfo.longVersionCode,
|
||||
installer = getRandomString(),
|
||||
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
|
||||
signatures = packageMetadata.signatures
|
||||
)
|
||||
|
||||
|
@ -123,6 +126,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
private fun expectChecks(packageMetadata: PackageMetadata = this.packageMetadata) {
|
||||
every { settingsManager.backupApks() } returns true
|
||||
every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns packageMetadata
|
||||
every { PackageUtils.computeSha256DigestBytes(signatureBytes) } returns signatureHash
|
||||
every { sigInfo.hasMultipleSigners() } returns false
|
||||
every { sigInfo.signingCertificateHistory } returns sigs
|
||||
}
|
||||
|
|
|
@ -26,15 +26,27 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
private val apkBackup = mockk<ApkBackup>()
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
|
||||
private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, metadataManager, settingsManager, notificationManager)
|
||||
private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, clock, metadataManager, settingsManager, notificationManager)
|
||||
|
||||
private val metadataOutputStream = mockk<OutputStream>()
|
||||
|
||||
@Test
|
||||
fun `device initialization succeeds and delegates to plugin`() {
|
||||
every { plugin.initializeDevice() } just Runs
|
||||
every { clock.time() } returns token
|
||||
every { plugin.initializeDevice(token) } returns true // TODO test when false
|
||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onDeviceInitialization(metadataOutputStream) } just Runs
|
||||
every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs
|
||||
every { kv.hasState() } returns false
|
||||
every { full.hasState() } returns false
|
||||
|
||||
assertEquals(TRANSPORT_OK, backup.initializeDevice())
|
||||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `device initialization does no-op when already initialized`() {
|
||||
every { clock.time() } returns token
|
||||
every { plugin.initializeDevice(token) } returns false
|
||||
every { kv.hasState() } returns false
|
||||
every { full.hasState() } returns false
|
||||
|
||||
|
@ -46,7 +58,8 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
fun `error notification when device initialization fails`() {
|
||||
val storage = Storage(Uri.EMPTY, getRandomString(), false)
|
||||
|
||||
every { plugin.initializeDevice() } throws IOException()
|
||||
every { clock.time() } returns token
|
||||
every { plugin.initializeDevice(token) } throws IOException()
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
every { notificationManager.onBackupError() } just Runs
|
||||
|
||||
|
@ -65,7 +78,8 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
val storage = mockk<Storage>()
|
||||
val documentFile = mockk<DocumentFile>()
|
||||
|
||||
every { plugin.initializeDevice() } throws IOException()
|
||||
every { clock.time() } returns token
|
||||
every { plugin.initializeDevice(token) } throws IOException()
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
every { storage.isUsb } returns true
|
||||
every { storage.getDocumentFile(context) } returns documentFile
|
||||
|
|
Loading…
Reference in a new issue