diff --git a/lib/cryptoaes/build.gradle.kts b/lib/cryptoaes/build.gradle.kts
new file mode 100644
index 000000000..5eaa29645
--- /dev/null
+++ b/lib/cryptoaes/build.gradle.kts
@@ -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)
+}
diff --git a/lib/cryptoaes/src/main/AndroidManifest.xml b/lib/cryptoaes/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1ac16ea73
--- /dev/null
+++ b/lib/cryptoaes/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt b/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt
new file mode 100644
index 000000000..58d219f70
--- /dev/null
+++ b/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt
@@ -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? {
+ 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(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())
+ }
+ }
+}
diff --git a/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/Deobfuscator.kt b/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/Deobfuscator.kt
new file mode 100644
index 000000000..e1edf4843
--- /dev/null
+++ b/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/Deobfuscator.kt
@@ -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('[', '(')
+ 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
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index af6836a8e..70961dc94 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -6,6 +6,9 @@ project(":lib-dataimage").projectDir = File("lib/dataimage")
include(":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") {
// Local development (full project build)
diff --git a/src/all/batoto/CHANGELOG.md b/src/all/batoto/CHANGELOG.md
index 7cd25b5d7..e240bf4e9 100644
--- a/src/all/batoto/CHANGELOG.md
+++ b/src/all/batoto/CHANGELOG.md
@@ -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
### Features
@@ -149,7 +163,7 @@
### Features
-* Scanlotor support
+* Scanlator support
### Fix
diff --git a/src/all/batoto/build.gradle b/src/all/batoto/build.gradle
index 46796c79f..b7892ea02 100644
--- a/src/all/batoto/build.gradle
+++ b/src/all/batoto/build.gradle
@@ -6,8 +6,12 @@ ext {
extName = 'Bato.to'
pkgNameSuffix = 'all.batoto'
extClass = '.BatoToFactory'
- extVersionCode = 29
+ extVersionCode = 30
isNsfw = true
}
apply from: "$rootDir/common.gradle"
+
+dependencies {
+ implementation(project(':lib-cryptoaes'))
+}
\ No newline at end of file
diff --git a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt
index 6a0f96fd0..2e3745dea 100644
--- a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt
+++ b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt
@@ -5,7 +5,8 @@ import android.content.SharedPreferences
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
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.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
@@ -463,9 +464,8 @@ open class BatoTo(
val batoWord = script.substringAfter("const batoWord =").substringBefore(";").trim()
val batoPass = script.substringAfter("const batoPass =").substringBefore(";").trim()
- val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($batoWord, $batoPass).toString(CryptoJS.enc.Utf8);"
-
- val imgAccListString = QuickJs.create().use { it.evaluate(decryptScript).toString() }
+ val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(batoPass)
+ val imgAccListString = CryptoAES.decrypt(batoWord.removeSurrounding("\""), evaluatedPass)
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
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")
private fun String.removeEntities(): String = Parser.unescapeEntities(this, true)
@@ -912,8 +908,6 @@ open class BatoTo(
).filterNot { it.value == siteLang }
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_TITLE = "Mirror"
private val MIRROR_PREF_ENTRIES = arrayOf(
diff --git a/src/all/mangapark/build.gradle b/src/all/mangapark/build.gradle
index cb2b73afe..03046c8c2 100644
--- a/src/all/mangapark/build.gradle
+++ b/src/all/mangapark/build.gradle
@@ -6,8 +6,12 @@ ext {
extName = 'MangaPark v3'
pkgNameSuffix = 'all.mangapark'
extClass = '.MangaParkFactory'
- extVersionCode = 17
+ extVersionCode = 18
isNsfw = true
}
apply from: "$rootDir/common.gradle"
+
+dependencies {
+ implementation(project(':lib-cryptoaes'))
+}
\ No newline at end of file
diff --git a/src/all/mangapark/src/eu/kanade/tachiyomi/extension/all/mangapark/MangaPark.kt b/src/all/mangapark/src/eu/kanade/tachiyomi/extension/all/mangapark/MangaPark.kt
index 522d55839..57ebde103 100644
--- a/src/all/mangapark/src/eu/kanade/tachiyomi/extension/all/mangapark/MangaPark.kt
+++ b/src/all/mangapark/src/eu/kanade/tachiyomi/extension/all/mangapark/MangaPark.kt
@@ -1,6 +1,7 @@
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.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
@@ -260,9 +261,8 @@ open class MangaPark(
val amWord = script.substringAfter("const amWord =").substringBefore(";").trim()
val amPass = script.substringAfter("const amPass =").substringBefore(";").trim()
- val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($amWord, $amPass).toString(CryptoJS.enc.Utf8);"
-
- val imgAccListString = QuickJs.create().use { it.evaluate(decryptScript).toString() }
+ val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(amPass)
+ val imgAccListString = CryptoAES.decrypt(amWord.removeSurrounding("\""), evaluatedPass)
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
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()
// Unused Stuff
@@ -287,7 +283,5 @@ open class MangaPark(
companion object {
const val PREFIX_ID_SEARCH = "id:"
-
- const val CryptoJSUrl = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"
}
}