Encrypt zip file with icons
While we still don't guarantee that an attacker with access to the storage can't find out which apps we use (APKs are still unencrypted after all), we go into this direction. Also, this should make it impossible for an attacker that can modify files to replace or otherwise mess with the icons.
This commit is contained in:
parent
eecfcdb285
commit
332387fd58
6 changed files with 54 additions and 35 deletions
|
@ -120,6 +120,7 @@ internal interface Crypto {
|
||||||
internal const val TYPE_METADATA: Byte = 0x00
|
internal const val TYPE_METADATA: Byte = 0x00
|
||||||
internal const val TYPE_BACKUP_KV: Byte = 0x01
|
internal const val TYPE_BACKUP_KV: Byte = 0x01
|
||||||
internal const val TYPE_BACKUP_FULL: Byte = 0x02
|
internal const val TYPE_BACKUP_FULL: Byte = 0x02
|
||||||
|
internal const val TYPE_ICONS: Byte = 0x03
|
||||||
|
|
||||||
internal class CryptoImpl(
|
internal class CryptoImpl(
|
||||||
private val keyManager: KeyManager,
|
private val keyManager: KeyManager,
|
||||||
|
|
|
@ -221,7 +221,7 @@ internal class RestoreViewModel(
|
||||||
val token = restorableBackup.token
|
val token = restorableBackup.token
|
||||||
val packagesWithIcons = try {
|
val packagesWithIcons = try {
|
||||||
plugin.getInputStream(token, FILE_BACKUP_ICONS).use {
|
plugin.getInputStream(token, FILE_BACKUP_ICONS).use {
|
||||||
iconManager.downloadIcons(it)
|
iconManager.downloadIcons(restorableBackup.version, token, it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error loading icons:", e)
|
Log.e(TAG, "Error loading icons:", e)
|
||||||
|
|
|
@ -101,7 +101,7 @@ internal class ApkBackupManager(
|
||||||
try {
|
try {
|
||||||
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
||||||
pluginManager.appPlugin.getOutputStream(token, FILE_BACKUP_ICONS).use {
|
pluginManager.appPlugin.getOutputStream(token, FILE_BACKUP_ICONS).use {
|
||||||
iconManager.uploadIcons(it)
|
iconManager.uploadIcons(token, it)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error uploading icons: ", e)
|
Log.e(TAG, "Error uploading icons: ", e)
|
||||||
|
|
|
@ -14,13 +14,19 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
|
import com.stevesoltys.seedvault.crypto.TYPE_ICONS
|
||||||
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
import java.util.zip.Deflater.BEST_SPEED
|
import java.util.zip.Deflater.BEST_SPEED
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
@ -35,33 +41,36 @@ private val TAG = IconManager::class.simpleName
|
||||||
internal class IconManager(
|
internal class IconManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val packageService: PackageService,
|
private val packageService: PackageService,
|
||||||
|
private val crypto: Crypto,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class, GeneralSecurityException::class)
|
||||||
fun uploadIcons(outputStream: OutputStream) {
|
fun uploadIcons(token: Long, outputStream: OutputStream) {
|
||||||
Log.d(TAG, "Start uploading icons")
|
Log.d(TAG, "Start uploading icons")
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
ZipOutputStream(outputStream).use { zip ->
|
crypto.newEncryptingStream(outputStream, getAD(VERSION, token)).use { cryptoStream ->
|
||||||
zip.setLevel(BEST_SPEED)
|
ZipOutputStream(cryptoStream).use { zip ->
|
||||||
val entries = mutableSetOf<String>()
|
zip.setLevel(BEST_SPEED)
|
||||||
packageService.allUserPackages.forEach {
|
val entries = mutableSetOf<String>()
|
||||||
val drawable = packageManager.getApplicationIcon(it.applicationInfo)
|
packageService.allUserPackages.forEach {
|
||||||
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
|
val drawable = packageManager.getApplicationIcon(it.applicationInfo)
|
||||||
val entry = ZipEntry(it.packageName)
|
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
|
||||||
zip.putNextEntry(entry)
|
val entry = ZipEntry(it.packageName)
|
||||||
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
|
zip.putNextEntry(entry)
|
||||||
entries.add(it.packageName)
|
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
|
||||||
zip.closeEntry()
|
entries.add(it.packageName)
|
||||||
}
|
zip.closeEntry()
|
||||||
packageService.launchableSystemApps.forEach {
|
}
|
||||||
val drawable = it.loadIcon(packageManager)
|
packageService.launchableSystemApps.forEach {
|
||||||
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
|
val drawable = it.loadIcon(packageManager)
|
||||||
// check for duplicates (e.g. updated launchable system app)
|
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
|
||||||
if (it.activityInfo.packageName in entries) return@forEach
|
// check for duplicates (e.g. updated launchable system app)
|
||||||
val entry = ZipEntry(it.activityInfo.packageName)
|
if (it.activityInfo.packageName in entries) return@forEach
|
||||||
zip.putNextEntry(entry)
|
val entry = ZipEntry(it.activityInfo.packageName)
|
||||||
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
|
zip.putNextEntry(entry)
|
||||||
zip.closeEntry()
|
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
|
||||||
|
zip.closeEntry()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Finished uploading icons")
|
Log.d(TAG, "Finished uploading icons")
|
||||||
|
@ -71,21 +80,23 @@ internal class IconManager(
|
||||||
* Downloads icons file from given [inputStream].
|
* Downloads icons file from given [inputStream].
|
||||||
* @return a set of package names for which icons were found
|
* @return a set of package names for which icons were found
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class, GeneralSecurityException::class)
|
||||||
fun downloadIcons(inputStream: InputStream): Set<String> {
|
fun downloadIcons(version: Byte, token: Long, inputStream: InputStream): Set<String> {
|
||||||
Log.d(TAG, "Start downloading icons")
|
Log.d(TAG, "Start downloading icons")
|
||||||
val folder = File(context.cacheDir, CACHE_FOLDER)
|
val folder = File(context.cacheDir, CACHE_FOLDER)
|
||||||
if (!folder.isDirectory && !folder.mkdirs())
|
if (!folder.isDirectory && !folder.mkdirs())
|
||||||
throw IOException("Can't create cache folder for icons")
|
throw IOException("Can't create cache folder for icons")
|
||||||
val set = mutableSetOf<String>()
|
val set = mutableSetOf<String>()
|
||||||
ZipInputStream(inputStream).use { zip ->
|
crypto.newDecryptingStream(inputStream, getAD(version, token)).use { cryptoStream ->
|
||||||
var entry = zip.nextEntry
|
ZipInputStream(cryptoStream).use { zip ->
|
||||||
while (entry != null) {
|
var entry = zip.nextEntry
|
||||||
File(folder, entry.name).outputStream().use { outputStream ->
|
while (entry != null) {
|
||||||
zip.copyTo(outputStream)
|
File(folder, entry.name).outputStream().use { outputStream ->
|
||||||
|
zip.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
set.add(entry.name)
|
||||||
|
entry = zip.nextEntry
|
||||||
}
|
}
|
||||||
set.add(entry.name)
|
|
||||||
entry = zip.nextEntry
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Finished downloading icons")
|
Log.d(TAG, "Finished downloading icons")
|
||||||
|
@ -122,4 +133,10 @@ internal class IconManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAD(version: Byte, token: Long) = ByteBuffer.allocate(2 + 8)
|
||||||
|
.put(version)
|
||||||
|
.put(TYPE_ICONS)
|
||||||
|
.put(token.toByteArray())
|
||||||
|
.array()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ val workerModule = module {
|
||||||
IconManager(
|
IconManager(
|
||||||
context = androidContext(),
|
context = androidContext(),
|
||||||
packageService = get(),
|
packageService = get(),
|
||||||
|
crypto = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
|
|
|
@ -249,7 +249,7 @@ internal class ApkBackupManagerTest : TransportTest() {
|
||||||
private suspend fun expectUploadIcons() {
|
private suspend fun expectUploadIcons() {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
coEvery { plugin.getOutputStream(token, FILE_BACKUP_ICONS) } returns stream
|
coEvery { plugin.getOutputStream(token, FILE_BACKUP_ICONS) } returns stream
|
||||||
every { iconManager.uploadIcons(stream) } just Runs
|
every { iconManager.uploadIcons(token, stream) } just Runs
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expectAllAppsWillGetBackedUp() {
|
private fun expectAllAppsWillGetBackedUp() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue