From e8661648da64a643ea2b06978e65f518d3d646b1 Mon Sep 17 00:00:00 2001 From: hexated Date: Tue, 29 Aug 2023 20:28:15 +0700 Subject: [PATCH] sora: fix Jeniusplay --- IdlixProvider/build.gradle.kts | 2 +- .../main/kotlin/com/hexated/IdlixProvider.kt | 39 +-- .../src/main/kotlin/com/hexated/Utils.kt | 95 ++++++ SoraStream/build.gradle.kts | 2 +- .../main/kotlin/com/hexated/SoraExtractor.kt | 100 ++---- .../src/main/kotlin/com/hexated/SoraParser.kt | 20 +- .../src/main/kotlin/com/hexated/SoraStream.kt | 10 +- .../main/kotlin/com/hexated/SoraStreamLite.kt | 10 - .../src/main/kotlin/com/hexated/SoraUtils.kt | 288 +++++------------- 9 files changed, 224 insertions(+), 342 deletions(-) create mode 100644 IdlixProvider/src/main/kotlin/com/hexated/Utils.kt diff --git a/IdlixProvider/build.gradle.kts b/IdlixProvider/build.gradle.kts index 6533b773..44b7432e 100644 --- a/IdlixProvider/build.gradle.kts +++ b/IdlixProvider/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 12 +version = 13 cloudstream { diff --git a/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt b/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt index 064c18dd..9d4afcaf 100644 --- a/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt +++ b/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt @@ -13,13 +13,12 @@ import org.jsoup.nodes.Element import java.net.URI class IdlixProvider : MainAPI() { - override var mainUrl = "https://tv.idlixprime.com" + override var mainUrl = "https://tv.idlixplus.net" private var directUrl = mainUrl override var name = "Idlix" override val hasMainPage = true override var lang = "id" override val hasDownloadSupport = true - private val session = Session(Requests().baseClient) override val supportedTypes = setOf( TvType.Movie, TvType.TvSeries, @@ -51,9 +50,9 @@ class IdlixProvider : MainAPI() { val url = request.data.split("?") val nonPaged = request.name == "Featured" && page <= 1 val req = if (nonPaged) { - session.get(request.data) + app.get(request.data) } else { - session.get("${url.first()}$page/?${url.lastOrNull()}") + app.get("${url.first()}$page/?${url.lastOrNull()}") } mainUrl = getBaseUrl(req.url) val document = req.document @@ -98,7 +97,7 @@ class IdlixProvider : MainAPI() { } override suspend fun search(query: String): List { - val req = session.get("$mainUrl/search/$query") + val req = app.get("$mainUrl/search/$query") mainUrl = getBaseUrl(req.url) val document = req.document return document.select("div.result-item").map { @@ -113,7 +112,7 @@ class IdlixProvider : MainAPI() { } override suspend fun load(url: String): LoadResponse { - val request = session.get(url) + val request = app.get(url) directUrl = getBaseUrl(request.url) val document = request.document val title = @@ -193,7 +192,7 @@ class IdlixProvider : MainAPI() { callback: (ExtractorLink) -> Unit ): Boolean { - val document = session.get(data).document + val document = app.get(data).document val id = document.select("meta#dooplay-ajax-counter").attr("data-postid") val type = if (data.contains("/movie/")) "movie" else "tv" @@ -201,22 +200,18 @@ class IdlixProvider : MainAPI() { it.attr("data-nume") }.apmap { nume -> safeApiCall { - var source = session.post( - url = "$directUrl/wp-admin/admin-ajax.php", - data = mapOf( - "action" to "doo_player_ajax", - "post" to id, - "nume" to nume, - "type" to type - ), + val source = app.get( + url = "$directUrl/wp-json/dooplayer/v2/$id/$type/$nume", headers = mapOf("X-Requested-With" to "XMLHttpRequest"), referer = data - ).let { tryParseJson(it.text) }?.embed_url ?: return@safeApiCall + ).let { tryParseJson(it.text) } ?: return@safeApiCall - if (source.startsWith("https://uservideo.xyz")) { - source = app.get(source).document.select("iframe").attr("src") + var decrypted = AesHelper.cryptoAESHandler(source.embed_url,source.key.toByteArray(), false)?.fixBloat() ?: return@safeApiCall + + if (decrypted.startsWith("https://uservideo.xyz")) { + decrypted = app.get(decrypted).document.select("iframe").attr("src") } - loadExtractor(source, directUrl, subtitleCallback, callback) + loadExtractor(decrypted, "$directUrl/", subtitleCallback, callback) } } @@ -224,9 +219,15 @@ class IdlixProvider : MainAPI() { return true } + private fun String.fixBloat() : String { + return this.replace("\"", "").replace("\\", "") + } + data class ResponseHash( @JsonProperty("embed_url") val embed_url: String, + @JsonProperty("key") val key: String, @JsonProperty("type") val type: String?, ) + } \ No newline at end of file diff --git a/IdlixProvider/src/main/kotlin/com/hexated/Utils.kt b/IdlixProvider/src/main/kotlin/com/hexated/Utils.kt new file mode 100644 index 00000000..96bb5a14 --- /dev/null +++ b/IdlixProvider/src/main/kotlin/com/hexated/Utils.kt @@ -0,0 +1,95 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils +import java.security.DigestException +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object AesHelper { + + fun cryptoAESHandler( + data: String, + pass: ByteArray, + encrypt: Boolean = true, + padding: String = "AES/CBC/PKCS5PADDING", + ): String? { + val parse = AppUtils.tryParseJson(data) ?: return null + val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: throw ErrorLoadingException("failed to generate key") + val cipher = Cipher.getInstance(padding) + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(parse.ct))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(parse.ct.toByteArray())) + } + } + + // https://stackoverflow.com/a/41434590/8166854 + private fun generateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = "MD5", + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): List? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return listOf( + generatedData.copyOfRange(0, keyLength), + generatedData.copyOfRange(keyLength, targetKeySize) + ) + } catch (e: DigestException) { + return null + } + } + + private fun String.hexToByteArray(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + private data class AesData( + @JsonProperty("ct") val ct: String, + @JsonProperty("iv") val iv: String, + @JsonProperty("s") val s: String + ) + +} \ No newline at end of file diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts index a9ef66fb..71a6eef1 100644 --- a/SoraStream/build.gradle.kts +++ b/SoraStream/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.konan.properties.Properties // use an integer for version numbers -version = 159 +version = 160 android { defaultConfig { diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index 5e131676..b73e815f 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -1,5 +1,6 @@ package com.hexated +import com.hexated.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -414,7 +415,7 @@ object SoraExtractor : SoraStream() { } else { "$idlixAPI/episode/$fixTitle-season-$season-episode-$episode" } - invokeWpmovies(url, subtitleCallback, callback) + invokeWpmovies(url, subtitleCallback, callback, encrypt = true) } suspend fun invokeMultimovies( @@ -455,8 +456,13 @@ object SoraExtractor : SoraStream() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit, fixIframe: Boolean = false, + encrypt: Boolean = false, ) { - val res = session.get(url ?: return) + fun String.fixBloat() : String { + return this.replace("\"", "").replace("\\", "") + } + val res = app.get(url ?: return) + val headers = mapOf("X-Requested-With" to "XMLHttpRequest") val referer = getBaseUrl(res.url) val document = res.document document.select("ul#playeroptionsul > li").map { @@ -466,13 +472,21 @@ object SoraExtractor : SoraStream() { it.attr("data-type") ) }.apmap { (id, nume, type) -> - val json = session.post( + val json = if(encrypt) app.get( + url = "$referer/wp-json/dooplayer/v2/$id/$type/$nume", + headers = headers, + referer = url + ) else app.post( url = "$referer/wp-admin/admin-ajax.php", data = mapOf( "action" to "doo_player_ajax", "post" to id, "nume" to nume, "type" to type - ), headers = mapOf("X-Requested-With" to "XMLHttpRequest"), referer = url + ), headers = headers, referer = url ) - val source = tryParseJson(json.text)?.embed_url?.let { - if (fixIframe) Jsoup.parse(it).select("IFRAME").attr("SRC") else it + val source = tryParseJson(json.text)?.let { + when { + encrypt -> cryptoAESHandler(it.embed_url,it.key.toByteArray(), false)?.fixBloat() + fixIframe -> Jsoup.parse(it.embed_url).select("IFRAME").attr("SRC") + else -> it.embed_url + } } ?: return@apmap if (!source.contains("youtube")) { loadExtractor(source, "$referer/", subtitleCallback, callback) @@ -2540,80 +2554,6 @@ object SoraExtractor : SoraStream() { } - suspend fun invokePutlocker( - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - val query = if (season == null) { - title - } else { - "$title - season $season" - } - - val res = app.get("$putlockerAPI/movie/search/$query").document - val scripData = res.select("div.movies-list div.ml-item").map { - it.selectFirst("h2")?.text() to it.selectFirst("a")?.attr("href") - } - val script = if (scripData.size == 1) { - scripData.first() - } else { - scripData.find { - if (season == null) { - it.first.equals(title, true) || (it.first?.contains( - "$title", true - ) == true && it.first?.contains("$year") == true) - } else { - it.first?.contains("$title", true) == true && it.first?.contains( - "Season $season", true - ) == true - } - } - } - - val id = fixUrl(script?.second ?: return).split("-").lastOrNull()?.removeSuffix("/") - val iframe = app.get("$putlockerAPI/ajax/movie_episodes/$id") - .parsedSafe()?.html?.let { Jsoup.parse(it) }?.let { server -> - if (season == null) { - server.select("div.les-content a").map { - it.attr("data-id") to it.attr("data-server") - } - } else { - server.select("div.les-content a").map { it } - .filter { it.text().contains("Episode $episode", true) }.map { - it.attr("data-id") to it.attr("data-server") - } - } - } - - iframe?.apmap { - delay(3000) - val embedUrl = app.get("$putlockerAPI/ajax/movie_embed/${it.first}") - .parsedSafe()?.src ?: return@apmap null - val sources = extractPutlockerSources(embedUrl)?.parsedSafe() - - argamap( - { - sources?.callback(embedUrl, "Server ${it.second}", callback) - }, - { - if (!sources?.backupLink.isNullOrBlank()) { - extractPutlockerSources(sources?.backupLink)?.parsedSafe() - ?.callback( - embedUrl, "Backup ${it.second}", callback - ) - } else { - return@argamap - } - }, - ) - - } - - } - suspend fun invokeCryMovies( imdbId: String? = null, title: String? = null, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt b/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt index db94285e..f01fd804 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt @@ -65,6 +65,7 @@ data class HdMovieBoxIframe( data class ResponseHash( @JsonProperty("embed_url") val embed_url: String, + @JsonProperty("key") val key: String, @JsonProperty("type") val type: String?, ) @@ -206,25 +207,6 @@ data class WatchOnlineResponse( @JsonProperty("subtitles") val subtitles: Any? = null, ) -data class PutlockerEpisodes( - @JsonProperty("html") val html: String? = null, -) - -data class PutlockerEmbed( - @JsonProperty("src") val src: String? = null, -) - -data class PutlockerSources( - @JsonProperty("file") val file: String, - @JsonProperty("label") val label: String? = null, - @JsonProperty("type") val type: String? = null, -) - -data class PutlockerResponses( - @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), - @JsonProperty("backupLink") val backupLink: String? = null, -) - data class CryMoviesProxyHeaders( @JsonProperty("request") val request: Map?, ) diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt index 16c74135..4f163d30 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -34,7 +34,6 @@ import com.hexated.SoraExtractor.invokeMoviezAdd import com.hexated.SoraExtractor.invokeNavy import com.hexated.SoraExtractor.invokeNinetv import com.hexated.SoraExtractor.invokeNowTv -import com.hexated.SoraExtractor.invokePutlocker import com.hexated.SoraExtractor.invokeRStream import com.hexated.SoraExtractor.invokeRidomovies import com.hexated.SoraExtractor.invokeShinobiMovies @@ -55,7 +54,6 @@ import com.hexated.SoraExtractor.invokeWatchsomuch import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.extractors.VidSrcExtractor -import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.ExtractorLink @@ -94,7 +92,7 @@ open class SoraStream : TmdbProvider() { const val hdMovieBoxAPI = "https://hdmoviebox.net" const val dreamfilmAPI = "https://dreamfilmsw.net" const val series9API = "https://series9.cx" - const val idlixAPI = "https://tv.idlixprime.com" + const val idlixAPI = "https://tv.idlixplus.net" const val noverseAPI = "https://www.nollyverse.com" const val filmxyAPI = "https://www.filmxy.vip" const val kimcartoonAPI = "https://kimcartoon.li" @@ -102,7 +100,7 @@ open class SoraStream : TmdbProvider() { const val crunchyrollAPI = "https://beta-api.crunchyroll.com" const val kissKhAPI = "https://kisskh.co" const val lingAPI = "https://ling-online.net" - const val uhdmoviesAPI = "https://uhdmovies.actor" + const val uhdmoviesAPI = "https://uhdmovies.wiki" const val fwatayakoAPI = "https://5100.svetacdn.in" const val gMoviesAPI = "https://gdrivemovies.xyz" const val fdMoviesAPI = "https://freedrivemovie.lol" @@ -118,7 +116,6 @@ open class SoraStream : TmdbProvider() { const val ask4MoviesAPI = "https://ask4movie.nl" const val watchOnlineAPI = "https://watchonline.ag" const val nineTvAPI = "https://moviesapi.club" - const val putlockerAPI = "https://ww7.putlocker.vip" const val fmoviesAPI = "https://fmovies.to" const val nowTvAPI = "https://myfilestorage.xyz" const val gokuAPI = "https://goku.sx" @@ -545,9 +542,6 @@ open class SoraStream : TmdbProvider() { callback ) }, - { - invokePutlocker(res.title, res.year, res.season, res.episode, callback) - }, { invokeTvMovies(res.title, res.season, res.episode, callback) }, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt index c5bf1a6d..ebe91e71 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt @@ -21,7 +21,6 @@ import com.hexated.SoraExtractor.invokeMovieHab import com.hexated.SoraExtractor.invokeNavy import com.hexated.SoraExtractor.invokeNinetv import com.hexated.SoraExtractor.invokeNowTv -import com.hexated.SoraExtractor.invokePutlocker import com.hexated.SoraExtractor.invokeRStream import com.hexated.SoraExtractor.invokeRidomovies import com.hexated.SoraExtractor.invokeSeries9 @@ -55,15 +54,6 @@ class SoraStreamLite : SoraStream() { val res = AppUtils.parseJson(data) argamap( - { - invokePutlocker( - res.title, - res.year, - res.season, - res.episode, - callback - ) - }, { invokeWatchsomuch( res.imdbId, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index f256bf87..3f640572 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -1,6 +1,7 @@ package com.hexated import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.hexated.DumpUtils.queryApi import com.hexated.SoraStream.Companion.anilistAPI import com.hexated.SoraStream.Companion.base64DecodeAPI @@ -10,7 +11,6 @@ import com.hexated.SoraStream.Companion.filmxyAPI import com.hexated.SoraStream.Companion.fmoviesAPI import com.hexated.SoraStream.Companion.gdbot import com.hexated.SoraStream.Companion.malsyncAPI -import com.hexated.SoraStream.Companion.putlockerAPI import com.hexated.SoraStream.Companion.smashyStreamAPI import com.hexated.SoraStream.Companion.tvMoviesAPI import com.hexated.SoraStream.Companion.watchOnlineAPI @@ -41,11 +41,9 @@ import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import kotlin.collections.ArrayList -import kotlin.math.min val bflixChipperKey = base64DecodeAPI("Yjc=ejM=TzA=YTk=WHE=WnU=bXU=RFo=") const val bflixKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -const val otakuzBaseUrl = "https://otakuz.live/" val encodedIndex = arrayOf( "GamMovies", "JSMovies", @@ -1055,52 +1053,6 @@ suspend fun getCrunchyrollIdFromMalSync(aniId: String?): String? { ?: regex.find("$crunchyroll")?.groupValues?.getOrNull(1) } -suspend fun extractPutlockerSources(url: String?): NiceResponse? { - val embedHost = url?.substringBefore("/embed-player") - val player = app.get( - url ?: return null, - referer = "${putlockerAPI}/" - ).document.select("div#player") - - val text = "\"${player.attr("data-id")}\"" - val password = player.attr("data-hash") - val cipher = CryptoAES.plEncrypt(password, text) - - return app.get( - "$embedHost/ajax/getSources/", params = mapOf( - "id" to cipher.cipherText, - "h" to cipher.password, - "a" to cipher.iv, - "t" to cipher.salt, - ), referer = url - ) -} - -suspend fun PutlockerResponses?.callback( - referer: String, - server: String, - callback: (ExtractorLink) -> Unit -) { - val ref = getBaseUrl(referer) - this?.sources?.map { source -> - val request = app.get(source.file, referer = ref) - callback.invoke( - ExtractorLink( - "Putlocker [$server]", - "Putlocker [$server]", - if (!request.isSuccessful) return@map null else source.file, - ref, - if (source.file.contains("m3u8")) getPutlockerQuality(request.text) else source.label?.replace( - Regex("[Pp]"), - "" - )?.trim()?.toIntOrNull() - ?: Qualities.P720.value, - source.file.contains("m3u8") - ) - ) - } -} - suspend fun convertTmdbToAnimeId( title: String?, date: String?, @@ -1655,161 +1607,6 @@ private enum class Symbol(val decimalValue: Int) { } } -// code found on https://stackoverflow.com/a/63701411 - -/** - * Conforming with CryptoJS AES method - */ -// see https://gist.github.com/thackerronak/554c985c3001b16810af5fc0eb5c358f -@Suppress("unused", "FunctionName", "SameParameterValue") -object CryptoAES { - - private const val KEY_SIZE = 256 - private const val IV_SIZE = 128 - private const val HASH_CIPHER = "AES/CBC/PKCS5Padding" - private const val AES = "AES" - private const val KDF_DIGEST = "MD5" - - // Seriously crypto-js, what's wrong with you? - private const val APPEND = "Salted__" - - /** - * Encrypt - * @param password passphrase - * @param plainText plain string - */ - fun encrypt(password: String, plainText: String): String { - val saltBytes = generateSalt(8) - val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_SIZE / 8) - EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) - val keyS = SecretKeySpec(key, AES) - val cipher = Cipher.getInstance(HASH_CIPHER) - val ivSpec = IvParameterSpec(iv) - cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec) - val cipherText = cipher.doFinal(plainText.toByteArray()) - // Thanks kientux for this: https://gist.github.com/kientux/bb48259c6f2133e628ad - // Create CryptoJS-like encrypted! - val sBytes = APPEND.toByteArray() - val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size) - System.arraycopy(sBytes, 0, b, 0, sBytes.size) - System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size) - System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size) - val bEncode = Base64.encode(b, Base64.NO_WRAP) - return String(bEncode) - } - - fun plEncrypt(password: String, plainText: String): EncryptResult { - val saltBytes = generateSalt(8) - val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_SIZE / 8) - EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) - val keyS = SecretKeySpec(key, AES) - val cipher = Cipher.getInstance(HASH_CIPHER) - val ivSpec = IvParameterSpec(iv) - cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec) - val cipherText = cipher.doFinal(plainText.toByteArray()) - val bEncode = Base64.encode(cipherText, Base64.NO_WRAP) - return EncryptResult( - String(bEncode).toHex(), - password.toHex(), - saltBytes.toHex(), - iv.toHex() - ) - } - - /** - * Decrypt - * Thanks Artjom B. for this: http://stackoverflow.com/a/29152379/4405051 - * @param password passphrase - * @param cipherText encrypted string - */ - fun decrypt(password: String, cipherText: String): String { - val ctBytes = Base64.decode(cipherText.toByteArray(), Base64.NO_WRAP) - val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16) - val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size) - val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_SIZE / 8) - EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) - val cipher = Cipher.getInstance(HASH_CIPHER) - val keyS = SecretKeySpec(key, AES) - cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv)) - val plainText = cipher.doFinal(cipherTextBytes) - return String(plainText) - } - - private fun EvpKDF( - password: ByteArray, - keySize: Int, - ivSize: Int, - salt: ByteArray, - resultKey: ByteArray, - resultIv: ByteArray - ): ByteArray { - return EvpKDF(password, keySize, ivSize, salt, 1, KDF_DIGEST, resultKey, resultIv) - } - - @Suppress("NAME_SHADOWING") - private fun EvpKDF( - password: ByteArray, - keySize: Int, - ivSize: Int, - salt: ByteArray, - iterations: Int, - hashAlgorithm: String, - resultKey: ByteArray, - resultIv: ByteArray - ): ByteArray { - val keySize = keySize / 32 - val ivSize = ivSize / 32 - val targetKeySize = keySize + ivSize - val derivedBytes = ByteArray(targetKeySize * 4) - var numberOfDerivedWords = 0 - var block: ByteArray? = null - val hash = MessageDigest.getInstance(hashAlgorithm) - while (numberOfDerivedWords < targetKeySize) { - if (block != null) { - hash.update(block) - } - hash.update(password) - block = hash.digest(salt) - hash.reset() - // Iterations - for (i in 1 until iterations) { - block = hash.digest(block!!) - hash.reset() - } - System.arraycopy( - block!!, 0, derivedBytes, numberOfDerivedWords * 4, - min(block.size, (targetKeySize - numberOfDerivedWords) * 4) - ) - numberOfDerivedWords += block.size / 4 - } - System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4) - System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4) - return derivedBytes // key + iv - } - - private fun generateSalt(length: Int): ByteArray { - return ByteArray(length).apply { - SecureRandom().nextBytes(this) - } - } - - private fun ByteArray.toHex(): String = - joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } - - private fun String.toHex(): String = toByteArray().toHex() - - data class EncryptResult( - val cipherText: String, - val password: String, - val salt: String, - val iv: String - ) - -} - object DumpUtils { private val deviceId = getDeviceId() @@ -1926,4 +1723,87 @@ object RSAEncryptionHelper { exception.printStackTrace() null } +} + +object AesHelper { + + fun cryptoAESHandler( + data: String, + pass: ByteArray, + encrypt: Boolean = true, + padding: String = "AES/CBC/PKCS5PADDING", + ): String? { + val parse = AppUtils.tryParseJson(data) ?: return null + val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: throw ErrorLoadingException("failed to generate key") + val cipher = Cipher.getInstance(padding) + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(parse.ct))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(parse.ct.toByteArray())) + } + } + + // https://stackoverflow.com/a/41434590/8166854 + private fun generateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = "MD5", + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): List? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return listOf( + generatedData.copyOfRange(0, keyLength), + generatedData.copyOfRange(keyLength, targetKeySize) + ) + } catch (e: DigestException) { + return null + } + } + + private fun String.hexToByteArray(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + private data class AesData( + @JsonProperty("ct") val ct: String, + @JsonProperty("iv") val iv: String, + @JsonProperty("s") val s: String + ) + } \ No newline at end of file