[Madara] Decrypt image list for Amuy and Cerise Scans (#14876)

* madata: add chapter image list decryption

* add final newline

* Exit early if there's no chapter protection

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

* Change approach to individual sources

multisrc doesn't like external dependencies

* unbump baseversioncode

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
beerpsi 2023-01-23 10:39:45 +07:00 committed by GitHub
parent 589f46de0b
commit ca789eca79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 3 deletions

View File

@ -0,0 +1,3 @@
dependencies {
implementation(project(':lib-cryptoaes'))
}

View File

@ -1,8 +1,15 @@
package eu.kanade.tachiyomi.extension.pt.amuy
import android.util.Base64
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Page
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
@ -19,4 +26,44 @@ class Amuy : Madara(
.build()
override val useNewChapterEndpoint = true
override fun pageListParse(document: Document): List<Page> {
val chapterProtector = document.getElementById("chapter-protector-data")?.html()
?: return super.pageListParse(document)
val password = chapterProtector
.substringAfter("wpmangaprotectornonce='")
.substringBefore("';")
val chapterData = json.parseToJsonElement(
chapterProtector
.substringAfter("chapter_data='")
.substringBefore("';")
.replace("\\/", "/")
).jsonObject
val unsaltedCiphertext = Base64.decode(chapterData["ct"]!!.jsonPrimitive.content, Base64.DEFAULT)
val salt = chapterData["s"]!!.jsonPrimitive.content.decodeHex()
val ciphertext = SALTED + salt + unsaltedCiphertext
val rawImgArray = CryptoAES.decrypt(Base64.encodeToString(ciphertext, Base64.DEFAULT), password)
val imgArrayString = json.parseToJsonElement(rawImgArray).jsonPrimitive.content
val imgArray = json.parseToJsonElement(imgArrayString).jsonArray
return imgArray.mapIndexed { idx, it ->
Page(idx, document.location(), it.jsonPrimitive.content)
}
}
// https://stackoverflow.com/a/66614516
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
val SALTED = "Salted__".toByteArray(Charsets.UTF_8)
}
}

View File

@ -0,0 +1,3 @@
dependencies {
implementation(project(':lib-cryptoaes'))
}

View File

@ -1,8 +1,15 @@
package eu.kanade.tachiyomi.extension.pt.cerisescans
import android.util.Base64
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Page
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
@ -19,4 +26,44 @@ class CeriseScans : Madara(
.build()
override val useNewChapterEndpoint = true
override fun pageListParse(document: Document): List<Page> {
val chapterProtector = document.getElementById("chapter-protector-data")?.html()
?: return super.pageListParse(document)
val password = chapterProtector
.substringAfter("wpmangaprotectornonce='")
.substringBefore("';")
val chapterData = json.parseToJsonElement(
chapterProtector
.substringAfter("chapter_data='")
.substringBefore("';")
.replace("\\/", "/")
).jsonObject
val unsaltedCiphertext = Base64.decode(chapterData["ct"]!!.jsonPrimitive.content, Base64.DEFAULT)
val salt = chapterData["s"]!!.jsonPrimitive.content.decodeHex()
val ciphertext = SALTED + salt + unsaltedCiphertext
val rawImgArray = CryptoAES.decrypt(Base64.encodeToString(ciphertext, Base64.DEFAULT), password)
val imgArrayString = json.parseToJsonElement(rawImgArray).jsonPrimitive.content
val imgArray = json.parseToJsonElement(imgArrayString).jsonArray
return imgArray.mapIndexed { idx, it ->
Page(idx, document.location(), it.jsonPrimitive.content)
}
}
// https://stackoverflow.com/a/66614516
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
val SALTED = "Salted__".toByteArray(Charsets.UTF_8)
}
}

View File

@ -358,6 +358,7 @@ abstract class Madara(
if (list.isNotEmpty()) { list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) } }
}
}
else -> {}
}
}
return GET(url.toString(), headers)
@ -488,6 +489,7 @@ abstract class Madara(
taxQueryIdx++
}
}
else -> {}
}
}
}
@ -816,7 +818,7 @@ abstract class Madara(
}
open val updatingRegex = "Updating|Atualizando".toRegex(RegexOption.IGNORE_CASE)
public fun String.notUpdating(): Boolean {
fun String.notUpdating(): Boolean {
return this.contains(updatingRegex).not()
}

View File

@ -35,7 +35,7 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("Aleatória Scan", "https://aleatoriascan.xyz", "pt-BR", className = "AleatoriaScan"),
SingleLang("AllPornComic", "https://allporncomic.com", "en", isNsfw = true),
SingleLang("Aln Scans", "https://alnscans.com", "en"),
SingleLang("Amuy", "https://amuyscans.com", "pt-BR", isNsfw = true),
SingleLang("Amuy", "https://amuyscans.com", "pt-BR", isNsfw = true, overrideVersionCode = 1),
SingleLang("Anikiga", "https://anikiga.com", "tr"),
SingleLang("Anisa Manga", "https://anisamanga.com", "tr"),
SingleLang("Ansh Scans", "https://anshscans.org", "en"),
@ -64,7 +64,7 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("CAT-translator", "https://cats-translator.com/manga", "th", className = "CatTranslator", overrideVersionCode = 2),
SingleLang("Cat300", "https://cat300.com", "th", isNsfw = true, className = "Cat300", overrideVersionCode = 1),
SingleLang("CatOnHeadTranslations", "https://catonhead.com", "en", overrideVersionCode = 2),
SingleLang("Cerise Scans", "https://cerisescan.com", "pt-BR", overrideVersionCode = 3),
SingleLang("Cerise Scans", "https://cerisescan.com", "pt-BR", overrideVersionCode = 4),
SingleLang("Chibi Manga", "https://www.cmreader.info", "en", overrideVersionCode = 1),
SingleLang("Clover Manga", "https://clover-manga.com", "tr", overrideVersionCode = 2),
SingleLang("Coffee Manga", "https://coffeemanga.io", "en", overrideVersionCode = 1),