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:
Torsten Grote 2024-05-23 18:19:36 -03:00
parent eecfcdb285
commit 332387fd58
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
6 changed files with 54 additions and 35 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,7 @@ val workerModule = module {
IconManager( IconManager(
context = androidContext(), context = androidContext(),
packageService = get(), packageService = get(),
crypto = get(),
) )
} }
single { single {

View file

@ -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() {