parent
fcf17fe23a
commit
a98364efbe
3 changed files with 24 additions and 15 deletions
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault.crypto
|
package com.stevesoltys.seedvault.crypto
|
||||||
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
||||||
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
||||||
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
|
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
|
||||||
|
@ -48,7 +47,7 @@ internal class KeyManagerImpl : KeyManager {
|
||||||
|
|
||||||
override fun storeBackupKey(seed: ByteArray) {
|
override fun storeBackupKey(seed: ByteArray) {
|
||||||
if (seed.size < KEY_SIZE_BYTES) throw IllegalArgumentException()
|
if (seed.size < KEY_SIZE_BYTES) throw IllegalArgumentException()
|
||||||
// TODO check if using first 256 of 512 bytes produced by PBKDF2WithHmacSHA512 is safe!
|
// TODO check if using first 256 of 512 bits produced by PBKDF2WithHmacSHA512 is safe!
|
||||||
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE_BYTES, "AES")
|
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE_BYTES, "AES")
|
||||||
val ksEntry = SecretKeyEntry(secretKeySpec)
|
val ksEntry = SecretKeyEntry(secretKeySpec)
|
||||||
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
||||||
|
@ -68,7 +67,7 @@ internal class KeyManagerImpl : KeyManager {
|
||||||
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
||||||
.setRandomizedEncryptionRequired(true)
|
.setRandomizedEncryptionRequired(true)
|
||||||
// unlocking is required only for decryption, so when restoring from backup
|
// unlocking is required only for decryption, so when restoring from backup
|
||||||
if (SDK_INT >= 28) builder.setUnlockedDeviceRequired(true)
|
builder.setUnlockedDeviceRequired(true)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
|
@ -94,7 +95,20 @@ internal class BackupCoordinator(
|
||||||
return targetPackage.packageName != plugin.providerPackageName
|
return targetPackage.packageName != plugin.providerPackageName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the transport about current quota for backup size of the package.
|
||||||
|
*
|
||||||
|
* @param packageName ID of package to provide the quota.
|
||||||
|
* @param isFullBackup If set, transport should return limit for full data backup,
|
||||||
|
* otherwise for key-value backup.
|
||||||
|
* @return Current limit on backup size in bytes.
|
||||||
|
*/
|
||||||
fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
|
fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
|
||||||
|
// try to back up APK here as later methods are sometimes not called called
|
||||||
|
val pm = context.packageManager
|
||||||
|
backUpApk(pm.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES))
|
||||||
|
|
||||||
|
// report back quota
|
||||||
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")
|
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")
|
||||||
val quota = if (isFullBackup) full.getQuota() else kv.getQuota()
|
val quota = if (isFullBackup) full.getQuota() else kv.getQuota()
|
||||||
Log.i(TAG, "Reported quota of $quota bytes.")
|
Log.i(TAG, "Reported quota of $quota bytes.")
|
||||||
|
@ -132,8 +146,7 @@ internal class BackupCoordinator(
|
||||||
// hook in here to back up APKs of apps that are otherwise not allowed for backup
|
// hook in here to back up APKs of apps that are otherwise not allowed for backup
|
||||||
backUpNotAllowedPackages()
|
backUpNotAllowedPackages()
|
||||||
}
|
}
|
||||||
val result = kv.performBackup(packageInfo, data, flags)
|
return kv.performBackup(packageInfo, data, flags)
|
||||||
return backUpApk(result, packageInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
|
@ -166,8 +179,7 @@ internal class BackupCoordinator(
|
||||||
|
|
||||||
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
||||||
cancelReason = UNKNOWN_ERROR
|
cancelReason = UNKNOWN_ERROR
|
||||||
val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
|
return full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||||
return backUpApk(result, targetPackage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
||||||
|
@ -254,27 +266,24 @@ internal class BackupCoordinator(
|
||||||
Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
|
Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
|
||||||
packageService.notAllowedPackages.forEach { optOutPackageInfo ->
|
packageService.notAllowedPackages.forEach { optOutPackageInfo ->
|
||||||
try {
|
try {
|
||||||
backUpApk(0, optOutPackageInfo, NOT_ALLOWED)
|
backUpApk(optOutPackageInfo, NOT_ALLOWED)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error backing up opt-out APK of ${optOutPackageInfo.packageName}", e)
|
Log.e(TAG, "Error backing up opt-out APK of ${optOutPackageInfo.packageName}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backUpApk(result: Int, packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR): Int {
|
private fun backUpApk(packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR) {
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER) return result
|
try {
|
||||||
return try {
|
|
||||||
apkBackup.backupApkIfNecessary(packageInfo, packageState) {
|
apkBackup.backupApkIfNecessary(packageInfo, packageState) {
|
||||||
plugin.getApkOutputStream(packageInfo)
|
plugin.getApkOutputStream(packageInfo)
|
||||||
}?.let { packageMetadata ->
|
}?.let { packageMetadata ->
|
||||||
val outputStream = plugin.getMetadataOutputStream()
|
val outputStream = plugin.getMetadataOutputStream()
|
||||||
metadataManager.onApkBackedUp(packageInfo, packageMetadata, outputStream)
|
metadataManager.onApkBackedUp(packageInfo, packageMetadata, outputStream)
|
||||||
}
|
}
|
||||||
result
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error while writing APK or metadata for $packageName", e)
|
Log.e(TAG, "Error while writing APK or metadata for $packageName", e)
|
||||||
TRANSPORT_PACKAGE_REJECTED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
val isFullBackup = Random.nextBoolean()
|
val isFullBackup = Random.nextBoolean()
|
||||||
val quota = Random.nextLong()
|
val quota = Random.nextLong()
|
||||||
|
|
||||||
|
expectApkBackupAndMetadataWrite()
|
||||||
if (isFullBackup) {
|
if (isFullBackup) {
|
||||||
every { full.getQuota() } returns quota
|
every { full.getQuota() } returns quota
|
||||||
} else {
|
} else {
|
||||||
|
@ -264,9 +265,9 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expectApkBackupAndMetadataWrite() {
|
private fun expectApkBackupAndMetadataWrite() {
|
||||||
every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
|
every { apkBackup.backupApkIfNecessary(any(), UNKNOWN_ERROR, any()) } returns packageMetadata
|
||||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs
|
every { metadataManager.onApkBackedUp(any(), packageMetadata, metadataOutputStream) } just Runs
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue