From 2d450add12a6aa077f61e3ab9cc4c17533b5a029 Mon Sep 17 00:00:00 2001 From: Secozzi <49240133+Secozzi@users.noreply.github.com> Date: Sat, 20 Sep 2025 08:44:05 +0000 Subject: [PATCH] Add kagane (#10599) * Add kagane * Small code cleanup * Make sure nsfw cookie is always added * Add interceptor to automatically refresh token * Small code cleanup pt. 2 --- src/en/kagane/build.gradle | 8 + src/en/kagane/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4993 bytes src/en/kagane/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2848 bytes .../kagane/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6372 bytes .../kagane/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 11257 bytes .../kagane/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 15865 bytes .../tachiyomi/extension/en/kagane/Dto.kt | 103 ++++ .../extension/en/kagane/ImageInterceptor.kt | 199 ++++++++ .../tachiyomi/extension/en/kagane/Kagane.kt | 439 ++++++++++++++++++ .../extension/en/kagane/Randomizer.kt | 109 +++++ .../extension/en/kagane/Scrambler.kt | 125 +++++ .../tachiyomi/extension/en/kagane/Utils.kt | 14 + 12 files changed, 997 insertions(+) create mode 100644 src/en/kagane/build.gradle create mode 100644 src/en/kagane/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/en/kagane/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/en/kagane/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/en/kagane/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/en/kagane/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt create mode 100644 src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/ImageInterceptor.kt create mode 100644 src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt create mode 100644 src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Randomizer.kt create mode 100644 src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Scrambler.kt create mode 100644 src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Utils.kt diff --git a/src/en/kagane/build.gradle b/src/en/kagane/build.gradle new file mode 100644 index 000000000..b0b192f34 --- /dev/null +++ b/src/en/kagane/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Kagane' + extClass = '.Kagane' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/kagane/res/mipmap-hdpi/ic_launcher.png b/src/en/kagane/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..14f638e51b1a6dcec6de9c55087d3596d7a6564d GIT binary patch literal 4993 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 LNkvXXu0mjfuRLDb literal 0 HcmV?d00001 diff --git a/src/en/kagane/res/mipmap-mdpi/ic_launcher.png b/src/en/kagane/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..605a2ff74bef9e5b37d618522d6ac033951a9249 GIT binary patch literal 2848 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 mJ^c7y4(J7-hbhp*kN9)jp0N(?~| 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 THu0ZYx&bvMZN(~i>xlmW5-`S7 literal 0 HcmV?d00001 diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt new file mode 100644 index 000000000..f70cfef5b --- /dev/null +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt @@ -0,0 +1,103 @@ +package eu.kanade.tachiyomi.extension.en.kagane + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import keiyoushi.utils.tryParse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Locale + +@Serializable +class BookDto( + val id: String, + val name: String, + val source: String, + val metadata: MetadataDto, + val booksMetadata: BooksMetadataDto, +) { + @Serializable + class MetadataDto( + val genres: List, + val status: String, + val summary: String, + ) + + @Serializable + class BooksMetadataDto( + val authors: List, + ) { + @Serializable + class AuthorDto( + val name: String, + val role: String, + ) + } + + fun toSManga(domain: String): SManga = SManga.create().apply { + title = name + url = "/series/$id" + description = buildString { + append(metadata.summary) + append("\n\n") + append("Source: ") + append(source) + } + thumbnail_url = "https://api.$domain/api/v1/series/$id/thumbnail" + author = getRoles(listOf("writer")) + artist = getRoles(listOf("inker", "colorist", "penciller")) + genre = metadata.genres.joinToString() + status = metadata.status.toStatus() + } + + private fun String.toStatus(): Int { + return when (this) { + "ONGOING" -> SManga.ONGOING + "ENDED" -> SManga.COMPLETED + else -> SManga.COMPLETED + } + } + + private fun getRoles(roles: List): String { + return booksMetadata.authors + .filter { roles.contains(it.role) } + .joinToString { it.name } + } +} + +@Serializable +class ChapterDto( + val id: String, + val metadata: MetadataDto, +) { + @Serializable + class MetadataDto( + val releaseDate: String? = null, + val title: String, + ) + + fun toSChapter(seriesId: String): SChapter = SChapter.create().apply { + url = "$seriesId;$id" + name = metadata.title + date_upload = dateFormat.tryParse(metadata.releaseDate) + } + + companion object { + private val dateFormat by lazy { + SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) + } + } +} + +@Serializable +class ChallengeDto( + @SerialName("access_token") + val accessToken: String, + @SerialName("page_count") + val pageCount: Int, +) + +@Serializable +class PaginationDto( + val hasNext: Boolean, +) diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/ImageInterceptor.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/ImageInterceptor.kt new file mode 100644 index 000000000..198253870 --- /dev/null +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/ImageInterceptor.kt @@ -0,0 +1,199 @@ +package eu.kanade.tachiyomi.extension.en.kagane + +import okhttp3.Interceptor +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import okio.IOException +import java.math.BigInteger +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +class ImageInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url + return if (url.queryParameterNames.contains("token")) { + val seriesId = url.pathSegments[3] + val chapterId = url.pathSegments[5] + val index = url.pathSegments.last().toInt() + + val imageResp = chain.proceed(chain.request()) + val imageBytes = imageResp.body.bytes() + val decrypted = decryptImage(imageBytes, seriesId, chapterId) + ?: throw IOException("Unable to decrypt data") + val unscrambled = processData(decrypted, index, seriesId, chapterId) + ?: throw IOException("Unable to unscramble data") + + Response.Builder().body(unscrambled.toResponseBody()) + .request(chain.request()) + .protocol(Protocol.HTTP_1_0) + .code(200) + .message("") + .build() + } else { + chain.proceed(chain.request()) + } + } + + data class WordArray(val words: IntArray, val sigBytes: Int) + + private fun wordArrayToBytes(e: WordArray): ByteArray { + val result = ByteArray(e.sigBytes) + for (i in 0 until e.sigBytes) { + val word = e.words[i ushr 2] + val shift = 24 - (i % 4) * 8 + result[i] = ((word ushr shift) and 0xFF).toByte() + } + return result + } + + private fun aesGcmDecrypt(keyWordArray: WordArray, ivWordArray: WordArray, cipherWordArray: WordArray): ByteArray? { + return try { + val keyBytes = wordArrayToBytes(keyWordArray) + val iv = wordArrayToBytes(ivWordArray) + val cipherBytes = wordArrayToBytes(cipherWordArray) + + val secretKey: SecretKey = SecretKeySpec(keyBytes, "AES") + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val spec = GCMParameterSpec(128, iv) + + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + cipher.doFinal(cipherBytes) + } catch (_: Exception) { + null + } + } + + private fun toWordArray(bytes: ByteArray): WordArray { + val words = IntArray((bytes.size + 3) / 4) + for (i in bytes.indices) { + val wordIndex = i / 4 + val shift = 24 - (i % 4) * 8 + words[wordIndex] = words[wordIndex] or ((bytes[i].toInt() and 0xFF) shl shift) + } + return WordArray(words, bytes.size) + } + + private fun decryptImage(payload: ByteArray, keyPart1: String, keyPart2: String): ByteArray? { + return try { + if (payload.size < 140) return null + + val iv = payload.sliceArray(128 until 140) + val ciphertext = payload.sliceArray(140 until payload.size) + + val keyHash = "$keyPart1:$keyPart2".sha256() + + val keyWA = toWordArray(keyHash) + val ivWA = toWordArray(iv) + val cipherWA = toWordArray(ciphertext) + + aesGcmDecrypt(keyWA, ivWA, cipherWA) + } catch (_: Exception) { + null + } + } + + private fun processData(input: ByteArray, index: Int, seriesId: String, chapterId: String): ByteArray? { + fun isValidImage(data: ByteArray): Boolean { + return when { + data.size >= 2 && data[0] == 0xFF.toByte() && data[1] == 0xD8.toByte() -> true + data.size >= 12 && data[0] == 'R'.code.toByte() && data[1] == 'I'.code.toByte() && + data[2] == 'F'.code.toByte() && data[3] == 'F'.code.toByte() && + data[8] == 'W'.code.toByte() && data[9] == 'E'.code.toByte() && + data[10] == 'B'.code.toByte() && data[11] == 'P'.code.toByte() -> true + data.size >= 2 && data[0] == 0xFF.toByte() && data[1] == 0x0A.toByte() -> true + data.size >= 12 && data.copyOfRange(0, 12).contentEquals( + byteArrayOf( + 0, + 0, + 0, + 12, + 'J'.code.toByte(), + 'X'.code.toByte(), + 'L'.code.toByte(), + ' '.code.toByte(), + ), + ) -> true + else -> false + } + } + + try { + var processed: ByteArray = input + + if (!isValidImage(processed)) { + val seed = generateSeed(seriesId, chapterId, "%04d.jpg".format(index)) + val scrambler = Scrambler(seed, 10) + val scrambleMapping = scrambler.getScrambleMapping() + processed = unscramble(processed, scrambleMapping, true) + if (!isValidImage(processed)) return null + } + + return processed + } catch (_: Exception) { + return null + } + } + + private fun generateSeed(t: String, n: String, e: String): BigInteger { + val sha256 = "$t:$n:$e".sha256() + var a = BigInteger.ZERO + for (i in 0 until 8) { + a = a.shiftLeft(8).or(BigInteger.valueOf((sha256[i].toInt() and 0xFF).toLong())) + } + return a + } + + private fun unscramble(data: ByteArray, mapping: List>, n: Boolean): ByteArray { + val s = mapping.size + val a = data.size + val l = a / s + val o = a % s + + val (r, i) = if (n) { + if (o > 0) { + Pair(data.copyOfRange(0, o), data.copyOfRange(o, a)) + } else { + Pair(ByteArray(0), data) + } + } else { + if (o > 0) { + Pair(data.copyOfRange(a - o, a), data.copyOfRange(0, a - o)) + } else { + Pair(ByteArray(0), data) + } + } + + val chunks = (0 until s).map { idx -> + val start = idx * l + val end = (idx + 1) * l + i.copyOfRange(start, end) + }.toMutableList() + + val u = Array(s) { ByteArray(0) } + + if (n) { + for ((e, m) in mapping) { + if (e < s && m < s) { + u[e] = chunks[m] + } + } + } else { + for ((e, m) in mapping) { + if (e < s && m < s) { + u[m] = chunks[e] + } + } + } + + val h = u.fold(ByteArray(0)) { acc, chunk -> acc + chunk } + + return if (n) { + h + r + } else { + r + h + } + } +} diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt new file mode 100644 index 000000000..efbb348b6 --- /dev/null +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt @@ -0,0 +1,439 @@ +package eu.kanade.tachiyomi.extension.en.kagane + +import android.app.Application +import android.content.SharedPreferences +import android.os.Handler +import android.os.Looper +import android.util.Base64 +import android.view.View +import android.webkit.CookieManager +import android.webkit.JavascriptInterface +import android.webkit.PermissionRequest +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +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.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +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.getPreferencesLazy +import keiyoushi.utils.parseAs +import keiyoushi.utils.toJsonString +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okio.IOException +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.charset.StandardCharsets +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.collections.forEach +import kotlin.getValue +import kotlin.text.split + +class Kagane : HttpSource(), ConfigurableSource { + + override val name = "Kagane" + + private val domain = "kagane.org" + private val apiUrl = "https://api.$domain" + override val baseUrl = "https://$domain" + + override val lang = "en" + + override val supportsLatest = false + + private val preferences by getPreferencesLazy() + + override val client = network.cloudflareClient.newBuilder() + .cookieJar( + object : CookieJar { + private val cookieManager by lazy { CookieManager.getInstance() } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + val urlString = url.toString() + cookies.forEach { cookieManager.setCookie(urlString, it.toString()) } + } + + override fun loadForRequest(url: HttpUrl): List { + val cookies = cookieManager.getCookie(url.toString()).orEmpty() + val cookieList = mutableListOf() + var hasNsfwCookie = false + + cookies.split(";").mapNotNullTo(cookieList) { c -> + var cookieValue = c + if (url.host == domain && c.contains("kagane_mature_content")) { + hasNsfwCookie = true + val (key, _) = c.split("=") + cookieValue = "$key=${preferences.showNsfw}" + } + + Cookie.parse(url, cookieValue) + } + + if (!hasNsfwCookie && url.host == domain) { + Cookie.parse(url, "kagane_mature_content=${preferences.showNsfw}")?.let { + cookieList.add(it) + } + } + + return cookieList + } + }, + ) + .addInterceptor(ImageInterceptor()) + .addInterceptor(::refreshTokenInterceptor) + .rateLimit(2) + .build() + + private fun refreshTokenInterceptor(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url + if (!url.queryParameterNames.contains("token")) { + return chain.proceed(request) + } + + val seriesId = url.pathSegments[3] + val chapterId = url.pathSegments[5] + + var response = chain.proceed( + request.newBuilder() + .url(url.newBuilder().setQueryParameter("token", accessToken).build()) + .build(), + ) + if (response.code == 401) { + response.close() + val challenge = try { + getChallengeResponse(seriesId, chapterId) + } catch (_: Exception) { + throw IOException("Failed to retrieve token") + } + accessToken = challenge.accessToken + response = chain.proceed( + request.newBuilder() + .url(url.newBuilder().setQueryParameter("token", accessToken).build()) + .build(), + ) + } + + return response + } + + // ============================== Popular =============================== + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/?page=$page", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + return pageListParse(response, "initialSeriesData") + } + + private fun pageListParse(response: Response, key: String): MangasPage { + val data = response.asJsoup().selectFirst("script:containsData($key)")!!.data() + + val mangaList = data.getNextData(key) + .parseAs>() + .map { it.toSManga(domain) } + + val pagination = data.getNextData("pagination", isList = false, selectFirst = false) + .parseAs() + + return MangasPage(mangaList, pagination.hasNext) + } + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request { + throw UnsupportedOperationException() + } + + override fun latestUpdatesParse(response: Response): MangasPage { + throw UnsupportedOperationException() + } + + // =============================== Search =============================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/search".toHttpUrl().newBuilder().apply { + addQueryParameter("name", query) + addQueryParameter("page", page.toString()) + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + return pageListParse(response, "ssrData") + } + + // =========================== Manga Details ============================ + + override fun mangaDetailsParse(response: Response): SManga { + val data = response.asJsoup().selectFirst("script:containsData(initialSeriesData)")!!.data() + .getNextData("initialSeriesData", isList = false) + + return data.parseAs().toSManga(domain) + } + + // ============================== Chapters ============================== + + override fun chapterListParse(response: Response): List { + val seriesId = response.request.url.pathSegments.last() + val data = response.asJsoup().selectFirst("script:containsData(initialBooksData)")!!.data() + .getNextData("initialBooksData") + .parseAs>() + .reversed() + + return data.map { it.toSChapter(seriesId) } + } + + override fun getChapterUrl(chapter: SChapter): String { + val (seriesId, chapterId) = chapter.url.split(";") + return "$baseUrl/series/$seriesId/reader/$chapterId" + } + + // =============================== Pages ================================ + + private val apiHeaders = headers.newBuilder().apply { + add("Origin", baseUrl) + add("Referer", "$baseUrl/") + }.build() + + private fun getCertificate(): String { + return client.newCall(GET("$apiUrl/api/v1/static/bin.bin", apiHeaders)).execute() + .body.bytes() + .toBase64() + } + + override fun fetchPageList(chapter: SChapter): Observable> { + val (seriesId, chapterId) = chapter.url.split(";") + + val challengeResp = getChallengeResponse(seriesId, chapterId) + accessToken = challengeResp.accessToken + val pages = (0 until challengeResp.pageCount).map { page -> + val pageUrl = "$apiUrl/api/v1/books".toHttpUrl().newBuilder().apply { + addPathSegment(seriesId) + addPathSegment("file") + addPathSegment(chapterId) + addPathSegment((page + 1).toString()) + addQueryParameter("token", accessToken) + }.build().toString() + + Page(page, imageUrl = pageUrl) + } + + return Observable.just(pages) + } + + private var accessToken: String = "" + private fun getChallengeResponse(seriesId: String, chapterId: String): ChallengeDto { + val f = "$seriesId:$chapterId".sha256().sliceArray(0 until 16) + + val interfaceName = "jsInterface" + val html = """ + + + + + Title + + + + + + """.trimIndent() + + val handler = Handler(Looper.getMainLooper()) + val latch = CountDownLatch(1) + val jsInterface = JsInterface(latch) + var webView: WebView? = null + + handler.post { + val innerWv = WebView(Injekt.get()) + + webView = innerWv + innerWv.settings.domStorageEnabled = true + innerWv.settings.javaScriptEnabled = true + innerWv.settings.blockNetworkImage = true + innerWv.settings.userAgentString = headers["User-Agent"] + innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + innerWv.addJavascriptInterface(jsInterface, interfaceName) + + innerWv.webChromeClient = object : WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest?) { + if (request?.resources?.contains(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID) == true) { + request.grant(request.resources) + } else { + super.onPermissionRequest(request) + } + } + } + + innerWv.loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null) + } + + latch.await(10, TimeUnit.SECONDS) + handler.post { webView?.destroy() } + + if (latch.count == 1L) { + throw Exception("Timed out getting drm challenge") + } + + if (jsInterface.challenge.isEmpty()) { + throw Exception("Failed to get drm challenge") + } + + val challengeUrl = "$apiUrl/api/v1/books/$seriesId/file/$chapterId" + val challengeBody = buildJsonObject { + put("challenge", jsInterface.challenge) + }.toJsonString().toRequestBody("application/json".toMediaType()) + + return client.newCall(POST(challengeUrl, apiHeaders, challengeBody)).execute() + .parseAs() + } + + private fun concat(vararg arrays: ByteArray): ByteArray = + arrays.reduce { acc, bytes -> acc + bytes } + + private fun getPssh(t: ByteArray): ByteArray { + val e = Base64.decode("7e+LqXnWSs6jyCfc1R0h7Q==", Base64.DEFAULT) + val zeroes = ByteArray(4) + + val i = byteArrayOf(18, t.size.toByte()) + t + val s = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(i.size).array() + + val innerBox = concat(zeroes, e, s, i) + val outerSize = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(innerBox.size + 8).array() + val psshHeader = "pssh".toByteArray(StandardCharsets.UTF_8) + + return concat(outerSize, psshHeader, innerBox) + } + + internal class JsInterface(private val latch: CountDownLatch) { + var challenge: String = "" + + @JavascriptInterface + @Suppress("UNUSED") + fun passPayload(rawData: String) { + try { + challenge = rawData + latch.countDown() + } catch (_: Exception) { + return + } + } + } + + override fun pageListParse(response: Response): List { + throw UnsupportedOperationException() + } + + override fun imageUrlParse(response: Response): String { + throw UnsupportedOperationException() + } + + // ============================ Preferences ============================= + + private val SharedPreferences.showNsfw + get() = this.getBoolean(SHOW_NSFW_KEY, true) + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = SHOW_NSFW_KEY + title = "Show nsfw entries" + setDefaultValue(true) + }.let(screen::addPreference) + } + + // ============================= Utilities ============================== + + private fun String.getNextData(key: String, isList: Boolean = true, selectFirst: Boolean = true): String { + val (startDel, endDel) = if (isList) '[' to ']' else '{' to '}' + + val keyIndex = if (selectFirst) this.indexOf(key) else this.lastIndexOf(key) + val start = this.indexOf(startDel, keyIndex) + + var depth = 1 + var i = start + 1 + + while (i < this.length && depth > 0) { + when (this[i]) { + startDel -> depth++ + endDel -> depth-- + } + i++ + } + + return "\"${this.substring(start, i)}\"".parseAs() + } + + companion object { + private const val SHOW_NSFW_KEY = "pref_show_nsfw" + } +} diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Randomizer.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Randomizer.kt new file mode 100644 index 000000000..86763fceb --- /dev/null +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Randomizer.kt @@ -0,0 +1,109 @@ +package eu.kanade.tachiyomi.extension.en.kagane + +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +class Randomizer(seedInput: BigInteger, t: Int) { + + val size: Int = t * t + val seed: BigInteger + private var state: BigInteger + private val entropyPool: ByteArray + val order: MutableList + + companion object { + private val MASK64 = BigInteger("FFFFFFFFFFFFFFFF", 16) + private val MASK32 = BigInteger("FFFFFFFF", 16) + private val MASK8 = BigInteger("FF", 16) + private val PRNG_MULT = BigInteger("27BB2EE687B0B0FD", 16) + private val RND_MULT_32 = BigInteger("45d9f3b", 16) + } + + init { + val seedMask = BigInteger("FFFFFFFFFFFFFFFF", 16) + seed = seedInput.and(seedMask) + state = hashSeed(seed) + entropyPool = expandEntropy(seed) + order = MutableList(size) { it } + permute() + } + + private fun hashSeed(e: BigInteger): BigInteger { + val md = e.toString().sha256() + return readBigUInt64BE(md, 0).xor(readBigUInt64BE(md, 8)) + } + + private fun readBigUInt64BE(bytes: ByteArray, offset: Int): BigInteger { + var n = BigInteger.ZERO + for (i in 0 until 8) { + n = n.shiftLeft(8).or(BigInteger.valueOf((bytes[offset + i].toInt() and 0xFF).toLong())) + } + return n + } + + private fun expandEntropy(e: BigInteger): ByteArray = + MessageDigest.getInstance("SHA-512").digest(e.toString().toByteArray(StandardCharsets.UTF_8)) + + private fun sbox(e: Int): Int { + val t = intArrayOf(163, 95, 137, 13, 55, 193, 107, 228, 114, 185, 22, 243, 68, 218, 158, 40) + return t[e and 15] xor t[e shr 4 and 15] + } + + fun prng(): BigInteger { + state = state.xor(state.shiftLeft(11).and(MASK64)) + state = state.xor(state.shiftRight(19)) + state = state.xor(state.shiftLeft(7).and(MASK64)) + state = state.multiply(PRNG_MULT).and(MASK64) + return state + } + + private fun roundFunc(e: BigInteger, t: Int): BigInteger { + var n = e.xor(prng()).xor(BigInteger.valueOf(t.toLong())) + + val rot = n.shiftLeft(5).or(n.shiftRight(3)).and(MASK32) + n = rot.multiply(RND_MULT_32).and(MASK32) + + val sboxVal = sbox(n.and(MASK8).toInt()) + n = n.xor(BigInteger.valueOf(sboxVal.toLong())) + + n = n.xor(n.shiftRight(13)) + return n + } + + private fun feistelMix(e: Int, t: Int, rounds: Int): Pair { + var r = BigInteger.valueOf(e.toLong()) + var i = BigInteger.valueOf(t.toLong()) + for (round in 0 until rounds) { + val ent = entropyPool[round % entropyPool.size].toInt() and 0xFF + r = r.xor(roundFunc(i, ent)) + val secondArg = ent xor (round * 31 and 255) + i = i.xor(roundFunc(r, secondArg)) + } + return Pair(r, i) + } + + private fun permute() { + val half = size / 2 + val sizeBig = BigInteger.valueOf(size.toLong()) + + for (t in 0 until half) { + val n = t + half + val (rBig, iBig) = feistelMix(t, n, 4) + val s = rBig.mod(sizeBig).toInt() + val a = iBig.mod(sizeBig).toInt() + val tmp = order[s] + order[s] = order[a] + order[a] = tmp + } + + for (e in size - 1 downTo 1) { + val ent = entropyPool[e % entropyPool.size].toInt() and 0xFF + val idxBig = prng().add(BigInteger.valueOf(ent.toLong())).mod(BigInteger.valueOf((e + 1).toLong())) + val n = idxBig.toInt() + val tmp = order[e] + order[e] = order[n] + order[n] = tmp + } + } +} diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Scrambler.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Scrambler.kt new file mode 100644 index 000000000..0c0d9c74a --- /dev/null +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Scrambler.kt @@ -0,0 +1,125 @@ +package eu.kanade.tachiyomi.extension.en.kagane + +import java.math.BigInteger + +class Scrambler(private val seed: BigInteger, private val gridSize: Int) { + + private val totalPieces: Int = gridSize * gridSize + private val randomizer: Randomizer = Randomizer(seed, gridSize) + private val dependencyGraph: DependencyGraph + private val scramblePath: List + + init { + dependencyGraph = buildDependencyGraph() + scramblePath = generateScramblePath() + } + + private data class DependencyGraph( + val graph: MutableMap>, + val inDegree: MutableMap, + ) + + private fun buildDependencyGraph(): DependencyGraph { + val graph = mutableMapOf>() + val inDegree = mutableMapOf() + + for (n in 0 until totalPieces) { + inDegree[n] = 0 + graph[n] = mutableListOf() + } + + val rng = Randomizer(seed, gridSize) + + for (r in 0 until totalPieces) { + val i = (rng.prng() % BigInteger.valueOf(3) + BigInteger.valueOf(2)).toInt() + repeat(i) { + val j = (rng.prng() % BigInteger.valueOf(totalPieces.toLong())).toInt() + if (j != r && !wouldCreateCycle(graph, j, r)) { + graph[j]!!.add(r) + inDegree[r] = inDegree[r]!! + 1 + } + } + } + + for (r in 0 until totalPieces) { + if (inDegree[r] == 0) { + var tries = 0 + while (tries < 10) { + val s = (rng.prng() % BigInteger.valueOf(totalPieces.toLong())).toInt() + if (s != r && !wouldCreateCycle(graph, s, r)) { + graph[s]!!.add(r) + inDegree[r] = inDegree[r]!! + 1 + break + } + tries++ + } + } + } + + return DependencyGraph(graph, inDegree) + } + + private fun wouldCreateCycle(graph: Map>, target: Int, start: Int): Boolean { + val visited = mutableSetOf() + val stack = ArrayDeque() + stack.add(start) + + while (stack.isNotEmpty()) { + val n = stack.removeLast() + if (n == target) return true + if (!visited.add(n)) continue + graph[n]?.let { stack.addAll(it) } + } + return false + } + + private fun generateScramblePath(): List { + val graphCopy = dependencyGraph.graph.mapValues { it.value.toMutableList() }.toMutableMap() + val inDegreeCopy = dependencyGraph.inDegree.toMutableMap() + + val queue = ArrayDeque() + for (n in 0 until totalPieces) { + if (inDegreeCopy[n] == 0) { + queue.add(n) + } + } + + val order = mutableListOf() + while (queue.isNotEmpty()) { + val i = queue.removeFirst() + order.add(i) + val neighbors = graphCopy[i] + if (neighbors != null) { + for (e in neighbors) { + inDegreeCopy[e] = inDegreeCopy[e]!! - 1 + if (inDegreeCopy[e] == 0) { + queue.add(e) + } + } + } + } + return order + } + + fun getScrambleMapping(): List> { + var e = randomizer.order.toMutableList() + + if (scramblePath.size == totalPieces) { + val t = Array(totalPieces) { 0 } + for (i in scramblePath.indices) { + t[i] = scramblePath[i] + } + val n = Array(totalPieces) { 0 } + for (r in 0 until totalPieces) { + n[r] = e[t[r]] + } + e = n.toMutableList() + } + + val result = mutableListOf>() + for (n in 0 until totalPieces) { + result.add(n to e[n]) + } + return result + } +} diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Utils.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Utils.kt new file mode 100644 index 000000000..7bb0e2daf --- /dev/null +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Utils.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.extension.en.kagane + +import android.util.Base64 +import java.security.MessageDigest + +fun ByteArray.toBase64(): String { + return Base64.encodeToString(this, Base64.NO_WRAP) +} + +fun String.sha256(): ByteArray { + return MessageDigest + .getInstance("SHA-256") + .digest(toByteArray()) +}