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:
Torsten Grote 2019-08-01 10:34:31 +02:00
parent 2ce625ac87
commit 1ee443a3d8
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
20 changed files with 108 additions and 36 deletions

View file

@ -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

View file

@ -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"

View file

@ -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)
}
}

View 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))
}

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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}"
}
}

View file

@ -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

View file

@ -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

View file

@ -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.
*/

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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.*

View file

@ -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

View file

@ -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

View file

@ -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

View 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>