Implemented local cover encryption (#881)

* Implemented local cover encryption and made coil capable of reading encrypted cover archives

* add check that the file is not an image before determining that it is a zip file
This commit is contained in:
Shamicen
2023-05-13 04:51:37 +02:00
committed by GitHub
parent 282a0c4e16
commit 291734a406
10 changed files with 81 additions and 154 deletions
@@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.screen
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
@@ -25,7 +24,6 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
@@ -57,13 +55,8 @@ import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
object SettingsSecurityScreen : SearchableSettings {
@@ -168,45 +161,6 @@ object SettingsSecurityScreen : SearchableSettings {
},
enabled = isCbzPasswordSet,
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.localCoverLocation(),
title = stringResource(R.string.save_local_manga_covers),
entries = SecurityPreferences.CoverCacheLocation.values()
.associateWith { stringResource(it.titleResId) },
enabled = passwordProtectDownloads,
onValueChanged = {
try {
withIOContext {
CbzCrypto.deleteLocalCoverCache(context)
CbzCrypto.deleteLocalCoverSystemFiles(context)
}
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
context.toast(e.toString(), Toast.LENGTH_SHORT).show()
false
}
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.delete_cached_local_source_covers),
subtitle = stringResource(R.string.delete_cached_local_source_covers_subtitle),
onClick = {
try {
CbzCrypto.deleteLocalCoverCache(context)
CbzCrypto.deleteLocalCoverSystemFiles(context)
context.toast(R.string.successfully_deleted_all_locally_cached_covers, Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
context.toast(R.string.something_went_wrong_deleting_your_cover_images, Toast.LENGTH_LONG).show()
}
},
enabled = produceState(false) {
withIOContext {
value = context.getExternalFilesDir("covers/local")?.absolutePath?.let { File(it).listFiles()?.isNotEmpty() } == true
}
}.value,
),
kotlin.run {
val navigator = LocalNavigator.currentOrThrow
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
@@ -214,7 +214,7 @@ class PreferenceModule(val application: Application) : InjektModule {
SourcePreferences(get())
}
addSingletonFactory {
SecurityPreferences(get(), application.applicationContext)
SecurityPreferences(get())
}
addSingletonFactory {
LibraryPreferences(get())
@@ -9,6 +9,9 @@ import coil.decode.ImageDecoderDecoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.FileHeader
import okio.BufferedSource
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder
@@ -19,9 +22,24 @@ import tachiyomi.decoder.ImageDecoder
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
override suspend fun decode(): DecodeResult {
val decoder = resources.sourceOrNull()?.use {
ImageDecoder.newInstance(it.inputStream())
// SY -->
var zip4j: ZipFile? = null
var entry: FileHeader? = null
if (resources.sourceOrNull()?.peek()?.use { CbzCrypto.detectCoverImageArchive(it.inputStream()) } == true) {
if (resources.source().peek().use { ImageUtil.findImageType(it.inputStream()) == null }) {
zip4j = ZipFile(resources.file().toFile().absolutePath)
entry = zip4j.fileHeaders.firstOrNull { it.fileName.equals(CbzCrypto.DEFAULT_COVER_NAME, ignoreCase = true) }
if (zip4j.isEncrypted) zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
}
}
val decoder = resources.sourceOrNull()?.use {
zip4j.use { zipFile ->
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream())
}
}
// SY <--
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
@@ -45,6 +63,9 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
private fun isApplicable(source: BufferedSource): Boolean {
val type = source.peek().inputStream().use {
// SY -->
if (CbzCrypto.detectCoverImageArchive(it)) return true
// SY <--
ImageUtil.findImageType(it)
}
return when (type) {