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 { ext {
extName = 'Sakura Mangás' extName = 'Sakura Mangás'
extClass = '.SakuraMangas' extClass = '.SakuraMangas'
extVersionCode = 2 extVersionCode = 3
isNsfw = true isNsfw = true
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies { 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.Base64
import android.util.Log 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.lib.synchrony.Deobfuscator
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit 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.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage 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.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs import keiyoushi.utils.parseAs
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -22,13 +29,11 @@ import okhttp3.Response
import okio.IOException import okio.IOException
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Calendar import java.util.Calendar
import kotlin.concurrent.thread import kotlin.concurrent.thread
class SakuraMangas : HttpSource() { class SakuraMangas : HttpSource(), ConfigurableSource {
override val lang = "pt-BR" override val lang = "pt-BR"
override val supportsLatest = true override val supportsLatest = true
@ -37,7 +42,13 @@ class SakuraMangas : HttpSource() {
override val baseUrl = "https://sakuramangas.org" override val baseUrl = "https://sakuramangas.org"
private val preferences = getPreferences()
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
preferences.getPrefUAType(),
preferences.getPrefCustomUA(),
)
.rateLimit(3, 2) .rateLimit(3, 2)
.build() .build()
@ -55,6 +66,13 @@ class SakuraMangas : HttpSource() {
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/") .set("Referer", "$baseUrl/")
.set("X-Requested-With", "XMLHttpRequest") .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 ======================================= // ================================ Popular =======================================
@ -176,10 +194,10 @@ class SakuraMangas : HttpSource() {
private val keys: Keys by lazy { private val keys: Keys by lazy {
val mangaInfoRegex = """(?:manga_info:\s+)(\d+)""".toRegex() val mangaInfoRegex = """(?:manga_info:\s+)(\d+)""".toRegex()
val chapterReadRegex = """(?:chapter_read:\s+)(\d+)""".toRegex() val chapterReadRegex = """(?:chapter_read:\s+)(\d+)""".toRegex()
val key1Regex = """(?:key1:\s+')([^']+)""".toRegex() val key1Regex = """(?:.Key-1.]\s?=\s+?.)([^']+)""".toRegex()
val key2Regex = """(?:key2:\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() .execute().body.string()
val deobfuscated = Deobfuscator.deobfuscateScript(script)!! val deobfuscated = Deobfuscator.deobfuscateScript(script)!!
@ -282,7 +300,7 @@ class SakuraMangas : HttpSource() {
.build() .build()
return POST( return POST(
"$baseUrl/dist/sakura/models/capitulo/__obf.__capltulos_read.php", "$baseUrl/dist/sakura/models/capitulo/__obf__capitulos_read.php",
pageHeaders, pageHeaders,
form.build(), form.build(),
) )
@ -302,7 +320,7 @@ class SakuraMangas : HttpSource() {
val baseUrl = document.baseUri().trimEnd('/') val baseUrl = document.baseUri().trimEnd('/')
return vortexDecipherV2(response.imageUrls, subtoken) return AetherCipher.decrypt(response.imageUrls, subtoken)
.parseAs<List<String>>() .parseAs<List<String>>()
.mapIndexed { index, url -> .mapIndexed { index, url ->
Page(index, imageUrl = "$baseUrl/$url".toHttpUrl().toString()) 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 override fun setupPreferenceScreen(screen: PreferenceScreen) {
private fun vortexDecipherV2(dataBase64: String, key: String): String { addRandomUAPreferenceToScreen(screen)
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))
} }
} }