sora: fix Jeniusplay

This commit is contained in:
hexated 2023-08-29 20:28:15 +07:00
parent 3b1d788587
commit e8661648da
9 changed files with 224 additions and 342 deletions

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 12
version = 13
cloudstream {

View file

@ -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<SearchResponse> {
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<ResponseHash>(it.text) }?.embed_url ?: return@safeApiCall
).let { tryParseJson<ResponseHash>(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?,
)
}

View file

@ -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<AesData>(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<ByteArray>? {
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
)
}

View file

@ -1,7 +1,7 @@
import org.jetbrains.kotlin.konan.properties.Properties
// use an integer for version numbers
version = 159
version = 160
android {
defaultConfig {

View file

@ -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<ResponseHash>(json.text)?.embed_url?.let {
if (fixIframe) Jsoup.parse(it).select("IFRAME").attr("SRC") else it
val source = tryParseJson<ResponseHash>(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<PutlockerEpisodes>()?.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<PutlockerEmbed>()?.src ?: return@apmap null
val sources = extractPutlockerSources(embedUrl)?.parsedSafe<PutlockerResponses>()
argamap(
{
sources?.callback(embedUrl, "Server ${it.second}", callback)
},
{
if (!sources?.backupLink.isNullOrBlank()) {
extractPutlockerSources(sources?.backupLink)?.parsedSafe<PutlockerResponses>()
?.callback(
embedUrl, "Backup ${it.second}", callback
)
} else {
return@argamap
}
},
)
}
}
suspend fun invokeCryMovies(
imdbId: String? = null,
title: String? = null,

View file

@ -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<PutlockerSources>? = arrayListOf(),
@JsonProperty("backupLink") val backupLink: String? = null,
)
data class CryMoviesProxyHeaders(
@JsonProperty("request") val request: Map<String, String>?,
)

View file

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

View file

@ -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<LinkData>(data)
argamap(
{
invokePutlocker(
res.title,
res.year,
res.season,
res.episode,
callback
)
},
{
invokeWatchsomuch(
res.imdbId,

View file

@ -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<AesData>(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<ByteArray>? {
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
)
}