SakuraMangas: Fix (#10777)

* Fix Cloudflare and encrypted pages

* Bump version
This commit is contained in:
Chopper 2025-09-30 04:59:03 -03:00 committed by Draff
parent 08b627c8e7
commit d0e9279214
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 128 additions and 53 deletions

View File

@ -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')
}

View File

@ -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
}
}

View File

@ -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<List<String>>()
.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)
}
}