cloudstream-extensions-hexated/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt

1685 lines
55 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.hexated
import android.util.Base64
import com.hexated.DumpUtils.queryApi
import com.hexated.SoraStream.Companion.anilistAPI
import com.hexated.SoraStream.Companion.crunchyrollAPI
import com.hexated.SoraStream.Companion.filmxyAPI
import com.hexated.SoraStream.Companion.gdbot
import com.hexated.SoraStream.Companion.hdmovies4uAPI
import com.hexated.SoraStream.Companion.malsyncAPI
import com.hexated.SoraStream.Companion.smashyStreamAPI
import com.hexated.SoraStream.Companion.tvMoviesAPI
import com.hexated.SoraStream.Companion.watchflxAPI
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.nicehttp.NiceResponse
import com.lagradost.nicehttp.RequestBodyTypes
import com.lagradost.nicehttp.requestCreator
import kotlinx.coroutines.delay
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.nodes.Document
import java.math.BigInteger
import java.net.*
import java.nio.charset.StandardCharsets
import java.security.*
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.collections.ArrayList
import kotlin.math.min
var watchflxCookies: Map<String, String>? = null
var filmxyCookies: Map<String, String>? = null
var sfServer: String? = null
val encodedIndex = arrayOf(
"GamMovies",
"JSMovies",
"BlackMovies",
"CodexMovies",
"RinzryMovies",
"EdithxMovies",
"XtremeMovies",
"PapaonMovies[1]",
"PapaonMovies[2]",
"JmdkhMovies",
"RubyMovies",
"ShinobiMovies",
"VitoenMovies",
)
val lockedIndex = arrayOf(
"CodexMovies",
"EdithxMovies",
)
val mkvIndex = arrayOf(
"EdithxMovies",
"JmdkhMovies",
)
val untrimmedIndex = arrayOf(
"PapaonMovies[1]",
"PapaonMovies[2]",
"EdithxMovies",
)
val needRefererIndex = arrayOf(
"ShinobiMovies",
)
val ddomainIndex = arrayOf(
"RinzryMovies",
"ShinobiMovies"
)
val mimeType = arrayOf(
"video/x-matroska",
"video/mp4",
"video/x-msvideo"
)
fun String.filterIframe(
seasonNum: Int? = null,
lastSeason: Int? = null,
year: Int?,
title: String?
): Boolean {
val slug = title.createSlug()
val dotSlug = slug?.replace("-", ".")
val spaceSlug = slug?.replace("-", " ")
return if (seasonNum != null) {
if (lastSeason == 1) {
this.contains(Regex("(?i)(S0?$seasonNum)|(Season\\s0?$seasonNum)|(\\d{3,4}p)")) && !this.contains(
"Download",
true
)
} else {
this.contains(Regex("(?i)(S0?$seasonNum)|(Season\\s0?$seasonNum)")) && !this.contains(
"Download",
true
)
}
} else {
this.contains(Regex("(?i)($year)|($dotSlug)|($spaceSlug)")) && !this.contains(
"Download",
true
)
}
}
fun String.filterMedia(title: String?, yearNum: Int?, seasonNum: Int?): Boolean {
val fixTitle = title.createSlug()?.replace("-", " ")
return if (seasonNum != null) {
when {
seasonNum > 1 -> this.contains(Regex("(?i)(Season\\s0?1-0?$seasonNum)|(S0?1-S?0?$seasonNum)")) && this.contains(
Regex("(?i)($fixTitle)|($title)")
)
else -> this.contains(Regex("(?i)(Season\\s0?1)|(S0?1)")) && this.contains(
Regex("(?i)($fixTitle)|($title)")
) && this.contains("$yearNum")
}
} else {
this.contains(Regex("(?i)($fixTitle)|($title)")) && this.contains("$yearNum")
}
}
fun Document.getMirrorLink(): String? {
return this.select("div.mb-4 a").randomOrNull()
?.attr("href")
}
fun Document.getMirrorServer(server: Int): String {
return this.select("div.text-center a:contains(Server $server)").attr("href")
}
suspend fun extractMirrorUHD(url: String, ref: String): String? {
var baseDoc = app.get(fixUrl(url, ref)).document
var downLink = baseDoc.getMirrorLink()
run lit@{
(1..2).forEach {
if (downLink != null) return@lit
val server = baseDoc.getMirrorServer(it.plus(1))
baseDoc = app.get(fixUrl(server, ref)).document
downLink = baseDoc.getMirrorLink()
}
}
return if (downLink?.contains("workers.dev") == true) downLink else base64Decode(
downLink?.substringAfter(
"download?url="
) ?: return null
)
}
suspend fun extractInstantUHD(url: String): String? {
val host = getBaseUrl(url)
val body = FormBody.Builder()
.addEncoded("keys", url.substringAfter("url="))
.build()
return app.post(
"$host/api", requestBody = body, headers = mapOf(
"x-token" to URI(url).host
), referer = "$host/"
).parsedSafe<Map<String, String>>()?.get("url")
}
suspend fun extractDirectUHD(url: String, niceResponse: NiceResponse): String? {
val document = niceResponse.document
val script = document.selectFirst("script:containsData(cf_token)")?.data() ?: return null
val actionToken = script.substringAfter("\"key\", \"").substringBefore("\");")
val cfToken = script.substringAfter("cf_token = \"").substringBefore("\";")
val body = FormBody.Builder()
.addEncoded("action", "direct")
.addEncoded("key", actionToken)
.addEncoded("action_token", cfToken)
.build()
val cookies = mapOf("PHPSESSID" to "${niceResponse.cookies["PHPSESSID"]}")
val direct = app.post(
url,
requestBody = body,
cookies = cookies,
referer = url,
headers = mapOf(
"x-token" to "driveleech.org"
)
).parsedSafe<Map<String, String>>()?.get("url")
return app.get(
direct ?: return null, cookies = cookies,
referer = url
).text.substringAfter("worker_url = '").substringBefore("';")
}
suspend fun extractBackupUHD(url: String): String? {
val resumeDoc = app.get(url)
val script = resumeDoc.document.selectFirst("script:containsData(FormData.)")?.data()
val ssid = resumeDoc.cookies["PHPSESSID"]
val baseIframe = getBaseUrl(url)
val fetchLink =
script?.substringAfter("fetch('")?.substringBefore("',")?.let { fixUrl(it, baseIframe) }
val token = script?.substringAfter("'token', '")?.substringBefore("');")
val body = FormBody.Builder()
.addEncoded("token", "$token")
.build()
val cookies = mapOf("PHPSESSID" to "$ssid")
val result = app.post(
fetchLink ?: return null,
requestBody = body,
headers = mapOf(
"Accept" to "*/*",
"Origin" to baseIframe,
"Sec-Fetch-Site" to "same-origin"
),
cookies = cookies,
referer = url
).text
return tryParseJson<UHDBackupUrl>(result)?.url
}
suspend fun extractGdbot(url: String): String? {
val headers = mapOf(
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
)
val res = app.get(
"$gdbot/", headers = headers
)
val token = res.document.selectFirst("input[name=_token]")?.attr("value")
val cookiesSet = res.headers.filter { it.first == "set-cookie" }
val xsrf =
cookiesSet.find { it.second.contains("XSRF-TOKEN") }?.second?.substringAfter("XSRF-TOKEN=")
?.substringBefore(";")
val session =
cookiesSet.find { it.second.contains("gdtot_proxy_session") }?.second?.substringAfter("gdtot_proxy_session=")
?.substringBefore(";")
val cookies = mapOf(
"gdtot_proxy_session" to "$session",
"XSRF-TOKEN" to "$xsrf"
)
val requestFile = app.post(
"$gdbot/file", data = mapOf(
"link" to url,
"_token" to "$token"
), headers = headers, referer = "$gdbot/", cookies = cookies
).document
return requestFile.selectFirst("div.mt-8 a.float-right")?.attr("href")
}
suspend fun extractDirectDl(url: String): String? {
val iframe = app.get(url).document.selectFirst("li.flex.flex-col.py-6 a:contains(Direct DL)")
?.attr("href")
val request = app.get(iframe ?: return null)
val driveDoc = request.document
val token = driveDoc.select("section#generate_url").attr("data-token")
val uid = driveDoc.select("section#generate_url").attr("data-uid")
val ssid = request.cookies["PHPSESSID"]
val body =
"""{"type":"DOWNLOAD_GENERATE","payload":{"uid":"$uid","access_token":"$token"}}""".toRequestBody(
RequestBodyTypes.JSON.toMediaTypeOrNull()
)
val json = app.post(
"https://rajbetmovies.com/action", requestBody = body, headers = mapOf(
"Accept" to "application/json, text/plain, */*",
"Cookie" to "PHPSESSID=$ssid",
"X-Requested-With" to "xmlhttprequest"
), referer = request.url
).text
return tryParseJson<DirectDl>(json)?.download_url
}
suspend fun extractDrivebot(url: String): String? {
val iframeDrivebot =
app.get(url).document.selectFirst("li.flex.flex-col.py-6 a:contains(Drivebot)")
?.attr("href") ?: return null
return getDrivebotLink(iframeDrivebot)
}
suspend fun extractGdflix(url: String): String? {
val iframeGdflix =
if (!url.contains("gdflix")) app.get(url).document.selectFirst("li.flex.flex-col.py-6 a:contains(GDFlix Direct)")
?.attr("href") ?: return null else url
val base = getBaseUrl(iframeGdflix)
val req = app.get(iframeGdflix).document.selectFirst("script:containsData(replace)")?.data()
?.substringAfter("replace(\"")
?.substringBefore("\")")?.let {
app.get(fixUrl(it, base))
} ?: return null
val iframeDrivebot2 = req.document.selectFirst("a.btn.btn-outline-warning")?.attr("href")
return getDrivebotLink(iframeDrivebot2)
// val reqUrl = req.url
// val ssid = req.cookies["PHPSESSID"]
// val script = req.document.selectFirst("script:containsData(formData =)")?.data()
// val key = Regex("append\\(\"key\", \"(\\S+?)\"\\);").find(script ?: return null)?.groupValues?.get(1)
//
// val body = FormBody.Builder()
// .addEncoded("action", "direct")
// .addEncoded("key", "$key")
// .addEncoded("action_token", "cf_token")
// .build()
//
// val gdriveUrl = app.post(
// reqUrl, requestBody = body,
// cookies = mapOf("PHPSESSID" to "$ssid"),
// headers = mapOf(
// "x-token" to URI(reqUrl).host
// )
// ).parsedSafe<Gdflix>()?.url
//
// return getDirectGdrive(gdriveUrl ?: return null)
}
suspend fun getDrivebotLink(url: String?): String? {
val driveDoc = app.get(url ?: return null)
val ssid = driveDoc.cookies["PHPSESSID"]
val script = driveDoc.document.selectFirst("script:containsData(var formData)")?.data()
val baseUrl = getBaseUrl(url)
val token = script?.substringAfter("'token', '")?.substringBefore("');")
val link =
script?.substringAfter("fetch('")?.substringBefore("',").let { "$baseUrl$it" }
val body = FormBody.Builder()
.addEncoded("token", "$token")
.build()
val cookies = mapOf("PHPSESSID" to "$ssid")
val file = app.post(
link,
requestBody = body,
headers = mapOf(
"Accept" to "*/*",
"Origin" to baseUrl,
"Sec-Fetch-Site" to "same-origin"
),
cookies = cookies,
referer = url
).parsedSafe<DriveBotLink>()?.url ?: return null
return if (file.startsWith("http")) file else app.get(
fixUrl(
file,
baseUrl
)
).document.selectFirst("script:containsData(window.open)")
?.data()?.substringAfter("window.open('")?.substringBefore("')")
}
suspend fun extractOiya(url: String, quality: String): String? {
val doc = app.get(url).document
return doc.selectFirst("div.wp-block-button a:matches((?i)$quality)")?.attr("href")
?: doc.selectFirst("div.wp-block-button a")?.attr("href")
}
fun deobfstr(hash: String, index: String): String {
var result = ""
for (i in hash.indices step 2) {
val j = hash.substring(i, i + 2)
result += (j.toInt(16) xor index[(i / 2) % index.length].code).toChar()
}
return result
}
suspend fun extractCovyn(url: String?): Pair<String?, String?>? {
val request = session.get(url ?: return null, referer = "${tvMoviesAPI}/")
val filehosting = session.baseClient.cookieJar.loadForRequest(url.toHttpUrl())
.find { it.name == "filehosting" }?.value
val headers = mapOf(
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Connection" to "keep-alive",
"Cookie" to "filehosting=$filehosting",
)
val iframe = request.document.findTvMoviesIframe()
delay(10500)
val request2 = session.get(
iframe ?: return null, referer = url, headers = headers
)
val iframe2 = request2.document.findTvMoviesIframe()
delay(10500)
val request3 = session.get(
iframe2 ?: return null, referer = iframe, headers = headers
)
val response = request3.document
val videoLink = response.selectFirst("button.btn.btn--primary")?.attr("onclick")
?.substringAfter("location = '")?.substringBefore("';")?.let {
app.get(
it, referer = iframe2, headers = headers
).url
}
val size = response.selectFirst("ul.row--list li:contains(Filesize) span:last-child")
?.text()
return Pair(videoLink, size)
}
suspend fun getDirectGdrive(url: String): String {
val fixUrl = if (url.contains("&export=download")) {
url
} else {
"https://drive.google.com/uc?id=${
Regex("(?:\\?id=|/d/)(\\S+)/").find("$url/")?.groupValues?.get(1)
}&export=download"
}
val doc = app.get(fixUrl).document
val form = doc.select("form#download-form").attr("action")
val uc = doc.select("input#uc-download-link").attr("value")
return app.post(
form, data = mapOf(
"uc-download-link" to uc
)
).url
}
suspend fun invokeSmashyFfix(
name: String,
url: String,
ref: String,
callback: (ExtractorLink) -> Unit,
) {
val json = app.get(url, referer = ref, headers = mapOf("X-Requested-With" to "XMLHttpRequest"))
.parsedSafe<SmashySources>()
json?.sourceUrls?.map {
M3u8Helper.generateM3u8(
"Smashy [$name]",
it,
""
).forEach(callback)
}
}
suspend fun invokeSmashyD(
url: String,
ref: String,
callback: (ExtractorLink) -> Unit,
) {
val json = app.get(url, referer = ref, headers = mapOf("X-Requested-With" to "XMLHttpRequest"))
.parsedSafe<SmashyDSources>()
json?.sourceUrls?.apmap {
M3u8Helper.generateM3u8(
"Smashy [Player D ${it.title}]",
it.file ?: return@apmap,
""
).forEach(callback)
}
}
suspend fun getDumpIdAndType(title: String?, year: Int?, season: Int?): Pair<String?, Int?> {
val res = tryParseJson<DumpQuickSearchData>(
queryApi(
"POST",
"${BuildConfig.DUMP_API}/search/searchWithKeyWord",
mapOf(
"searchKeyWord" to "$title",
"size" to "50",
)
)
)?.searchResults
val media = if (res?.size == 1) {
res.firstOrNull()
} else {
res?.find {
when (season) {
null -> {
it.name.equals(
title,
true
) && it.releaseTime == "$year" && it.domainType == 0
}
1 -> {
it.name?.contains(
"$title",
true
) == true && (it.releaseTime == "$year" || it.name.contains(
"Season $season",
true
)) && it.domainType == 1
}
else -> {
it.name?.contains(Regex("(?i)$title\\s?($season|${season.toRomanNumeral()}|Season\\s$season)")) == true && it.releaseTime == "$year" && it.domainType == 1
}
}
}
}
return media?.id to media?.domainType
}
suspend fun fetchDumpEpisodes(id: String, type: String, episode: Int?): EpisodeVo? {
return tryParseJson<DumpMediaDetail>(
queryApi(
"GET",
"${BuildConfig.DUMP_API}/movieDrama/get",
mapOf(
"category" to type,
"id" to id,
)
)
)?.episodeVo?.find {
it.seriesNo == (episode ?: 0)
}
}
suspend fun invokeDrivetot(
url: String,
tags: String? = null,
size: String? = null,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
) {
val res = app.get(url)
val data = res.document.select("form input").associate { it.attr("name") to it.attr("value") }
app.post(res.url, data = data, cookies = res.cookies).document.select("div.card-body a")
.apmap { ele ->
val href = base64Decode(ele.attr("href").substringAfterLast("/")).let {
if (it.contains("hubcloud.lol")) it.replace("hubcloud.lol", "hubcloud.in") else it
}
loadExtractor(href, "$hdmovies4uAPI/", subtitleCallback) { link ->
callback.invoke(
ExtractorLink(
link.source,
"${link.name} $tags [$size]",
link.url,
link.referer,
link.quality,
link.type,
link.headers,
link.extractorData
)
)
}
}
}
suspend fun bypassBqrecipes(url: String): String? {
var res = app.get(url)
var location = res.text.substringAfter(".replace('").substringBefore("');")
var cookies = res.cookies
res = app.get(location, cookies = cookies)
cookies = cookies + res.cookies
val document = res.document
location = document.select("form#recaptcha").attr("action")
val data =
document.select("form#recaptcha input").associate { it.attr("name") to it.attr("value") }
res = app.post(location, data = data, cookies = cookies)
location = res.document.selectFirst("a#messagedown")?.attr("href") ?: return null
cookies = (cookies + res.cookies).minus("var")
return app.get(location, cookies = cookies, allowRedirects = false).headers["location"]
}
suspend fun bypassOuo(url: String?): String? {
var res = session.get(url ?: return null)
run lit@{
(1..2).forEach { _ ->
if (res.headers["location"] != null) return@lit
val document = res.document
val nextUrl = document.select("form").attr("action")
val data = document.select("form input").mapNotNull {
it.attr("name") to it.attr("value")
}.toMap().toMutableMap()
val captchaKey =
document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
.attr("src").substringAfter("render=")
val token = getCaptchaToken(url, captchaKey)
data["x-token"] = token ?: ""
res = session.post(
nextUrl,
data = data,
headers = mapOf("content-type" to "application/x-www-form-urlencoded"),
allowRedirects = false
)
}
}
return res.headers["location"]
}
suspend fun bypassFdAds(url: String?): String? {
val directUrl =
app.get(url ?: return null, verify = false).document.select("a#link").attr("href")
.substringAfter("/go/")
.let { base64Decode(it) }
val doc = app.get(directUrl, verify = false).document
val lastDoc = app.post(
doc.select("form#landing").attr("action"),
data = mapOf("go" to doc.select("form#landing input").attr("value")),
verify = false
).document
val json = lastDoc.select("form#landing input[name=newwpsafelink]").attr("value")
.let { base64Decode(it) }
val finalJson =
tryParseJson<FDAds>(json)?.linkr?.substringAfter("redirect=")?.let { base64Decode(it) }
return tryParseJson<Safelink>(finalJson)?.safelink
}
suspend fun bypassHrefli(url: String): String? {
val postUrl = url.substringBefore("?id=").substringAfter("/?")
val res = app.post(
postUrl, data = mapOf(
"_wp_http" to url.substringAfter("?id=")
)
).document
val link = res.select("form#landing").attr("action")
val wpHttp = res.select("input[name=_wp_http2]").attr("value")
val token = res.select("input[name=token]").attr("value")
val blogRes = app.post(
link, data = mapOf(
"_wp_http2" to wpHttp,
"token" to token
)
).text
val skToken = blogRes.substringAfter("?go=").substringBefore("\"")
val driveUrl = app.get(
"$postUrl?go=$skToken", cookies = mapOf(
skToken to wpHttp
)
).document.selectFirst("meta[http-equiv=refresh]")?.attr("content")?.substringAfter("url=")
val path = app.get(driveUrl ?: return null).text.substringAfter("replace(\"")
.substringBefore("\")")
if (path == "/404") return null
return fixUrl(path, getBaseUrl(driveUrl))
}
suspend fun bypassTechmny(url: String): String? {
val techRes = app.get(url).document
val postUrl = url.substringBefore("?id=").substringAfter("/?")
val (goUrl, goHeader) = if (techRes.selectFirst("form#landing input[name=_wp_http_c]") != null) {
var res = app.post(
postUrl, data = mapOf(
"_wp_http_c" to url.substringAfter("?id=")
)
)
val (longC, catC, _) = getTechmnyCookies(res.text)
var headers = mapOf("Cookie" to "$longC; $catC")
var formLink = res.document.selectFirst("center a")?.attr("href")
res = app.get(formLink ?: return null, headers = headers)
val (longC2, _, postC) = getTechmnyCookies(res.text)
headers = mapOf("Cookie" to "$catC; $longC2; $postC")
formLink = res.document.selectFirst("center a")?.attr("href")
res = app.get(formLink ?: return null, headers = headers)
val goToken = res.text.substringAfter("?go=").substringBefore("\"")
val tokenUrl = "$postUrl?go=$goToken"
val newLongC = "$goToken=" + longC2.substringAfter("=")
headers = mapOf("Cookie" to "$catC; rdst_post=; $newLongC")
Pair(tokenUrl, headers)
} else {
val secondPage = techRes.getNextTechPage().document
val thirdPage = secondPage.getNextTechPage().text
val goToken = thirdPage.substringAfter("?go=").substringBefore("\"")
val tokenUrl = "$postUrl?go=$goToken"
val headers = mapOf(
"Cookie" to "$goToken=${
secondPage.select("form#landing input[name=_wp_http2]").attr("value")
}"
)
Pair(tokenUrl, headers)
}
val driveUrl =
app.get(goUrl, headers = goHeader).document.selectFirst("meta[http-equiv=refresh]")
?.attr("content")?.substringAfter("url=")
val path = app.get(driveUrl ?: return null).text.substringAfter("replace(\"")
.substringBefore("\")")
if (path == "/404") return null
return fixUrl(path, getBaseUrl(driveUrl))
}
private suspend fun Document.getNextTechPage(): NiceResponse {
return app.post(
this.select("form").attr("action"),
data = this.select("form input").mapNotNull {
it.attr("name") to it.attr("value")
}.toMap().toMutableMap()
)
}
suspend fun bypassDriveleech(url: String): String? {
val path = app.get(url).text.substringAfter("replace(\"")
.substringBefore("\")")
if (path == "/404") return null
return fixUrl(path, getBaseUrl(url))
}
private fun getTechmnyCookies(page: String): Triple<String, String, String> {
val cat = "rdst_cat"
val post = "rdst_post"
val longC = page.substringAfter(".setTime")
.substringAfter("document.cookie = \"")
.substringBefore("\"")
.substringBefore(";")
val catC = if (page.contains("$cat=")) {
page.substringAfterLast("$cat=")
.substringBefore(";").let {
"$cat=$it"
}
} else {
""
}
val postC = if (page.contains("$post=")) {
page.substringAfterLast("$post=")
.substringBefore(";").let {
"$post=$it"
}
} else {
""
}
return Triple(longC, catC, postC)
}
suspend fun getTvMoviesServer(url: String, season: Int?, episode: Int?): Pair<String, String?>? {
val req = app.get(url)
if (!req.isSuccessful) return null
val doc = req.document
return if (season == null) {
doc.select("table.wp-block-table tr:last-child td:first-child").text() to
doc.selectFirst("table.wp-block-table tr a")?.attr("href").let { link ->
app.get(link ?: return null).document.select("div#text-url a")
.mapIndexed { index, element ->
element.attr("href") to element.parent()?.textNodes()?.getOrNull(index)
?.text()
}.filter { it.second?.contains("Subtitles", true) == false }
.map { it.first }
}.lastOrNull()
} else {
doc.select("div.vc_tta-panels div#Season-$season table.wp-block-table tr:last-child td:first-child")
.text() to
doc.select("div.vc_tta-panels div#Season-$season table.wp-block-table tr a")
.mapNotNull { ele ->
app.get(ele.attr("href")).document.select("div#text-url a")
.mapIndexed { index, element ->
element.attr("href") to element.parent()?.textNodes()
?.getOrNull(index)?.text()
}.find { it.second?.contains("Episode $episode", true) == true }?.first
}.lastOrNull()
}
}
suspend fun getSfServer() = sfServer ?: fetchSfServer().also { sfServer = it }
suspend fun fetchSfServer(): String {
return app.get("https://raw.githubusercontent.com/hexated/cloudstream-resources/main/sfmovies_server").text
}
suspend fun getFilmxyCookies(url: String) =
filmxyCookies ?: fetchFilmxyCookies(url).also { filmxyCookies = it }
suspend fun fetchFilmxyCookies(url: String): Map<String, String> {
val defaultCookies =
mutableMapOf("G_ENABLED_IDPS" to "google", "true_checker" to "1", "XID" to "1")
session.get(
url,
headers = mapOf(
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
),
cookies = defaultCookies,
)
val phpsessid = session.baseClient.cookieJar.loadForRequest(url.toHttpUrl())
.first { it.name == "PHPSESSID" }.value
defaultCookies["PHPSESSID"] = phpsessid
val userNonce =
app.get(
"$filmxyAPI/login/?redirect_to=$filmxyAPI/",
cookies = defaultCookies
).document.select("script")
.find { it.data().contains("var userNonce") }?.data()?.let {
Regex("var\\suserNonce.*?\"(\\S+?)\";").find(it)?.groupValues?.get(1)
}
val cookieUrl = "${filmxyAPI}/wp-admin/admin-ajax.php"
session.post(
cookieUrl,
data = mapOf(
"action" to "guest_login",
"nonce" to "$userNonce",
),
headers = mapOf(
"X-Requested-With" to "XMLHttpRequest",
),
cookies = defaultCookies
)
val cookieJar = session.baseClient.cookieJar.loadForRequest(cookieUrl.toHttpUrl())
.associate { it.name to it.value }.toMutableMap()
return cookieJar.plus(defaultCookies)
}
suspend fun getWatchflxCookies() =
watchflxCookies ?: fetchWatchflxCookies().also { watchflxCookies = it }
suspend fun fetchWatchflxCookies(): Map<String, String> {
session.get(watchflxAPI)
val cookies = session.baseClient.cookieJar.loadForRequest(watchflxAPI.toHttpUrl())
.associate { it.name to it.value }
val loginUrl = "$watchflxAPI/cookie-based-login"
session.post(
loginUrl, data = mapOf(
"continue_as_temp" to "true"
), cookies = cookies, headers = mapOf("X-Requested-With" to "XMLHttpRequest")
)
return session.baseClient.cookieJar.loadForRequest(loginUrl.toHttpUrl())
.associate { it.name to it.value }
}
fun Document.findTvMoviesIframe(): String? {
return this.selectFirst("script:containsData(var seconds)")?.data()?.substringAfter("href='")
?.substringBefore("'>")
}
//modified code from https://github.com/jmir1/aniyomi-extensions/blob/master/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt
suspend fun getCrunchyrollToken(): Map<String, String> {
val client = app.baseClient.newBuilder()
.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("cr-unblocker.us.to", 1080)))
.build()
Authenticator.setDefault(object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication("crunblocker", "crunblocker".toCharArray())
}
})
val request = requestCreator(
method = "POST",
url = "$crunchyrollAPI/auth/v1/token",
headers = mapOf(
"User-Agent" to "Crunchyroll/3.26.1 Android/11 okhttp/4.9.2",
"Content-Type" to "application/x-www-form-urlencoded",
"Authorization" to "Basic ${BuildConfig.CRUNCHYROLL_BASIC_TOKEN}"
),
data = mapOf(
"refresh_token" to app.get(BuildConfig.CRUNCHYROLL_REFRESH_TOKEN).text,
"grant_type" to "refresh_token",
"scope" to "offline_access"
)
)
val response = tryParseJson<CrunchyrollToken>(client.newCall(request).execute().body.string())
return mapOf("Authorization" to "${response?.tokenType} ${response?.accessToken}")
}
suspend fun getCrunchyrollId(aniId: String?): String? {
val query = """
query media(${'$'}id: Int, ${'$'}type: MediaType, ${'$'}isAdult: Boolean) {
Media(id: ${'$'}id, type: ${'$'}type, isAdult: ${'$'}isAdult) {
id
externalLinks {
id
site
url
type
}
}
}
""".trimIndent().trim()
val variables = mapOf(
"id" to aniId,
"isAdult" to false,
"type" to "ANIME",
)
val data = mapOf(
"query" to query,
"variables" to variables
).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
val externalLinks = app.post(anilistAPI, requestBody = data)
.parsedSafe<AnilistResponses>()?.data?.Media?.externalLinks
return (externalLinks?.find { it.site == "VRV" }
?: externalLinks?.find { it.site == "Crunchyroll" })?.url?.let {
Regex("series/(\\w+)/?").find(it)?.groupValues?.get(1)
}
}
suspend fun getCrunchyrollIdFromMalSync(aniId: String?): String? {
val res = app.get("$malsyncAPI/mal/anime/$aniId").parsedSafe<MalSyncRes>()?.Sites
val vrv = res?.get("Vrv")?.map { it.value }?.firstOrNull()?.get("url")
val crunchyroll = res?.get("Vrv")?.map { it.value }?.firstOrNull()?.get("url")
val regex = Regex("series/(\\w+)/?")
return regex.find("$vrv")?.groupValues?.getOrNull(1)
?: regex.find("$crunchyroll")?.groupValues?.getOrNull(1)
}
suspend fun convertTmdbToAnimeId(
title: String?,
date: String?,
airedDate: String?,
type: TvType
): AniIds {
val sDate = date?.split("-")
val sAiredDate = airedDate?.split("-")
val year = sDate?.firstOrNull()?.toIntOrNull()
val airedYear = sAiredDate?.firstOrNull()?.toIntOrNull()
val season = getSeason(sDate?.get(1)?.toIntOrNull())
val airedSeason = getSeason(sAiredDate?.get(1)?.toIntOrNull())
return if (type == TvType.AnimeMovie) {
tmdbToAnimeId(title, airedYear, "", type)
} else {
val ids = tmdbToAnimeId(title, year, season, type)
if (ids.id == null && ids.idMal == null) tmdbToAnimeId(
title,
airedYear,
airedSeason,
type
) else ids
}
}
suspend fun tmdbToAnimeId(title: String?, year: Int?, season: String?, type: TvType): AniIds {
val query = """
query (
${'$'}page: Int = 1
${'$'}search: String
${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]
${'$'}type: MediaType
${'$'}season: MediaSeason
${'$'}seasonYear: Int
${'$'}format: [MediaFormat]
) {
Page(page: ${'$'}page, perPage: 20) {
media(
search: ${'$'}search
sort: ${'$'}sort
type: ${'$'}type
season: ${'$'}season
seasonYear: ${'$'}seasonYear
format_in: ${'$'}format
) {
id
idMal
}
}
}
""".trimIndent().trim()
val variables = mapOf(
"search" to title,
"sort" to "SEARCH_MATCH",
"type" to "ANIME",
"season" to season?.uppercase(),
"seasonYear" to year,
"format" to listOf(if (type == TvType.AnimeMovie) "MOVIE" else "TV")
).filterValues { value -> value != null && value.toString().isNotEmpty() }
val data = mapOf(
"query" to query,
"variables" to variables
).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
val res = app.post(anilistAPI, requestBody = data)
.parsedSafe<AniSearch>()?.data?.Page?.media?.firstOrNull()
return AniIds(res?.id, res?.idMal)
}
fun generateWpKey(r: String, m: String): String {
val rList = r.split("\\x").toTypedArray()
var n = ""
val decodedM = String(base64Decode(m.split("").reversed().joinToString("")).toCharArray())
for (s in decodedM.split("|")) {
n += "\\x" + rList[Integer.parseInt(s) + 1]
}
return n
}
suspend fun loadCustomTagExtractor(
tag: String? = null,
url: String,
referer: String? = null,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
quality: Int? = null,
) {
loadExtractor(url, referer, subtitleCallback) { link ->
callback.invoke(
ExtractorLink(
link.source,
"${link.name} $tag",
link.url,
link.referer,
when (link.type) {
ExtractorLinkType.M3U8 -> link.quality
else -> quality ?: link.quality
},
link.type,
link.headers,
link.extractorData
)
)
}
}
suspend fun loadCustomExtractor(
name: String? = null,
url: String,
referer: String? = null,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
quality: Int? = null,
) {
loadExtractor(url, referer, subtitleCallback) { link ->
callback.invoke(
ExtractorLink(
name ?: link.source,
name ?: link.name,
link.url,
link.referer,
when {
link.name == "VidSrc" -> Qualities.P1080.value
link.type == ExtractorLinkType.M3U8 -> link.quality
else -> quality ?: link.quality
},
link.type,
link.headers,
link.extractorData
)
)
}
}
fun getSeason(month: Int?): String? {
val seasons = arrayOf(
"Winter", "Winter", "Spring", "Spring", "Spring", "Summer",
"Summer", "Summer", "Fall", "Fall", "Fall", "Winter"
)
if (month == null) return null
return seasons[month - 1]
}
fun getEpisodeSlug(
season: Int? = null,
episode: Int? = null,
): Pair<String, String> {
return if (season == null && episode == null) {
"" to ""
} else {
(if (season!! < 10) "0$season" else "$season") to (if (episode!! < 10) "0$episode" else "$episode")
}
}
fun getTitleSlug(title: String? = null): Pair<String?, String?> {
val slug = title.createSlug()
return slug?.replace("-", "\\W") to title?.replace(" ", "_")
}
fun getIndexQuery(
title: String? = null,
year: Int? = null,
season: Int? = null,
episode: Int? = null
): String {
val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode)
return (if (season == null) {
"$title ${year ?: ""}"
} else {
"$title S${seasonSlug}E${episodeSlug}"
}).trim()
}
fun searchIndex(
title: String? = null,
season: Int? = null,
episode: Int? = null,
year: Int? = null,
response: String,
isTrimmed: Boolean = true,
): List<IndexMedia>? {
val files = tryParseJson<IndexSearch>(response)?.data?.files?.filter { media ->
matchingIndex(
media.name ?: return null,
media.mimeType ?: return null,
title ?: return null,
year,
season,
episode
)
}?.distinctBy { it.name }?.sortedByDescending { it.size?.toLongOrNull() ?: 0 } ?: return null
return if (isTrimmed) {
files.let { file ->
listOfNotNull(
file.find { it.name?.contains("2160p", true) == true },
file.find { it.name?.contains("1080p", true) == true }
)
}
} else {
files
}
}
fun matchingIndex(
mediaName: String?,
mediaMimeType: String?,
title: String?,
year: Int?,
season: Int?,
episode: Int?,
include720: Boolean = false
): Boolean {
val (wSlug, dwSlug) = getTitleSlug(title)
val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode)
return (if (season == null) {
mediaName?.contains(Regex("(?i)(?:$wSlug|$dwSlug).*$year")) == true
} else {
mediaName?.contains(Regex("(?i)(?:$wSlug|$dwSlug).*S${seasonSlug}.?E${episodeSlug}")) == true
}) && mediaName?.contains(
if (include720) Regex("(?i)(2160p|1080p|720p)") else Regex("(?i)(2160p|1080p)")
) == true && ((mediaMimeType in mimeType) || mediaName.contains(Regex("\\.mkv|\\.mp4|\\.avi")))
}
fun decodeIndexJson(json: String): String {
val slug = json.reversed().substring(24)
return base64Decode(slug.substring(0, slug.length - 20))
}
fun String.decodePrimewireXor(key: String): String {
val sb = StringBuilder()
var i = 0
while (i < this.length) {
var j = 0
while (j < key.length && i < this.length) {
sb.append((this[i].code xor key[j].code).toChar())
j++
i++
}
}
return sb.toString()
}
fun vidsrctoDecrypt(text: String): String {
val parse = Base64.decode(text.toByteArray(), Base64.URL_SAFE)
val cipher = Cipher.getInstance("RC4")
cipher.init(
Cipher.DECRYPT_MODE,
SecretKeySpec("8z5Ag5wgagfsOuhz".toByteArray(), "RC4"),
cipher.parameters
)
return decode(cipher.doFinal(parse).toString(Charsets.UTF_8))
}
fun String?.createSlug(): String? {
return this?.replace(Regex("[^\\w\\s-]"), "")
?.replace(" ", "-")
?.replace(Regex("( )|( -)|(- )|(--)"), "-")
?.lowercase()
}
fun getLanguage(str: String): String {
return if (str.contains("(in_ID)")) "Indonesian" else str
}
fun bytesToGigaBytes(number: Double): Double = number / 1024000000
fun getKisskhTitle(str: String?): String? {
return str?.replace(Regex("[^a-zA-Z\\d]"), "-")
}
fun String.getFileSize(): Float? {
val size = Regex("(?i)(\\d+\\.?\\d+\\sGB|MB)").find(this)?.groupValues?.get(0)?.trim()
val num = Regex("(\\d+\\.?\\d+)").find(size ?: return null)?.groupValues?.get(0)?.toFloat()
?: return null
return when {
size.contains("GB") -> num * 1000000
else -> num * 1000
}
}
fun getUhdTags(str: String?): String {
return Regex("\\d{3,4}[Pp]\\.?(.*?)\\[").find(str ?: "")?.groupValues?.getOrNull(1)
?.replace(".", " ")?.trim()
?: str ?: ""
}
fun getIndexQualityTags(str: String?, fullTag: Boolean = false): String {
return if (fullTag) Regex("(?i)(.*)\\.(?:mkv|mp4|avi)").find(str ?: "")?.groupValues?.get(1)
?.trim() ?: str ?: "" else Regex("(?i)\\d{3,4}[pP]\\.?(.*?)\\.(mkv|mp4|avi)").find(
str ?: ""
)?.groupValues?.getOrNull(1)
?.replace(".", " ")?.trim() ?: str ?: ""
}
fun getIndexQuality(str: String?): Int {
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
?: Qualities.Unknown.value
}
fun getIndexSize(str: String?): String? {
return Regex("(?i)([\\d.]+\\s*(?:gb|mb))").find(str ?: "")?.groupValues?.getOrNull(1)?.trim()
}
fun getQuality(str: String): Int {
return when (str) {
"360p" -> Qualities.P240.value
"480p" -> Qualities.P360.value
"720p" -> Qualities.P480.value
"1080p" -> Qualities.P720.value
"1080p Ultra" -> Qualities.P1080.value
else -> getQualityFromName(str)
}
}
fun getGMoviesQuality(str: String): Int {
return when {
str.contains("480P", true) -> Qualities.P480.value
str.contains("720P", true) -> Qualities.P720.value
str.contains("1080P", true) -> Qualities.P1080.value
str.contains("4K", true) -> Qualities.P2160.value
else -> Qualities.Unknown.value
}
}
fun getFDoviesQuality(str: String): String {
return when {
str.contains("1080P", true) -> "1080P"
str.contains("4K", true) -> "4K"
else -> ""
}
}
fun getVipLanguage(str: String): String {
return when (str) {
"in_ID" -> "Indonesian"
"pt" -> "Portuguese"
else -> str.split("_").first().let {
SubtitleHelper.fromTwoLettersToLanguage(it).toString()
}
}
}
fun getDbgoLanguage(str: String): String {
return when (str) {
"Русский" -> "Russian"
"Українська" -> "Ukrainian"
else -> str
}
}
fun fixCrunchyrollLang(language: String?): String? {
return SubtitleHelper.fromTwoLettersToLanguage(language ?: return null)
?: SubtitleHelper.fromTwoLettersToLanguage(language.substringBefore("-"))
}
fun getDeviceId(length: Int = 16): String {
val allowedChars = ('a'..'f') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
fun String.encodeUrl(): String {
val url = URL(this)
val uri = URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref)
return uri.toURL().toString()
}
fun getBaseUrl(url: String): String {
return URI(url).let {
"${it.scheme}://${it.host}"
}
}
fun String.getHost(): String {
return fixTitle(URI(this).host.substringBeforeLast(".").substringAfterLast("."))
}
fun isUpcoming(dateString: String?): Boolean {
return try {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
unixTimeMS < dateTime
} catch (t: Throwable) {
logError(t)
false
}
}
fun getDate(): TmdbDate {
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val calender = Calendar.getInstance()
val today = formatter.format(calender.time)
calender.add(Calendar.WEEK_OF_YEAR, 1)
val nextWeek = formatter.format(calender.time)
return TmdbDate(today, nextWeek)
}
fun decode(input: String): String = URLDecoder.decode(input, "utf-8")
fun encode(input: String): String = URLEncoder.encode(input, "utf-8").replace("+", "%20")
fun base64DecodeAPI(api: String): String {
return api.chunked(4).map { base64Decode(it) }.reversed().joinToString("")
}
fun decryptStreamUrl(data: String): String {
fun getTrash(arr: List<String>, item: Int): List<String> {
val trash = ArrayList<List<String>>()
for (i in 1..item) {
trash.add(arr)
}
return trash.reduce { acc, list ->
val temp = ArrayList<String>()
acc.forEach { ac ->
list.forEach { li ->
temp.add(ac.plus(li))
}
}
return@reduce temp
}
}
val trashList = listOf("@", "#", "!", "^", "$")
val trashSet = getTrash(trashList, 2) + getTrash(trashList, 3)
var trashString = data.replace("#2", "").split("//_//").joinToString("")
trashSet.forEach {
val temp = base64Encode(it.toByteArray())
trashString = trashString.replace(temp, "")
}
return base64Decode(trashString)
}
fun fixUrl(url: String, domain: String): String {
if (url.startsWith("http")) {
return url
}
if (url.isEmpty()) {
return ""
}
val startsWithNoHttp = url.startsWith("//")
if (startsWithNoHttp) {
return "https:$url"
} else {
if (url.startsWith('/')) {
return domain + url
}
return "$domain/$url"
}
}
fun Int.toRomanNumeral(): String = Symbol.closestBelow(this)
.let { symbol ->
if (symbol != null) {
"$symbol${(this - symbol.decimalValue).toRomanNumeral()}"
} else {
""
}
}
private enum class Symbol(val decimalValue: Int) {
I(1),
IV(4),
V(5),
IX(9),
X(10);
companion object {
fun closestBelow(value: Int) =
values()
.sortedByDescending { it.decimalValue }
.firstOrNull { value >= it.decimalValue }
}
}
object DumpUtils {
private val deviceId = getDeviceId()
suspend fun queryApi(method: String, url: String, params: Map<String, String>): String {
return app.custom(
method,
url,
requestBody = if (method == "POST") params.toJson()
.toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) else null,
params = if (method == "GET") params else emptyMap(),
headers = createHeaders(params)
).parsedSafe<HashMap<String, String>>()?.get("data").let {
cryptoHandler(
it.toString(),
deviceId,
false
)
}
}
private fun createHeaders(
params: Map<String, String>,
currentTime: String = System.currentTimeMillis().toString(),
): Map<String, String> {
return mapOf(
"lang" to "en",
"currentTime" to currentTime,
"sign" to getSign(currentTime, params).toString(),
"aesKey" to getAesKey().toString(),
)
}
private fun cryptoHandler(
string: String,
secretKeyString: String,
encrypt: Boolean = true
): String {
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
val cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, secretKey)
String(cipher.doFinal(base64DecodeArray(string)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
base64Encode(cipher.doFinal(string.toByteArray()))
}
}
private fun getAesKey(): String? {
val publicKey =
RSAEncryptionHelper.getPublicKeyFromString(BuildConfig.DUMP_KEY) ?: return null
return RSAEncryptionHelper.encryptText(deviceId, publicKey)
}
private fun getSign(currentTime: String, params: Map<String, String>): String? {
val chipper = listOf(
currentTime,
params.map { it.value }.reversed().joinToString("")
.let { base64Encode(it.toByteArray()) }).joinToString("")
val enc = cryptoHandler(chipper, deviceId)
return md5(enc)
}
private fun md5(input: String): String {
val md = MessageDigest.getInstance("MD5")
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
}
}
object RSAEncryptionHelper {
private const val RSA_ALGORITHM = "RSA"
private const val CIPHER_TYPE_FOR_RSA = "RSA/ECB/PKCS1Padding"
private val keyFactory = KeyFactory.getInstance(RSA_ALGORITHM)
private val cipher = Cipher.getInstance(CIPHER_TYPE_FOR_RSA)
fun getPublicKeyFromString(publicKeyString: String): PublicKey? =
try {
val keySpec =
X509EncodedKeySpec(Base64.decode(publicKeyString.toByteArray(), Base64.NO_WRAP))
keyFactory.generatePublic(keySpec)
} catch (exception: Exception) {
exception.printStackTrace()
null
}
fun getPrivateKeyFromString(privateKeyString: String): PrivateKey? =
try {
val keySpec =
PKCS8EncodedKeySpec(Base64.decode(privateKeyString.toByteArray(), Base64.DEFAULT))
keyFactory.generatePrivate(keySpec)
} catch (exception: Exception) {
exception.printStackTrace()
null
}
fun encryptText(plainText: String, publicKey: PublicKey): String? =
try {
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
Base64.encodeToString(cipher.doFinal(plainText.toByteArray()), Base64.NO_WRAP)
} catch (exception: Exception) {
exception.printStackTrace()
null
}
fun decryptText(encryptedText: String, privateKey: PrivateKey): String? =
try {
cipher.init(Cipher.DECRYPT_MODE, privateKey)
String(cipher.doFinal(Base64.decode(encryptedText, Base64.DEFAULT)))
} catch (exception: Exception) {
exception.printStackTrace()
null
}
}
// 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 CryptoJS {
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"
// 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)
}
/**
* 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)
}
}
}
object AESGCM {
fun ByteArray.decrypt(pass: String): String {
val (key, iv) = generateKeyAndIv(pass)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, iv))
return String(cipher.doFinal(this), StandardCharsets.UTF_8)
}
private fun generateKeyAndIv(pass: String): Pair<ByteArray, ByteArray> {
val datePart = getCurrentUTCDateString().take(16)
val hexString = datePart + pass
val byteArray = hexString.toByteArray(StandardCharsets.UTF_8)
val digest = MessageDigest.getInstance("SHA-256").digest(byteArray)
return digest.copyOfRange(0, digest.size / 2) to digest.copyOfRange(
digest.size / 2,
digest.size
)
}
private fun getCurrentUTCDateString(): String {
val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.getDefault())
dateFormat.timeZone = TimeZone.getTimeZone("GMT")
return dateFormat.format(Date())
}
}