diff --git a/src/pt/sakuramangas/build.gradle b/src/pt/sakuramangas/build.gradle index c37322332..b6a1778b8 100644 --- a/src/pt/sakuramangas/build.gradle +++ b/src/pt/sakuramangas/build.gradle @@ -1,12 +1,13 @@ ext { extName = 'Sakura Mangás' extClass = '.SakuraMangas' - extVersionCode = 2 + extVersionCode = 3 isNsfw = true } apply from: "$rootDir/common.gradle" dependencies { - implementation(project(":lib:synchrony")) + implementation project(":lib:synchrony") + implementation project(':lib:randomua') } diff --git a/src/pt/sakuramangas/src/eu/kanade/tachiyomi/extension/pt/sakuramangas/AetherCipher.kt b/src/pt/sakuramangas/src/eu/kanade/tachiyomi/extension/pt/sakuramangas/AetherCipher.kt new file mode 100644 index 000000000..0686ef4b0 --- /dev/null +++ b/src/pt/sakuramangas/src/eu/kanade/tachiyomi/extension/pt/sakuramangas/AetherCipher.kt @@ -0,0 +1,97 @@ +package eu.kanade.tachiyomi.extension.pt.sakuramangas + +import android.util.Base64 +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.MessageDigest +import kotlin.experimental.xor + +// Function extracted from https://sakuramangas.org/dist/sakura/pages/capitulo/capitulo.v139w.obk.js +object AetherCipher { + + private data class KeystreamResult(val keystream: ByteArray, val finalState: LongArray) + + fun decrypt(data: String, key: String): String { + return try { + val encryptedBytes = Base64.decode(data, Base64.DEFAULT) + + val scheduledKey = generateScheduledKey(key) + + val sha256Digest = MessageDigest.getInstance("SHA-256").digest(key.toByteArray(Charsets.UTF_8)) + val buffer = ByteBuffer.wrap(sha256Digest).order(ByteOrder.LITTLE_ENDIAN) + val initialState = LongArray(8) { + buffer.getInt(it * 4).toLong() and 0xFFFFFFFFL + } + + val keystreamResult = generateKeystream(encryptedBytes.size, initialState, scheduledKey) + val keystream = keystreamResult.keystream + val finalState = keystreamResult.finalState + val lastStateValue = finalState[7] + + val reversedXORBytes = reverseAetherXORChain(encryptedBytes, lastStateValue.toInt()) + + val decryptedBytes = ByteArray(encryptedBytes.size) + for (i in encryptedBytes.indices) { + decryptedBytes[i] = reversedXORBytes[i] xor keystream[i] + } + String(decryptedBytes, Charsets.UTF_8) + } catch (_: Exception) { + throw Error("Could not decrypt chapter data.") + } + } + + private fun generateScheduledKey(key: String): IntArray { + val sha512Digest = MessageDigest.getInstance("SHA-512").digest(key.toByteArray(Charsets.UTF_8)) + val s = IntArray(256) { it } + var j = 0 + + for (i in 255 downTo 1) { + val shaByte = sha512Digest[i % sha512Digest.size].toInt() and 0xFF // Converte byte para int sem sinal + j = (j + s[i] + shaByte) % (i + 1) + val temp = s[i] + s[i] = s[j] + s[j] = temp + } + return s + } + + private fun generateKeystream(length: Int, initialState: LongArray, scheduledKey: IntArray): KeystreamResult { + val state = initialState.clone() + val keystream = ByteArray(length) + + for (i in 0 until length) { + state[0] = (state[0] + 2654435769L) and 0xFFFFFFFFL + state[1] = (state[1] xor state[7]) and 0xFFFFFFFFL + state[2] = (state[2] + state[0]) and 0xFFFFFFFFL + state[3] = (state[3] xor state[1].rotateLeft(5)) and 0xFFFFFFFFL + state[4] = (state[4] - state[2]) and 0xFFFFFFFFL + + val index = (state[7] and 255L).toInt() + state[5] = (state[5] xor scheduledKey[index].toLong()) and 0xFFFFFFFFL + + val rotation = (state[0] and 31L).toInt() + state[6] = (state[6] + state[3].rotateLeft(rotation)) and 0xFFFFFFFFL + state[7] = (state[7] xor state[4]) and 0xFFFFFFFFL + + val keyByte = (state[0] xor state[2] xor state[5] xor state[7]) and 255L + keystream[i] = keyByte.toByte() + } + return KeystreamResult(keystream, state) + } + + private fun reverseAetherXORChain(data: ByteArray, initialValue: Int): ByteArray { + if (data.isEmpty()) return ByteArray(0) + + val result = ByteArray(data.size) + result[0] = (data[0].toInt() xor (initialValue and 0xFF)).toByte() + + for (i in 1 until data.size) { + result[i] = data[i] xor result[i - 1] + } + return result + } + + private fun Long.rotateLeft(bits: Int): Long { + return ((this shl bits) or (this ushr (32 - bits))) and 0xFFFFFFFFL + } +} diff --git a/src/pt/sakuramangas/src/eu/kanade/tachiyomi/extension/pt/sakuramangas/SakuraMangas.kt b/src/pt/sakuramangas/src/eu/kanade/tachiyomi/extension/pt/sakuramangas/SakuraMangas.kt index 684444551..c375327fd 100644 --- a/src/pt/sakuramangas/src/eu/kanade/tachiyomi/extension/pt/sakuramangas/SakuraMangas.kt +++ b/src/pt/sakuramangas/src/eu/kanade/tachiyomi/extension/pt/sakuramangas/SakuraMangas.kt @@ -2,10 +2,16 @@ package eu.kanade.tachiyomi.extension.pt.sakuramangas import android.util.Base64 import android.util.Log +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen +import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA +import eu.kanade.tachiyomi.lib.randomua.getPrefUAType +import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -14,6 +20,7 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs import okhttp3.FormBody import okhttp3.HttpUrl.Companion.toHttpUrl @@ -22,13 +29,11 @@ import okhttp3.Response import okio.IOException import org.jsoup.Jsoup import org.jsoup.nodes.Element -import java.nio.ByteBuffer -import java.nio.ByteOrder import java.security.MessageDigest import java.util.Calendar import kotlin.concurrent.thread -class SakuraMangas : HttpSource() { +class SakuraMangas : HttpSource(), ConfigurableSource { override val lang = "pt-BR" override val supportsLatest = true @@ -37,7 +42,13 @@ class SakuraMangas : HttpSource() { override val baseUrl = "https://sakuramangas.org" + private val preferences = getPreferences() + override val client = network.cloudflareClient.newBuilder() + .setRandomUserAgent( + preferences.getPrefUAType(), + preferences.getPrefCustomUA(), + ) .rateLimit(3, 2) .build() @@ -55,6 +66,13 @@ class SakuraMangas : HttpSource() { override fun headersBuilder() = super.headersBuilder() .set("Referer", "$baseUrl/") .set("X-Requested-With", "XMLHttpRequest") + .set("Connection", "keep-alive") + .set("Cache-Control", "no-cache") + .apply { + if (!preferences.getPrefCustomUA().isNullOrEmpty()) { + set("User-Agent", preferences.getPrefCustomUA()!!) + } + } // ================================ Popular ======================================= @@ -176,10 +194,10 @@ class SakuraMangas : HttpSource() { private val keys: Keys by lazy { val mangaInfoRegex = """(?:manga_info:\s+)(\d+)""".toRegex() val chapterReadRegex = """(?:chapter_read:\s+)(\d+)""".toRegex() - val key1Regex = """(?:key1:\s+')([^']+)""".toRegex() - val key2Regex = """(?:key2:\s+')([^']+)""".toRegex() + val key1Regex = """(?:.Key-1.]\s?=\s+?.)([^']+)""".toRegex() + val key2Regex = """(?:.Key-2.]\s?=\s+?.)([^']+)""".toRegex() - val script = client.newCall(GET("$baseUrl/dist/sakura/global/security.obf.js", headers)) + val script = client.newCall(GET("$baseUrl/dist/sakura/global/security.oby.js", headers)) .execute().body.string() val deobfuscated = Deobfuscator.deobfuscateScript(script)!! @@ -282,7 +300,7 @@ class SakuraMangas : HttpSource() { .build() return POST( - "$baseUrl/dist/sakura/models/capitulo/__obf.__capltulos_read.php", + "$baseUrl/dist/sakura/models/capitulo/__obf__capitulos_read.php", pageHeaders, form.build(), ) @@ -302,7 +320,7 @@ class SakuraMangas : HttpSource() { val baseUrl = document.baseUri().trimEnd('/') - return vortexDecipherV2(response.imageUrls, subtoken) + return AetherCipher.decrypt(response.imageUrls, subtoken) .parseAs>() .mapIndexed { index, url -> Page(index, imageUrl = "$baseUrl/$url".toHttpUrl().toString()) @@ -431,48 +449,7 @@ class SakuraMangas : HttpSource() { } } - // Function extracted from https://sakuramangas.org/dist/sakura/pages/capitulo/capitulo.v100w.obs.js - private fun vortexDecipherV2(dataBase64: String, key: String): String { - try { - val digest = MessageDigest.getInstance("SHA-256").digest(key.toByteArray(Charsets.UTF_8)) - val buffer = ByteBuffer.wrap(digest).order(ByteOrder.LITTLE_ENDIAN) - - var v1 = buffer.int.toUInt() - var v2 = buffer.int.toUInt() - var v3 = buffer.int.toUInt() - var v4 = buffer.int.toUInt() - val c1 = buffer.int.toUInt() - val c2 = buffer.int.toUInt() - - val decoded = Base64.decode(dataBase64, Base64.DEFAULT) - - val output = ByteArray(decoded.size) - - for (i in decoded.indices) { - if (i % 2 == 0) { - v1 = (v1 + 2654435769u) - v2 = (v2 xor c1) - v3 = (v3 + v2) - val shift = (v1 and 31u).toInt() - v4 = (((v4 xor v3).rotateLeft(shift))) - } else { - v3 = (v3 + 1640531527u) - v4 = (v4 xor c2) - v1 = (v1 + v4) - val shift = (v3 and 31u).toInt() - v2 = (((v2 xor v1).rotateLeft(shift))) - } - val mask = (v1 xor v2 xor v3 xor v4).toInt() and 0xFF - output[i] = (decoded[i].toInt() xor mask).toByte() - } - - return output.toString(Charsets.UTF_8) - } catch (e: Exception) { - throw IOException("Não foi possível descriptografar os dados do capítulo.", e) - } - } - - private fun UInt.rotateLeft(bits: Int): UInt { - return (this shl bits) or (this shr (32 - bits)) + override fun setupPreferenceScreen(screen: PreferenceScreen) { + addRandomUAPreferenceToScreen(screen) } }