BatoTo: Replace CryptoJS use with javax.crypto (#13562)
* Replace CryptoJS use with javax.crypto * Migrated BatoToCryptoUtils to a lib module * Replaced CryptoJS with javax.crypto for Mangapark
This commit is contained in:
parent
d463a0900a
commit
6147a40686
|
@ -0,0 +1,21 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
targetSdk = AndroidConfig.targetSdk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.kotlin.stdlib)
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="eu.kanade.tachiyomi.lib.cryptoaes" />
|
|
@ -0,0 +1,99 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.cryptoaes
|
||||||
|
// Thanks to Vlad on Stackoverflow: https://stackoverflow.com/a/63701411
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.Arrays
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conforming with CryptoJS AES method
|
||||||
|
*/
|
||||||
|
// see https://gist.github.com/thackerronak/554c985c3001b16810af5fc0eb5c358f
|
||||||
|
@Suppress("unused", "FunctionName")
|
||||||
|
object CryptoAES {
|
||||||
|
|
||||||
|
private const val KEY_SIZE = 256
|
||||||
|
private const val IV_SIZE = 128
|
||||||
|
private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING"
|
||||||
|
private const val AES = "AES"
|
||||||
|
private const val KDF_DIGEST = "MD5"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt
|
||||||
|
* Thanks Artjom B. for this: http://stackoverflow.com/a/29152379/4405051
|
||||||
|
* @param password passphrase
|
||||||
|
* @param cipherText encrypted string
|
||||||
|
*/
|
||||||
|
fun decrypt(cipherText: String, password: String): String {
|
||||||
|
try {
|
||||||
|
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
||||||
|
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
||||||
|
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
||||||
|
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
|
||||||
|
val cipher = Cipher.getInstance(HASH_CIPHER)
|
||||||
|
val keyS = SecretKeySpec(keyAndIV!![0], AES)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(keyAndIV!![1]))
|
||||||
|
return cipher.doFinal(cipherTextBytes).toString(Charsets.UTF_8)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a key and an initialization vector (IV) with the given salt and password.
|
||||||
|
*
|
||||||
|
* Thanks to @Codo on Stackoverflow (https://stackoverflow.com/a/41434590)
|
||||||
|
* This method is equivalent to OpenSSL's EVP_BytesToKey function
|
||||||
|
* (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c).
|
||||||
|
* By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data.
|
||||||
|
*
|
||||||
|
* @param keyLength the length of the generated key (in bytes)
|
||||||
|
* @param ivLength the length of the generated IV (in bytes)
|
||||||
|
* @param iterations the number of digestion rounds
|
||||||
|
* @param salt the salt data (8 bytes of data or `null`)
|
||||||
|
* @param password the password data (optional)
|
||||||
|
* @param md the message digest algorithm to use
|
||||||
|
* @return an two-element array with the generated key and IV
|
||||||
|
*/
|
||||||
|
private fun generateKeyAndIV(keyLength: Int, ivLength: Int, iterations: Int, salt: ByteArray, password: ByteArray, md: MessageDigest): Array<ByteArray?>? {
|
||||||
|
val digestLength = md.digestLength
|
||||||
|
val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
|
||||||
|
val generatedData = ByteArray(requiredLength)
|
||||||
|
var generatedLength = 0
|
||||||
|
return try {
|
||||||
|
md.reset()
|
||||||
|
|
||||||
|
// Repeat process until sufficient data has been generated
|
||||||
|
while (generatedLength < keyLength + ivLength) {
|
||||||
|
|
||||||
|
// Digest data (last digest if available, password data, salt if available)
|
||||||
|
if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength)
|
||||||
|
md.update(password)
|
||||||
|
if (salt != null) md.update(salt, 0, 8)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
|
||||||
|
// additional rounds
|
||||||
|
for (i in 1 until iterations) {
|
||||||
|
md.update(generatedData, generatedLength, digestLength)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
}
|
||||||
|
generatedLength += digestLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy key and IV into separate byte arrays
|
||||||
|
val result = arrayOfNulls<ByteArray>(2)
|
||||||
|
result[0] = generatedData.copyOfRange(0, keyLength)
|
||||||
|
if (ivLength > 0) result[1] = generatedData.copyOfRange(keyLength, keyLength + ivLength)
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
// Clean out temporary data
|
||||||
|
Arrays.fill(generatedData, 0.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.cryptoaes
|
||||||
|
/**
|
||||||
|
* Helper class to deobfuscate JavaScript strings encoded in JSFuck style.
|
||||||
|
*
|
||||||
|
* More info on JSFuck found [here](https://en.wikipedia.org/wiki/JSFuck).
|
||||||
|
*
|
||||||
|
* Currently only supports Numeric and decimal ('.') characters
|
||||||
|
*/
|
||||||
|
object Deobfuscator {
|
||||||
|
fun deobfuscateJsPassword(inputString: String): String {
|
||||||
|
var idx = 0
|
||||||
|
val brackets = listOf<Char>('[', '(')
|
||||||
|
var evaluatedString = StringBuilder()
|
||||||
|
while (idx < inputString.length) {
|
||||||
|
val chr = inputString[idx]
|
||||||
|
if (chr !in brackets) {
|
||||||
|
idx++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val closingIndex = getMatchingBracketIndex(idx, inputString)
|
||||||
|
if (chr == '[') {
|
||||||
|
val digit = calculateDigit(inputString.substring(idx, closingIndex))
|
||||||
|
evaluatedString.append(digit)
|
||||||
|
} else {
|
||||||
|
evaluatedString.append('.')
|
||||||
|
if (inputString.getOrNull(closingIndex + 1) == '[') {
|
||||||
|
val skippingIndex = getMatchingBracketIndex(closingIndex + 1, inputString)
|
||||||
|
idx = skippingIndex + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx = closingIndex + 1
|
||||||
|
}
|
||||||
|
return evaluatedString.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMatchingBracketIndex(openingIndex: Int, inputString: String): Int {
|
||||||
|
val openingBracket = inputString[openingIndex]
|
||||||
|
val closingBracket = when (openingBracket) {
|
||||||
|
'[' -> ']'
|
||||||
|
else -> ')'
|
||||||
|
}
|
||||||
|
var counter = 0
|
||||||
|
for (idx in openingIndex until inputString.length) {
|
||||||
|
if (inputString[idx] == openingBracket) counter++
|
||||||
|
if (inputString[idx] == closingBracket) counter--
|
||||||
|
|
||||||
|
if (counter == 0) return idx // found matching bracket
|
||||||
|
if (counter < 0) return -1 // unbalanced brackets
|
||||||
|
}
|
||||||
|
return -1 // matching bracket not found
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateDigit(inputSubString: String): Char {
|
||||||
|
/* 0 == '+[]'
|
||||||
|
1 == '+!+[]'
|
||||||
|
2 == '!+[]+!+[]'
|
||||||
|
3 == '!+[]+!+[]+!+[]'
|
||||||
|
...
|
||||||
|
therefore '!+[]' count equals the digit
|
||||||
|
if count equals 0, check for '+[]' just to be sure
|
||||||
|
*/
|
||||||
|
val digit = "\\!\\+\\[\\]".toRegex().findAll(inputSubString).count() // matches '!+[]'
|
||||||
|
if (digit == 0) {
|
||||||
|
if ("\\+\\[\\]".toRegex().findAll(inputSubString).count() == 1) { // matches '+[]'
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
} else if (digit in 1..9) {
|
||||||
|
return digit.digitToChar()
|
||||||
|
}
|
||||||
|
return '-' // Illegal digit
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,9 @@ project(":lib-dataimage").projectDir = File("lib/dataimage")
|
||||||
include(":lib-unpacker")
|
include(":lib-unpacker")
|
||||||
project(":lib-unpacker").projectDir = File("lib/unpacker")
|
project(":lib-unpacker").projectDir = File("lib/unpacker")
|
||||||
|
|
||||||
|
include(":lib-cryptoaes")
|
||||||
|
project(":lib-cryptoaes").projectDir = File("lib/cryptoaes")
|
||||||
|
|
||||||
if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
|
if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
|
||||||
// Local development (full project build)
|
// Local development (full project build)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,17 @@
|
||||||
|
## 1.3.30
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
* Replace CryptoJS with Native Kotlin Functions
|
||||||
|
* Remove QuickJS dependency
|
||||||
|
|
||||||
|
## 1.3.29
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
* Cleanup pageListParse function
|
||||||
|
* Replace Duktape with QuickJS
|
||||||
|
|
||||||
## 1.3.28
|
## 1.3.28
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
@ -149,7 +163,7 @@
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* Scanlotor support
|
* Scanlator support
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,12 @@ ext {
|
||||||
extName = 'Bato.to'
|
extName = 'Bato.to'
|
||||||
pkgNameSuffix = 'all.batoto'
|
pkgNameSuffix = 'all.batoto'
|
||||||
extClass = '.BatoToFactory'
|
extClass = '.BatoToFactory'
|
||||||
extVersionCode = 29
|
extVersionCode = 30
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib-cryptoaes'))
|
||||||
|
}
|
|
@ -5,7 +5,8 @@ import android.content.SharedPreferences
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import app.cash.quickjs.QuickJs
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||||
|
import eu.kanade.tachiyomi.lib.cryptoaes.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.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
@ -463,9 +464,8 @@ open class BatoTo(
|
||||||
val batoWord = script.substringAfter("const batoWord =").substringBefore(";").trim()
|
val batoWord = script.substringAfter("const batoWord =").substringBefore(";").trim()
|
||||||
val batoPass = script.substringAfter("const batoPass =").substringBefore(";").trim()
|
val batoPass = script.substringAfter("const batoPass =").substringBefore(";").trim()
|
||||||
|
|
||||||
val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($batoWord, $batoPass).toString(CryptoJS.enc.Utf8);"
|
val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(batoPass)
|
||||||
|
val imgAccListString = CryptoAES.decrypt(batoWord.removeSurrounding("\""), evaluatedPass)
|
||||||
val imgAccListString = QuickJs.create().use { it.evaluate(decryptScript).toString() }
|
|
||||||
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
|
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
|
||||||
|
|
||||||
return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) ->
|
return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) ->
|
||||||
|
@ -473,10 +473,6 @@ open class BatoTo(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cryptoJS by lazy {
|
|
||||||
client.newCall(GET(CryptoJSUrl, headers)).execute().body!!.string()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
private fun String.removeEntities(): String = Parser.unescapeEntities(this, true)
|
private fun String.removeEntities(): String = Parser.unescapeEntities(this, true)
|
||||||
|
@ -912,8 +908,6 @@ open class BatoTo(
|
||||||
).filterNot { it.value == siteLang }
|
).filterNot { it.value == siteLang }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CryptoJSUrl = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"
|
|
||||||
|
|
||||||
private const val MIRROR_PREF_KEY = "MIRROR"
|
private const val MIRROR_PREF_KEY = "MIRROR"
|
||||||
private const val MIRROR_PREF_TITLE = "Mirror"
|
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||||
private val MIRROR_PREF_ENTRIES = arrayOf(
|
private val MIRROR_PREF_ENTRIES = arrayOf(
|
||||||
|
|
|
@ -6,8 +6,12 @@ ext {
|
||||||
extName = 'MangaPark v3'
|
extName = 'MangaPark v3'
|
||||||
pkgNameSuffix = 'all.mangapark'
|
pkgNameSuffix = 'all.mangapark'
|
||||||
extClass = '.MangaParkFactory'
|
extClass = '.MangaParkFactory'
|
||||||
extVersionCode = 17
|
extVersionCode = 18
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib-cryptoaes'))
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.mangapark
|
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||||
|
|
||||||
import app.cash.quickjs.QuickJs
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||||
|
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
@ -260,9 +261,8 @@ open class MangaPark(
|
||||||
val amWord = script.substringAfter("const amWord =").substringBefore(";").trim()
|
val amWord = script.substringAfter("const amWord =").substringBefore(";").trim()
|
||||||
val amPass = script.substringAfter("const amPass =").substringBefore(";").trim()
|
val amPass = script.substringAfter("const amPass =").substringBefore(";").trim()
|
||||||
|
|
||||||
val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($amWord, $amPass).toString(CryptoJS.enc.Utf8);"
|
val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(amPass)
|
||||||
|
val imgAccListString = CryptoAES.decrypt(amWord.removeSurrounding("\""), evaluatedPass)
|
||||||
val imgAccListString = QuickJs.create().use { it.evaluate(decryptScript).toString() }
|
|
||||||
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
|
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
|
||||||
|
|
||||||
return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) ->
|
return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) ->
|
||||||
|
@ -270,10 +270,6 @@ open class MangaPark(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cryptoJS by lazy {
|
|
||||||
client.newCall(GET(CryptoJSUrl, headers)).execute().body!!.string()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = mpFilters.getFilterList()
|
override fun getFilterList() = mpFilters.getFilterList()
|
||||||
|
|
||||||
// Unused Stuff
|
// Unused Stuff
|
||||||
|
@ -287,7 +283,5 @@ open class MangaPark(
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val PREFIX_ID_SEARCH = "id:"
|
const val PREFIX_ID_SEARCH = "id:"
|
||||||
|
|
||||||
const val CryptoJSUrl = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue