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