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_BACKUP_KV: Byte = 0x01
internal const val TYPE_BACKUP_FULL: Byte = 0x02
internal const val TYPE_ICONS: Byte = 0x03
internal class CryptoImpl(
private val keyManager: KeyManager,

View file

@ -221,7 +221,7 @@ internal class RestoreViewModel(
val token = restorableBackup.token
val packagesWithIcons = try {
plugin.getInputStream(token, FILE_BACKUP_ICONS).use {
iconManager.downloadIcons(it)
iconManager.downloadIcons(restorableBackup.version, token, it)
}
} catch (e: Exception) {
Log.e(TAG, "Error loading icons:", e)

View file

@ -101,7 +101,7 @@ internal class ApkBackupManager(
try {
val token = settingsManager.getToken() ?: throw IOException("no current token")
pluginManager.appPlugin.getOutputStream(token, FILE_BACKUP_ICONS).use {
iconManager.uploadIcons(it)
iconManager.uploadIcons(token, it)
}
} catch (e: IOException) {
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.toDrawable
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.security.GeneralSecurityException
import java.util.zip.Deflater.BEST_SPEED
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@ -35,33 +41,36 @@ private val TAG = IconManager::class.simpleName
internal class IconManager(
private val context: Context,
private val packageService: PackageService,
private val crypto: Crypto,
) {
@Throws(IOException::class)
fun uploadIcons(outputStream: OutputStream) {
@Throws(IOException::class, GeneralSecurityException::class)
fun uploadIcons(token: Long, outputStream: OutputStream) {
Log.d(TAG, "Start uploading icons")
val packageManager = context.packageManager
ZipOutputStream(outputStream).use { zip ->
zip.setLevel(BEST_SPEED)
val entries = mutableSetOf<String>()
packageService.allUserPackages.forEach {
val drawable = packageManager.getApplicationIcon(it.applicationInfo)
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
val entry = ZipEntry(it.packageName)
zip.putNextEntry(entry)
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
entries.add(it.packageName)
zip.closeEntry()
}
packageService.launchableSystemApps.forEach {
val drawable = it.loadIcon(packageManager)
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
// check for duplicates (e.g. updated launchable system app)
if (it.activityInfo.packageName in entries) return@forEach
val entry = ZipEntry(it.activityInfo.packageName)
zip.putNextEntry(entry)
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
zip.closeEntry()
crypto.newEncryptingStream(outputStream, getAD(VERSION, token)).use { cryptoStream ->
ZipOutputStream(cryptoStream).use { zip ->
zip.setLevel(BEST_SPEED)
val entries = mutableSetOf<String>()
packageService.allUserPackages.forEach {
val drawable = packageManager.getApplicationIcon(it.applicationInfo)
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
val entry = ZipEntry(it.packageName)
zip.putNextEntry(entry)
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
entries.add(it.packageName)
zip.closeEntry()
}
packageService.launchableSystemApps.forEach {
val drawable = it.loadIcon(packageManager)
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
// check for duplicates (e.g. updated launchable system app)
if (it.activityInfo.packageName in entries) return@forEach
val entry = ZipEntry(it.activityInfo.packageName)
zip.putNextEntry(entry)
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
zip.closeEntry()
}
}
}
Log.d(TAG, "Finished uploading icons")
@ -71,21 +80,23 @@ internal class IconManager(
* Downloads icons file from given [inputStream].
* @return a set of package names for which icons were found
*/
@Throws(IOException::class, SecurityException::class)
fun downloadIcons(inputStream: InputStream): Set<String> {
@Throws(IOException::class, SecurityException::class, GeneralSecurityException::class)
fun downloadIcons(version: Byte, token: Long, inputStream: InputStream): Set<String> {
Log.d(TAG, "Start downloading icons")
val folder = File(context.cacheDir, CACHE_FOLDER)
if (!folder.isDirectory && !folder.mkdirs())
throw IOException("Can't create cache folder for icons")
val set = mutableSetOf<String>()
ZipInputStream(inputStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
File(folder, entry.name).outputStream().use { outputStream ->
zip.copyTo(outputStream)
crypto.newDecryptingStream(inputStream, getAD(version, token)).use { cryptoStream ->
ZipInputStream(cryptoStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
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")
@ -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(
context = androidContext(),
packageService = get(),
crypto = get(),
)
}
single {

View file

@ -249,7 +249,7 @@ internal class ApkBackupManagerTest : TransportTest() {
private suspend fun expectUploadIcons() {
val stream = ByteArrayOutputStream()
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() {