SakuraMangas: Fix (#10777)
* Fix Cloudflare and encrypted pages * Bump version
This commit is contained in:
parent
08b627c8e7
commit
d0e9279214
@ -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')
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user