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:
pratyush3757 2022-09-25 00:23:39 +05:30 committed by GitHub
parent d463a0900a
commit 6147a40686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 231 additions and 23 deletions

View File

@ -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)
}

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.lib.cryptoaes" />

View File

@ -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())
}
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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'))
}

View File

@ -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(

View File

@ -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'))
}

View File

@ -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"
} }
} }