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_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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ val workerModule = module {
|
|||
IconManager(
|
||||
context = androidContext(),
|
||||
packageService = get(),
|
||||
crypto = get(),
|
||||
)
|
||||
}
|
||||
single {
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue