fix password protect downloads and copying ComicInfo files in LocalSource (#1084)

* fix password protect downloads

* fixed copying of ComicInfo file in LocalSource.kt

* Return correct archive file

* Applied upstream fix

* Use tempFileManager instead of file path

* Use streams instead of files
This commit is contained in:
Shamicen 2024-03-02 17:56:57 +01:00 committed by GitHub
parent dd412e33ad
commit eed8ffb9d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 170 additions and 105 deletions

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.os.Build
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.domain.chapter.model.toSChapter import eu.kanade.domain.chapter.model.toSChapter
import eu.kanade.domain.manga.model.getComicInfo import eu.kanade.domain.manga.model.getComicInfo
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addFilesToZip
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
@ -43,8 +43,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response import okhttp3.Response
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@ -66,7 +64,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets
import java.util.Locale import java.util.Locale
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -663,31 +660,15 @@ class Downloader(
dirname: String, dirname: String,
tmpDir: UniFile, tmpDir: UniFile,
) { ) {
val zipFile = File(context.externalCacheDir, "$dirname.cbz$TMP_DIR_SUFFIX")
val zip = ZipFile(zipFile)
val zipParameters = ZipParameters()
CbzCrypto.setZipParametersEncrypted(zipParameters)
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) zip.charset = StandardCharsets.ISO_8859_1
tmpDir.filePath?.let { addPaddingToImage(File(it)) } tmpDir.filePath?.let { addPaddingToImage(File(it)) }
zip.addFiles( tmpDir.listFiles()?.toList()?.let { files ->
tmpDir.listFiles()?.map { img -> img.filePath?.let { File(it) } }, mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
zipParameters, ?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz())
) }
zip.close()
val realZip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
realZip.openOutputStream().use { out ->
zipFile.inputStream().use {
it.copyTo(out)
}
}
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz") mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
tmpDir.delete() tmpDir.delete()
zipFile.delete()
} }
private fun addPaddingToImage(imageDir: File) { private fun addPaddingToImage(imageDir: File) {

View File

@ -37,10 +37,6 @@ internal class ZipPageLoader(file: File) : PageLoader() {
} }
init { init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zip4j.charset = StandardCharsets.ISO_8859_1
}
Zip4jFile(file).use { zip -> Zip4jFile(file).use { zip ->
if (zip.isEncrypted) { if (zip.isEncrypted) {
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) { if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {

View File

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.util.storage
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -11,9 +13,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority import logcat.LogPriority
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.exception.ZipException
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.io.outputstream.ZipOutputStream
import net.lingala.zip4j.model.LocalFileHeader
import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.ZipParameters
import net.lingala.zip4j.model.enums.AesKeyStrength import net.lingala.zip4j.model.enums.AesKeyStrength
import net.lingala.zip4j.model.enums.EncryptionMethod import net.lingala.zip4j.model.enums.EncryptionMethod
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -221,6 +228,133 @@ object CbzCrypto {
} }
return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true) return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true)
} }
fun UniFile.isEncryptedZip(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.nextEntry
stream.close()
false
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
true
} else throw zipException
}
}
fun UniFile.testCbzPassword(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.setPassword(getDecryptedPasswordCbz())
stream.nextEntry
stream.close()
true
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
false
} else throw zipException
}
}
fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) {
val zipOutputStream =
if (password != null) ZipOutputStream(this.openOutputStream(), password)
else ZipOutputStream(this.openOutputStream())
val zipParameters = ZipParameters()
zipParameters.fileNameInZip = filename
if (password != null) setZipParametersEncrypted(zipParameters)
zipOutputStream.putNextEntry(zipParameters)
zipOutputStream.use { output ->
inputStream.use { input ->
input.copyTo(output)
}
}
}
fun UniFile.addFilesToZip(files: List<UniFile>, password: CharArray? = null) {
val zipOutputStream =
if (password != null) ZipOutputStream(this.openOutputStream(), password)
else ZipOutputStream(this.openOutputStream())
files.forEach {
val zipParameters = ZipParameters()
if (password != null) setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = it.name
zipOutputStream.putNextEntry(zipParameters)
it.openInputStream().use { input ->
input.copyTo(zipOutputStream)
}
zipOutputStream.closeEntry()
}
zipOutputStream.close()
}
fun UniFile.getZipInputStream(filename: String): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
try {
while (run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}) {
if (fileHeader?.fileName == filename) return zipInputStream
}
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
} else throw zipException
}
return null
}
fun UniFile.getCoverStreamFromZip(): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
val fileHeaderList: MutableList<LocalFileHeader?> = mutableListOf()
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
try {
while (run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}) {
fileHeaderList.add(fileHeader)
}
var coverHeader = fileHeaderList
.mapNotNull { it }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) }
val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) }
if (coverStream != null) {
if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null
}
return coverHeader?.fileName?.let { getZipInputStream(it) }
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
return null
} else throw zipException
}
}
} }
private const val BUFFER_SIZE = 2048 private const val BUFFER_SIZE = 2048

View File

@ -11,6 +11,10 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStreamFromZip
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getZipInputStream
import eu.kanade.tachiyomi.util.storage.CbzCrypto.isEncryptedZip
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -18,8 +22,6 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream import kotlinx.serialization.json.encodeToStream
import logcat.LogPriority import logcat.LogPriority
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters
import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.AndroidXmlReader
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@ -44,7 +46,6 @@ import tachiyomi.source.local.io.Format
import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.io.LocalSourceFileSystem
import tachiyomi.source.local.metadata.fillMetadata import tachiyomi.source.local.metadata.fillMetadata
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
@ -199,15 +200,12 @@ actual class LocalSource(
} }
// SY --> // SY -->
comicInfoArchiveFile != null -> { comicInfoArchiveFile != null -> {
val comicInfoArchive = ZipFile(tempFileManager.createTempFile(comicInfoArchiveFile))
noXmlFile?.delete() noXmlFile?.delete()
if (CbzCrypto.checkCbzPassword(comicInfoArchive, CbzCrypto.getDecryptedPasswordCbz())) { comicInfoArchiveFile.getZipInputStream(COMIC_INFO_FILE)
comicInfoArchive.setPassword(CbzCrypto.getDecryptedPasswordCbz()) ?.let { setMangaDetailsFromComicInfoFile(it, manga) }
val comicInfoEntry = comicInfoArchive.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }
setMangaDetailsFromComicInfoFile(comicInfoArchive.getInputStream(comicInfoEntry), manga)
}
} }
// SY <-- // SY <--
// Old custom JSON format // Old custom JSON format
@ -239,18 +237,13 @@ actual class LocalSource(
.filter(Archive::isSupported) .filter(Archive::isSupported)
.toList() .toList()
val folderPath = mangaDir?.filePath val copiedFile = mangaDir?.let { copyComicInfoFileFromArchive(chapterArchives, it) }
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
// SY --> // SY -->
if (copiedFile != null && copiedFile.name != COMIC_INFO_ARCHIVE) { if (copiedFile != null && copiedFile.name != COMIC_INFO_ARCHIVE) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga) setMangaDetailsFromComicInfoFile(copiedFile.openInputStream(), manga)
} else if (copiedFile != null && copiedFile.name == COMIC_INFO_ARCHIVE) { } else if (copiedFile != null && copiedFile.name == COMIC_INFO_ARCHIVE) {
val comicInfoArchive = ZipFile(copiedFile) copiedFile.getZipInputStream(COMIC_INFO_FILE)?.let { setMangaDetailsFromComicInfoFile(it, manga) }
comicInfoArchive.setPassword(CbzCrypto.getDecryptedPasswordCbz())
val comicInfoEntry = comicInfoArchive.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }
setMangaDetailsFromComicInfoFile(comicInfoArchive.getInputStream(comicInfoEntry), manga)
} // SY <-- } // SY <--
else { else {
// Avoid re-scanning // Avoid re-scanning
@ -265,33 +258,20 @@ actual class LocalSource(
return@withIOContext manga return@withIOContext manga
} }
private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folderPath: String?): File? { private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folder: UniFile): UniFile? {
for (chapter in chapterArchives) { for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) { when (Format.valueOf(chapter)) {
is Format.Zip -> { is Format.Zip -> {
ZipFile(tempFileManager.createTempFile(chapter)).use { zip: ZipFile ->
// SY --> // SY -->
if (zip.isEncrypted && !CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz()) chapter.getZipInputStream(COMIC_INFO_FILE)?.buffered().use { stream ->
) { return stream?.let { copyComicInfoFile(it, folder) }
return null
} else if (
zip.isEncrypted && CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())
) {
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
}
zip.getFileHeader(COMIC_INFO_FILE)?.let { comicInfoFile ->
// SY <--
zip.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
}
}
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(tempFileManager.createTempFile(chapter)).use { rar -> JunrarArchive(tempFileManager.createTempFile(chapter)).use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream -> rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folder)
} }
} }
} }
@ -302,24 +282,20 @@ actual class LocalSource(
return null return null
} }
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File { private fun copyComicInfoFile(comicInfoFileStream: InputStream, folder: UniFile): UniFile? {
// SY --> // SY -->
if ( if (
CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.getPasswordProtectDlPref() &&
CbzCrypto.isPasswordSet() CbzCrypto.isPasswordSet()
) { ) {
val zipParameters = ZipParameters() val comicInfoArchive = folder.createFile(COMIC_INFO_ARCHIVE)
CbzCrypto.setZipParametersEncrypted(zipParameters) comicInfoArchive?.addStreamToZip(comicInfoFileStream, COMIC_INFO_FILE, CbzCrypto.getDecryptedPasswordCbz())
zipParameters.fileNameInZip = COMIC_INFO_FILE
val zipEncrypted = ZipFile("$folderPath/$COMIC_INFO_ARCHIVE") return comicInfoArchive
zipEncrypted.setPassword(CbzCrypto.getDecryptedPasswordCbz())
zipEncrypted.addStream(comicInfoFileStream, zipParameters)
return zipEncrypted.file
} else { } else {
// SY <-- // SY <--
return File("$folderPath/$COMIC_INFO_FILE").apply { return folder.createFile(COMIC_INFO_FILE)?.apply {
outputStream().use { outputStream -> openOutputStream().use { outputStream ->
comicInfoFileStream.use { it.copyTo(outputStream) } comicInfoFileStream.use { it.copyTo(outputStream) }
} }
} }
@ -413,20 +389,16 @@ actual class LocalSource(
entry?.let { coverManager.update(manga, it.openInputStream()) } entry?.let { coverManager.update(manga, it.openInputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(tempFileManager.createTempFile(format.file)).use { zip ->
// SY --> // SY -->
var encrypted = false format.file.getCoverStreamFromZip()?.let { inputStream ->
if (zip.isEncrypted) { coverManager.update(
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz()) manga,
encrypted = true inputStream,
format.file.isEncryptedZip()
)
} }
val entry = zip.fileHeaders.toList()
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip.getInputStream(it) } }
entry?.let { coverManager.update(manga, zip.getInputStream(it), encrypted) }
// SY <-- // SY <--
} }
}
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive -> JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders

View File

@ -4,13 +4,11 @@ import android.content.Context
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters
import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.storage.nameWithoutExtension
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.io.LocalSourceFileSystem
import java.io.File
import java.io.InputStream import java.io.InputStream
private const val DEFAULT_COVER_NAME = "cover.jpg" private const val DEFAULT_COVER_NAME = "cover.jpg"
@ -60,23 +58,7 @@ actual class LocalCoverManager(
inputStream.use { input -> inputStream.use { input ->
// SY --> // SY -->
if (encrypted) { if (encrypted) {
val tempFile = File.createTempFile( targetFile.addStreamToZip(inputStream, DEFAULT_COVER_NAME, CbzCrypto.getDecryptedPasswordCbz())
targetFile.nameWithoutExtension.orEmpty().padEnd(3), // Prefix must be 3+ chars
null,
)
val zip4j = ZipFile(tempFile)
val zipParameters = ZipParameters()
zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
CbzCrypto.setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = DEFAULT_COVER_NAME
zip4j.addStream(input, zipParameters)
zip4j.close()
targetFile.openOutputStream().use { output ->
tempFile.inputStream().use { input ->
input.copyTo(output)
}
}
DiskUtil.createNoMediaFile(directory, context) DiskUtil.createNoMediaFile(directory, context)
manga.thumbnail_url = targetFile.uri.toString() manga.thumbnail_url = targetFile.uri.toString()