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