Add a unique ID to the device folder name to avoid collisions
when using several devices of the same model with the same account
This commit is contained in:
parent
2ce625ac87
commit
1ee443a3d8
20 changed files with 108 additions and 36 deletions
|
@ -1,5 +1,13 @@
|
|||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := default-permissions_com.stevesoltys.backup.xml
|
||||
LOCAL_MODULE_CLASS := ETC
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/default-permissions
|
||||
LOCAL_SRC_FILES := $(LOCAL_MODULE)
|
||||
include $(BUILD_PREBUILT)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := permissions_com.stevesoltys.backup.xml
|
||||
LOCAL_MODULE_CLASS := ETC
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
android:name="android.permission.BACKUP"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<!-- This is needed to retrieve the serial number of the device,
|
||||
so we can store the backups for each device in a unique location -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".Backup"
|
||||
|
@ -35,7 +37,7 @@
|
|||
|
||||
<activity
|
||||
android:name="com.stevesoltys.backup.activity.MainActivity"
|
||||
android:label="@string/app_name"/>
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<activity
|
||||
android:name="com.stevesoltys.backup.activity.backup.CreateBackupActivity"
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
package com.stevesoltys.backup
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission.READ_PHONE_STATE
|
||||
import android.app.Application
|
||||
import android.app.backup.IBackupManager
|
||||
import android.content.Context.BACKUP_SERVICE
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Build
|
||||
import android.os.ServiceManager.getService
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.crypto.KeyManager
|
||||
import com.stevesoltys.backup.crypto.KeyManagerImpl
|
||||
import com.stevesoltys.backup.settings.getDeviceName
|
||||
import com.stevesoltys.backup.settings.setDeviceName
|
||||
import io.github.novacrypto.hashing.Sha256
|
||||
import io.github.novacrypto.hashing.Sha256.sha256
|
||||
import io.github.novacrypto.hashing.Sha256.sha256Twice
|
||||
import java.lang.AssertionError
|
||||
|
||||
const val JOB_ID_BACKGROUND_BACKUP = 1
|
||||
|
||||
private val TAG = Backup::class.java.simpleName
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
* @author Torsten Grote
|
||||
|
@ -24,4 +37,25 @@ class Backup : Application() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
storeDeviceName()
|
||||
}
|
||||
|
||||
private fun storeDeviceName() {
|
||||
if (getDeviceName(this) != null) return // we already have a stored device name
|
||||
|
||||
val permission = READ_PHONE_STATE
|
||||
if (checkSelfPermission(permission) != PERMISSION_GRANTED) {
|
||||
throw AssertionError("You need to grant the $permission permission.")
|
||||
}
|
||||
// TODO consider just using a hash for the entire device name and store metadata in an encrypted file
|
||||
val id = sha256Twice(Build.getSerial().toByteArray(Utf8))
|
||||
.copyOfRange(0, 8)
|
||||
.encodeBase64()
|
||||
val name = "${Build.MANUFACTURER} ${Build.MODEL} ($id)"
|
||||
Log.i(TAG, "Initialized device name to: $name")
|
||||
setDeviceName(this, name)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
18
app/src/main/java/com/stevesoltys/backup/Base64Utils.kt
Normal file
18
app/src/main/java/com/stevesoltys/backup/Base64Utils.kt
Normal file
|
@ -0,0 +1,18 @@
|
|||
package com.stevesoltys.backup
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
val Utf8: Charset = Charset.forName("UTF-8")
|
||||
|
||||
fun ByteArray.encodeBase64(): String {
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(this)
|
||||
}
|
||||
|
||||
fun String.encodeBase64(): String {
|
||||
return toByteArray(Utf8).encodeBase64()
|
||||
}
|
||||
|
||||
fun String.decodeBase64(): String {
|
||||
return String(Base64.getUrlDecoder().decode(this))
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package com.stevesoltys.backup.header
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
internal const val VERSION: Byte = 0
|
||||
internal const val MAX_PACKAGE_LENGTH_SIZE = 255
|
||||
internal const val MAX_KEY_LENGTH_SIZE = MAX_PACKAGE_LENGTH_SIZE
|
||||
|
@ -39,5 +37,3 @@ class SegmentHeader(
|
|||
check(nonce.size == IV_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
val Utf8: Charset = Charset.forName("UTF-8")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.backup.header
|
||||
|
||||
import com.stevesoltys.backup.Utf8
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.backup.header
|
||||
|
||||
import com.stevesoltys.backup.Utf8
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
|
|
@ -5,8 +5,8 @@ import android.net.Uri
|
|||
import android.preference.PreferenceManager.getDefaultSharedPreferences
|
||||
|
||||
private const val PREF_KEY_BACKUP_URI = "backupUri"
|
||||
private const val PREF_KEY_DEVICE_NAME = "deviceName"
|
||||
private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword"
|
||||
private const val PREF_KEY_BACKUPS_SCHEDULED = "backupsScheduled"
|
||||
|
||||
fun setBackupFolderUri(context: Context, uri: Uri) {
|
||||
getDefaultSharedPreferences(context)
|
||||
|
@ -21,6 +21,17 @@ fun getBackupFolderUri(context: Context): Uri? {
|
|||
return Uri.parse(uriStr)
|
||||
}
|
||||
|
||||
fun setDeviceName(context: Context, name: String) {
|
||||
getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString(PREF_KEY_DEVICE_NAME, name)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getDeviceName(context: Context): String? {
|
||||
return getDefaultSharedPreferences(context).getString(PREF_KEY_DEVICE_NAME, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is insecure and not supposed to be part of a release,
|
||||
* but rather an intermediate step towards a generated passphrase.
|
||||
|
@ -38,6 +49,7 @@ fun getBackupPassword(context: Context): String? {
|
|||
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null)
|
||||
}
|
||||
|
||||
@Deprecated("Our own scheduling will be removed")
|
||||
fun setBackupsScheduled(context: Context) {
|
||||
getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
|
@ -45,6 +57,7 @@ fun setBackupsScheduled(context: Context) {
|
|||
.apply()
|
||||
}
|
||||
|
||||
@Deprecated("Our own scheduling will be removed")
|
||||
fun areBackupsScheduled(context: Context): Boolean {
|
||||
return getDefaultSharedPreferences(context).getBoolean(PREF_KEY_BACKUPS_SCHEDULED, false)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package com.stevesoltys.backup.transport
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.crypto.CipherFactoryImpl
|
||||
import com.stevesoltys.backup.crypto.CryptoImpl
|
||||
import com.stevesoltys.backup.header.HeaderReaderImpl
|
||||
import com.stevesoltys.backup.header.HeaderWriterImpl
|
||||
import com.stevesoltys.backup.settings.getBackupFolderUri
|
||||
import com.stevesoltys.backup.settings.getDeviceName
|
||||
import com.stevesoltys.backup.transport.backup.BackupCoordinator
|
||||
import com.stevesoltys.backup.transport.backup.FullBackup
|
||||
import com.stevesoltys.backup.transport.backup.InputFactory
|
||||
|
@ -24,7 +24,7 @@ class PluginManager(context: Context) {
|
|||
|
||||
// We can think about using an injection framework such as Dagger to simplify this.
|
||||
|
||||
private val storage = DocumentsStorage(context, getBackupFolderUri(context), getDeviceName())
|
||||
private val storage = DocumentsStorage(context, getBackupFolderUri(context), getDeviceName(context)!!)
|
||||
|
||||
private val headerWriter = HeaderWriterImpl()
|
||||
private val headerReader = HeaderReaderImpl()
|
||||
|
@ -47,10 +47,4 @@ class PluginManager(context: Context) {
|
|||
|
||||
internal val restoreCoordinator = RestoreCoordinator(restorePlugin, kvRestore, fullRestore)
|
||||
|
||||
|
||||
private fun getDeviceName(): String {
|
||||
// TODO add device specific unique ID to the end
|
||||
return "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ interface FullBackupPlugin {
|
|||
|
||||
fun getQuota(): Long
|
||||
|
||||
// TODO consider using a salted hash for the package name to not leak it to the storage server
|
||||
@Throws(IOException::class)
|
||||
fun getOutputStream(targetPackage: PackageInfo): OutputStream
|
||||
|
||||
|
|
|
@ -5,12 +5,11 @@ import android.content.pm.PackageInfo
|
|||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.crypto.Crypto
|
||||
import com.stevesoltys.backup.encodeBase64
|
||||
import com.stevesoltys.backup.header.HeaderWriter
|
||||
import com.stevesoltys.backup.header.Utf8
|
||||
import com.stevesoltys.backup.header.VersionHeader
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
import java.io.IOException
|
||||
import java.util.Base64.getUrlEncoder
|
||||
|
||||
class KVBackupState(internal val packageName: String)
|
||||
|
||||
|
@ -139,7 +138,7 @@ class KVBackup(
|
|||
}
|
||||
// encode key
|
||||
val key = changeSet.key
|
||||
val base64Key = getUrlEncoder().withoutPadding().encodeToString(key.toByteArray(Utf8))
|
||||
val base64Key = key.encodeBase64()
|
||||
val dataSize = changeSet.dataSize
|
||||
|
||||
// read and encrypt value
|
||||
|
|
|
@ -11,6 +11,7 @@ interface KVBackupPlugin {
|
|||
*/
|
||||
fun getQuota(): Long
|
||||
|
||||
// TODO consider using a salted hash for the package name (and key) to not leak it to the storage server
|
||||
/**
|
||||
* Return true if there are records stored for the given package.
|
||||
*/
|
||||
|
|
|
@ -6,8 +6,6 @@ import com.stevesoltys.backup.transport.backup.FullBackupPlugin
|
|||
import com.stevesoltys.backup.transport.backup.KVBackupPlugin
|
||||
import java.io.IOException
|
||||
|
||||
private const val NO_MEDIA = ".nomedia"
|
||||
|
||||
class DocumentsProviderBackupPlugin(
|
||||
private val storage: DocumentsStorage,
|
||||
packageManager: PackageManager) : BackupPlugin {
|
||||
|
@ -25,9 +23,6 @@ class DocumentsProviderBackupPlugin(
|
|||
// get or create root backup dir
|
||||
val rootDir = storage.rootBackupDir ?: throw IOException()
|
||||
|
||||
// create .nomedia file to prevent Android's MediaScanner from trying to index the backup
|
||||
rootDir.createOrGetFile(NO_MEDIA)
|
||||
|
||||
// create backup folders
|
||||
val kvDir = storage.defaultKvBackupDir
|
||||
val fullDir = storage.defaultFullBackupDir
|
||||
|
|
|
@ -13,6 +13,7 @@ import java.io.OutputStream
|
|||
const val DIRECTORY_FULL_BACKUP = "full"
|
||||
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
||||
private const val ROOT_DIR_NAME = ".AndroidBackup"
|
||||
private const val NO_MEDIA = ".nomedia"
|
||||
private const val MIME_TYPE = "application/octet-stream"
|
||||
|
||||
private val TAG = DocumentsStorage::class.java.simpleName
|
||||
|
@ -25,7 +26,10 @@ class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String)
|
|||
val folderUri = parentFolder ?: return@lazy null
|
||||
val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError()
|
||||
try {
|
||||
parent.createOrGetDirectory(ROOT_DIR_NAME)
|
||||
val rootDir = parent.createOrGetDirectory(ROOT_DIR_NAME)
|
||||
// create .nomedia file to prevent Android's MediaScanner from trying to index the backup
|
||||
rootDir.createOrGetFile(NO_MEDIA)
|
||||
rootDir
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating root backup dir.", e)
|
||||
null
|
||||
|
|
|
@ -7,12 +7,12 @@ import android.content.pm.PackageInfo
|
|||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.crypto.Crypto
|
||||
import com.stevesoltys.backup.decodeBase64
|
||||
import com.stevesoltys.backup.header.HeaderReader
|
||||
import com.stevesoltys.backup.header.UnsupportedVersionException
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Base64.getUrlDecoder
|
||||
|
||||
private class KVRestoreState(
|
||||
internal val token: Long,
|
||||
|
@ -132,7 +132,7 @@ internal class KVRestore(
|
|||
}
|
||||
|
||||
private class DecodedKey(internal val base64Key: String) : Comparable<DecodedKey> {
|
||||
internal val key = String(getUrlDecoder().decode(base64Key))
|
||||
internal val key = base64Key.decodeBase64()
|
||||
|
||||
override fun compareTo(other: DecodedKey) = key.compareTo(other.key)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.backup.header
|
||||
|
||||
import com.stevesoltys.backup.Utf8
|
||||
import com.stevesoltys.backup.assertContains
|
||||
import com.stevesoltys.backup.getRandomString
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
|
|
|
@ -10,17 +10,16 @@ import android.os.ParcelFileDescriptor
|
|||
import com.stevesoltys.backup.crypto.CipherFactoryImpl
|
||||
import com.stevesoltys.backup.crypto.CryptoImpl
|
||||
import com.stevesoltys.backup.crypto.KeyManagerTestImpl
|
||||
import com.stevesoltys.backup.transport.backup.*
|
||||
import com.stevesoltys.backup.encodeBase64
|
||||
import com.stevesoltys.backup.header.HeaderReaderImpl
|
||||
import com.stevesoltys.backup.header.HeaderWriterImpl
|
||||
import com.stevesoltys.backup.header.Utf8
|
||||
import com.stevesoltys.backup.transport.backup.*
|
||||
import com.stevesoltys.backup.transport.restore.*
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class CoordinatorIntegrationTest : TransportTest() {
|
||||
|
@ -53,9 +52,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val appData = ByteArray(42).apply { Random.nextBytes(this) }
|
||||
private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) }
|
||||
private val key = "RestoreKey"
|
||||
private val key64 = Base64.getUrlEncoder().withoutPadding().encodeToString(key.toByteArray(Utf8))
|
||||
private val key64 = key.encodeBase64()
|
||||
private val key2 = "RestoreKey2"
|
||||
private val key264 = Base64.getUrlEncoder().withoutPadding().encodeToString(key2.toByteArray(Utf8))
|
||||
private val key264 = key2.encodeBase64()
|
||||
|
||||
init {
|
||||
every { backupPlugin.kvBackupPlugin } returns kvBackupPlugin
|
||||
|
|
|
@ -2,9 +2,9 @@ package com.stevesoltys.backup.transport.backup
|
|||
|
||||
import android.app.backup.BackupDataInput
|
||||
import android.app.backup.BackupTransport.*
|
||||
import com.stevesoltys.backup.Utf8
|
||||
import com.stevesoltys.backup.getRandomString
|
||||
import com.stevesoltys.backup.header.MAX_KEY_LENGTH_SIZE
|
||||
import com.stevesoltys.backup.header.Utf8
|
||||
import com.stevesoltys.backup.header.VersionHeader
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
|
|
|
@ -3,9 +3,9 @@ package com.stevesoltys.backup.transport.restore
|
|||
import android.app.backup.BackupDataOutput
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import com.stevesoltys.backup.encodeBase64
|
||||
import com.stevesoltys.backup.getRandomByteArray
|
||||
import com.stevesoltys.backup.header.UnsupportedVersionException
|
||||
import com.stevesoltys.backup.header.Utf8
|
||||
import com.stevesoltys.backup.header.VERSION
|
||||
import com.stevesoltys.backup.header.VersionHeader
|
||||
import io.mockk.*
|
||||
|
@ -14,7 +14,6 @@ import org.junit.jupiter.api.Assertions.assertThrows
|
|||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.Base64.getUrlEncoder
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class KVRestoreTest : RestoreTest() {
|
||||
|
@ -24,10 +23,10 @@ internal class KVRestoreTest : RestoreTest() {
|
|||
private val restore = KVRestore(plugin, outputFactory, headerReader, crypto)
|
||||
|
||||
private val key = "Restore Key"
|
||||
private val key64 = getUrlEncoder().withoutPadding().encodeToString(key.toByteArray(Utf8))
|
||||
private val key64 = key.encodeBase64()
|
||||
private val versionHeader = VersionHeader(VERSION, packageInfo.packageName, key)
|
||||
private val key2 = "Restore Key2"
|
||||
private val key264 = getUrlEncoder().withoutPadding().encodeToString(key2.toByteArray(Utf8))
|
||||
private val key264 = key2.encodeBase64()
|
||||
private val versionHeader2 = VersionHeader(VERSION, packageInfo.packageName, key2)
|
||||
|
||||
@Test
|
||||
|
|
6
default-permissions_com.stevesoltys.backup.xml
Normal file
6
default-permissions_com.stevesoltys.backup.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<exceptions>
|
||||
<exception package="com.stevesoltys.backup">
|
||||
<permission name="android.permission.READ_PHONE_STATE" fixed="true"/>
|
||||
</exception>
|
||||
</exceptions>
|
Loading…
Reference in a new issue