update few providers
This commit is contained in:
parent
471a1703a3
commit
dd612e12bc
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 1
|
version = 2
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -150,8 +150,8 @@ class Animasu : MainAPI() {
|
||||||
link.name,
|
link.name,
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
if(!link.isM3u8) getIndexQuality(quality) else link.quality,
|
if(link.type != ExtractorLinkType.M3U8) getIndexQuality(quality) else link.quality,
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 8
|
version = 9
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.hexated
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
import com.lagradost.nicehttp.NiceResponse
|
import com.lagradost.nicehttp.NiceResponse
|
||||||
|
@ -144,7 +145,7 @@ class AnimeSailProvider : MainAPI() {
|
||||||
Jsoup.parse(base64Decode(it.attr("data-em"))).select("iframe").attr("src")
|
Jsoup.parse(base64Decode(it.attr("data-em"))).select("iframe").attr("src")
|
||||||
?: throw ErrorLoadingException("No iframe found")
|
?: throw ErrorLoadingException("No iframe found")
|
||||||
)
|
)
|
||||||
|
val quality = getIndexQuality(it.text())
|
||||||
when {
|
when {
|
||||||
iframe.startsWith("$mainUrl/utils/player/arch/") || iframe.startsWith(
|
iframe.startsWith("$mainUrl/utils/player/arch/") || iframe.startsWith(
|
||||||
"$mainUrl/utils/player/race/"
|
"$mainUrl/utils/player/race/"
|
||||||
|
@ -156,15 +157,13 @@ class AnimeSailProvider : MainAPI() {
|
||||||
iframe.contains("/race/") -> "Race"
|
iframe.contains("/race/") -> "Race"
|
||||||
else -> this.name
|
else -> this.name
|
||||||
}
|
}
|
||||||
val quality =
|
|
||||||
Regex("\\.(\\d{3,4})\\.").find(link)?.groupValues?.get(1)
|
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
source = source,
|
source = source,
|
||||||
name = source,
|
name = source,
|
||||||
url = link,
|
url = link,
|
||||||
referer = mainUrl,
|
referer = mainUrl,
|
||||||
quality = quality?.toIntOrNull() ?: Qualities.Unknown.value
|
quality = quality
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -175,16 +174,16 @@ class AnimeSailProvider : MainAPI() {
|
||||||
val link = "https://rasa-cintaku-semakin-berantai.xyz/v/${
|
val link = "https://rasa-cintaku-semakin-berantai.xyz/v/${
|
||||||
iframe.substringAfter("id=").substringBefore("&token")
|
iframe.substringAfter("id=").substringBefore("&token")
|
||||||
}"
|
}"
|
||||||
loadExtractor(link, mainUrl, subtitleCallback, callback)
|
loadFixedExtractor(link, quality, mainUrl, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
iframe.startsWith("$mainUrl/utils/player/framezilla/") || iframe.startsWith("https://uservideo.xyz") -> {
|
iframe.startsWith("$mainUrl/utils/player/framezilla/") || iframe.startsWith("https://uservideo.xyz") -> {
|
||||||
request(iframe, ref = data).document.select("iframe").attr("src")
|
request(iframe, ref = data).document.select("iframe").attr("src")
|
||||||
.let { link ->
|
.let { link ->
|
||||||
loadExtractor(fixUrl(link), mainUrl, subtitleCallback, callback)
|
loadFixedExtractor(fixUrl(link), quality, mainUrl, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
loadExtractor(iframe, mainUrl, subtitleCallback, callback)
|
loadFixedExtractor(iframe, quality, mainUrl, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,4 +192,32 @@ class AnimeSailProvider : MainAPI() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadFixedExtractor(
|
||||||
|
url: String,
|
||||||
|
quality: Int?,
|
||||||
|
referer: String? = null,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
loadExtractor(url, referer, subtitleCallback) { link ->
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
link.name,
|
||||||
|
link.name,
|
||||||
|
link.url,
|
||||||
|
link.referer,
|
||||||
|
if(link.type == ExtractorLinkType.M3U8) link.quality else quality ?: Qualities.Unknown.value,
|
||||||
|
link.type,
|
||||||
|
link.headers,
|
||||||
|
link.extractorData
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIndexQuality(str: String): Int {
|
||||||
|
return Regex("(\\d{3,4})[pP]").find(str)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||||
|
?: Qualities.Unknown.value
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 5
|
version = 6
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -158,7 +158,7 @@ open class Aniworld : MainAPI() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
link.quality,
|
link.quality,
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 10
|
version = 11
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -121,8 +121,8 @@ class Nodrakorid : DutaMovie() {
|
||||||
link.name,
|
link.name,
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
if(link.isM3u8) link.quality else quality ?: Qualities.Unknown.value,
|
if(link.type == ExtractorLinkType.M3U8) link.quality else quality ?: Qualities.Unknown.value,
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 10
|
version = 11
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -213,7 +213,7 @@ class Hdfilmcehennemi : MainAPI() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
link.quality,
|
link.quality,
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 14
|
version = 15
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.hexated
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
@ -10,12 +11,7 @@ import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.security.DigestException
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class KuronimeProvider : MainAPI() {
|
class KuronimeProvider : MainAPI() {
|
||||||
override var mainUrl = "https://45.12.2.26"
|
override var mainUrl = "https://45.12.2.26"
|
||||||
|
@ -186,10 +182,11 @@ class KuronimeProvider : MainAPI() {
|
||||||
|
|
||||||
argamap(
|
argamap(
|
||||||
{
|
{
|
||||||
val decrypt = cryptoAES(
|
val decrypt = AesHelper.cryptoAESHandler(
|
||||||
servers?.src ?: return@argamap,
|
base64Decode(servers?.src ?: return@argamap),
|
||||||
KEY.toByteArray(),
|
KEY.toByteArray(),
|
||||||
false
|
false,
|
||||||
|
"AES/CBC/NoPadding"
|
||||||
)
|
)
|
||||||
val source =
|
val source =
|
||||||
tryParseJson<Sources>(decrypt?.toJsonFormat())?.src?.replace("\\", "")
|
tryParseJson<Sources>(decrypt?.toJsonFormat())?.src?.replace("\\", "")
|
||||||
|
@ -206,10 +203,11 @@ class KuronimeProvider : MainAPI() {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
val decrypt = cryptoAES(
|
val decrypt = AesHelper.cryptoAESHandler(
|
||||||
servers?.mirror ?: return@argamap,
|
base64Decode(servers?.mirror ?: return@argamap),
|
||||||
KEY.toByteArray(),
|
KEY.toByteArray(),
|
||||||
false
|
false,
|
||||||
|
"AES/CBC/NoPadding"
|
||||||
)
|
)
|
||||||
tryParseJson<Mirrors>(decrypt)?.embed?.map { embed ->
|
tryParseJson<Mirrors>(decrypt)?.embed?.map { embed ->
|
||||||
embed.value.apmap {
|
embed.value.apmap {
|
||||||
|
@ -249,7 +247,7 @@ class KuronimeProvider : MainAPI() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
getQualityFromName(quality),
|
getQualityFromName(quality),
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
@ -263,86 +261,6 @@ class KuronimeProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.decodeHex(): ByteArray {
|
|
||||||
check(length % 2 == 0) { "Must have an even length" }
|
|
||||||
return chunked(2)
|
|
||||||
.map { it.toInt(16).toByte() }
|
|
||||||
.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cryptoAES(
|
|
||||||
data: String,
|
|
||||||
pass: ByteArray,
|
|
||||||
encrypt: Boolean = true
|
|
||||||
): String? {
|
|
||||||
val json = tryParseJson<AesData>(base64Decode(data))
|
|
||||||
?: throw ErrorLoadingException("No Data Found")
|
|
||||||
val (key, iv) = generateKeyAndIv(pass, json.s.decodeHex()) ?: return null
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
|
|
||||||
return if (!encrypt) {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
String(cipher.doFinal(base64DecodeArray(json.ct)))
|
|
||||||
} else {
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
base64Encode(cipher.doFinal(json.ct.toByteArray()))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class AesData(
|
|
||||||
@JsonProperty("ct") val ct: String,
|
|
||||||
@JsonProperty("iv") val iv: String,
|
|
||||||
@JsonProperty("s") val s: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Mirrors(
|
data class Mirrors(
|
||||||
@JsonProperty("embed") val embed: Map<String, Map<String, String>> = emptyMap(),
|
@JsonProperty("embed") val embed: Map<String, Map<String, String>> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 7
|
version = 8
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Paistream : Streampai() {
|
||||||
}
|
}
|
||||||
|
|
||||||
class TvMinioppai : Streampai() {
|
class TvMinioppai : Streampai() {
|
||||||
override val name = "Tv.Minioppai"
|
override val name = "Minioppai"
|
||||||
override val mainUrl = "https://tv.minioppai.org"
|
override val mainUrl = "https://tv.minioppai.org"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ class Minioppai : MainAPI() {
|
||||||
|
|
||||||
override val mainPage = mainPageOf(
|
override val mainPage = mainPageOf(
|
||||||
"$mainUrl/watch" to "New Episode",
|
"$mainUrl/watch" to "New Episode",
|
||||||
"$mainUrl/popular" to "Popular Hentai",
|
"$mainUrl/populars" to "Popular Hentai",
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getMainPage(
|
override suspend fun getMainPage(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 5
|
version = 6
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -170,8 +170,8 @@ class Nekopoi : MainAPI() {
|
||||||
link.name,
|
link.name,
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
if (link.isM3u8) link.quality else it.first,
|
if (link.type == ExtractorLinkType.M3U8) link.quality else it.first,
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -164,7 +164,7 @@ class Nimegami : MainAPI() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
getQualityFromName(quality),
|
getQualityFromName(quality),
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 22
|
version = 23
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -195,7 +195,7 @@ class OploverzProvider : MainAPI() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
name.fixQuality(),
|
name.fixQuality(),
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 13
|
version = 14
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -236,7 +236,7 @@ class OtakudesuProvider : MainAPI() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
quality,
|
quality,
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 13
|
version = 14
|
||||||
|
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
|
|
|
@ -203,7 +203,7 @@ class Samehadaku : MainAPI() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
name.fixQuality(),
|
name.fixQuality(),
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import org.jetbrains.kotlin.konan.properties.Properties
|
import org.jetbrains.kotlin.konan.properties.Properties
|
||||||
|
|
||||||
// use an integer for version numbers
|
// use an integer for version numbers
|
||||||
version = 163
|
version = 164
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
package com.hexated
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.extractors.Filesim
|
||||||
|
import com.lagradost.cloudstream3.extractors.GMPlayer
|
||||||
|
import com.lagradost.cloudstream3.extractors.StreamSB
|
||||||
|
import com.lagradost.cloudstream3.extractors.Voe
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
open class Playm4u : ExtractorApi() {
|
||||||
|
override val name = "Playm4u"
|
||||||
|
override val mainUrl = "https://play9str.playm4u.xyz"
|
||||||
|
override val requiresReferer = true
|
||||||
|
private val password = "plhq@@@22"
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val document = app.get(url, referer = referer).document
|
||||||
|
val script = document.selectFirst("script:containsData(idfile =)")?.data() ?: return
|
||||||
|
val passScript = document.selectFirst("script:containsData(domain_ref =)")?.data() ?: return
|
||||||
|
|
||||||
|
val pass = passScript.substringAfter("CryptoJS.MD5('").substringBefore("')")
|
||||||
|
val amount = passScript.substringAfter(".toString()), ").substringBefore("));").toInt()
|
||||||
|
|
||||||
|
val idFile = "idfile\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
|
||||||
|
val idUser = "idUser\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
|
||||||
|
val domainApi = "DOMAIN_API\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
|
||||||
|
val nameKeyV3 = "NameKeyV3\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
|
||||||
|
val dataEnc = caesarShift(
|
||||||
|
mahoa(
|
||||||
|
"Win32|$idUser|$idFile|$referer",
|
||||||
|
md5(pass)
|
||||||
|
), amount
|
||||||
|
).toHex()
|
||||||
|
|
||||||
|
val captchaKey =
|
||||||
|
document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
||||||
|
.attr("src").substringAfter("render=")
|
||||||
|
val token = getCaptchaToken(
|
||||||
|
url,
|
||||||
|
captchaKey,
|
||||||
|
referer = referer
|
||||||
|
)
|
||||||
|
|
||||||
|
val source = app.post(
|
||||||
|
domainApi, data = mapOf(
|
||||||
|
"namekey" to nameKeyV3,
|
||||||
|
"token" to "$token",
|
||||||
|
"referrer" to "$referer",
|
||||||
|
"data" to "$dataEnc|${md5(dataEnc + password)}",
|
||||||
|
), referer = "$mainUrl/"
|
||||||
|
).parsedSafe<Source>()
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
source?.data ?: return,
|
||||||
|
"$mainUrl/",
|
||||||
|
Qualities.P1080.value,
|
||||||
|
INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
source.sub?.substringBefore("|")?.toLanguage() ?: return,
|
||||||
|
source.sub.substringAfter("|"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun caesarShift(str: String, amount: Int): String {
|
||||||
|
var output = ""
|
||||||
|
val adjustedAmount = if (amount < 0) amount + 26 else amount
|
||||||
|
for (element in str) {
|
||||||
|
var c = element
|
||||||
|
if (c.isLetter()) {
|
||||||
|
val code = c.code
|
||||||
|
c = when (code) {
|
||||||
|
in 65..90 -> ((code - 65 + adjustedAmount) % 26 + 65).toChar()
|
||||||
|
in 97..122 -> ((code - 97 + adjustedAmount) % 26 + 97).toChar()
|
||||||
|
else -> c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += c
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mahoa(input: String, key: String): String {
|
||||||
|
val a = CryptoJS.encrypt(key, input)
|
||||||
|
return a.replace("U2FsdGVkX1", "")
|
||||||
|
.replace("/", "|a")
|
||||||
|
.replace("+", "|b")
|
||||||
|
.replace("=", "|c")
|
||||||
|
.replace("|", "-z")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun md5(input: String): String {
|
||||||
|
val md = MessageDigest.getInstance("MD5")
|
||||||
|
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toHex(): String {
|
||||||
|
return this.toByteArray().joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.findIn(data: String): String {
|
||||||
|
return this.toRegex().find(data)?.groupValues?.get(1) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toLanguage() : String {
|
||||||
|
return if(this == "EN") "English" else this
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
@JsonProperty("data") val data: String? = null,
|
||||||
|
@JsonProperty("sub") val sub: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TravelR : GMPlayer() {
|
||||||
|
override val name = "TravelR"
|
||||||
|
override val mainUrl = "https://travel-russia.xyz"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Mwish : Filesim() {
|
||||||
|
override val name = "Mwish"
|
||||||
|
override var mainUrl = "https://mwish.pro"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Animefever : Filesim() {
|
||||||
|
override val name = "Animefever"
|
||||||
|
override var mainUrl = "https://animefever.fun"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Multimovies : Filesim() {
|
||||||
|
override val name = "Multimovies"
|
||||||
|
override var mainUrl = "https://multimovies.cloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultimoviesSB : StreamSB() {
|
||||||
|
override var name = "Multimovies"
|
||||||
|
override var mainUrl = "https://multimovies.website"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Yipsu : Voe() {
|
||||||
|
override val name = "Yipsu"
|
||||||
|
override var mainUrl = "https://yip.su"
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.hexated
|
package com.hexated
|
||||||
|
|
||||||
import com.hexated.AesHelper.cryptoAESHandler
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
@ -10,6 +9,7 @@ import com.lagradost.cloudstream3.extractors.Filesim
|
||||||
import com.lagradost.cloudstream3.extractors.GMPlayer
|
import com.lagradost.cloudstream3.extractors.GMPlayer
|
||||||
import com.lagradost.cloudstream3.extractors.StreamSB
|
import com.lagradost.cloudstream3.extractors.StreamSB
|
||||||
import com.lagradost.cloudstream3.extractors.Voe
|
import com.lagradost.cloudstream3.extractors.Voe
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||||
import com.lagradost.cloudstream3.extractors.helper.GogoHelper
|
import com.lagradost.cloudstream3.extractors.helper.GogoHelper
|
||||||
import com.lagradost.cloudstream3.network.CloudflareKiller
|
import com.lagradost.cloudstream3.network.CloudflareKiller
|
||||||
import com.lagradost.nicehttp.RequestBodyTypes
|
import com.lagradost.nicehttp.RequestBodyTypes
|
||||||
|
@ -127,7 +127,7 @@ object SoraExtractor : SoraStream() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
if (link.name == "VidSrc") Qualities.P1080.value else link.quality,
|
if (link.name == "VidSrc") Qualities.P1080.value else link.quality,
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
@ -273,7 +273,7 @@ object SoraExtractor : SoraStream() {
|
||||||
video.url,
|
video.url,
|
||||||
video.referer,
|
video.referer,
|
||||||
Qualities.P1080.value,
|
Qualities.P1080.value,
|
||||||
video.isM3u8,
|
video.type,
|
||||||
video.headers,
|
video.headers,
|
||||||
video.extractorData
|
video.extractorData
|
||||||
)
|
)
|
||||||
|
@ -415,7 +415,7 @@ object SoraExtractor : SoraStream() {
|
||||||
} else {
|
} else {
|
||||||
"$idlixAPI/episode/$fixTitle-season-$season-episode-$episode"
|
"$idlixAPI/episode/$fixTitle-season-$season-episode-$episode"
|
||||||
}
|
}
|
||||||
invokeWpmovies(url, subtitleCallback, callback, encrypt = true, key = "\\x5a\\x6d\\x5a\\x6c\\x4e\\x7a\\x55\\x79\\x4d\\x54\\x56\\x6a\\x5a\\x47\\x52\\x69\\x5a\\x44\\x55\\x30\\x5a\\x6d\\x59\\x35\\x4f\\x57\\x45\\x33\\x4d\\x44\\x4a\\x69\\x4e\\x32\\x4a\\x6c\\x4f\\x54\\x42\\x6c\\x4e\\x7a\\x49\\x3d")
|
invokeWpmovies(url, subtitleCallback, callback, encrypt = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invokeMultimovies(
|
suspend fun invokeMultimovies(
|
||||||
|
@ -480,7 +480,7 @@ object SoraExtractor : SoraStream() {
|
||||||
)
|
)
|
||||||
val source = tryParseJson<ResponseHash>(json.text)?.let {
|
val source = tryParseJson<ResponseHash>(json.text)?.let {
|
||||||
when {
|
when {
|
||||||
encrypt -> cryptoAESHandler(it.embed_url,(key ?: return@apmap).toByteArray(), false)?.fixBloat()
|
encrypt -> cryptoAESHandler(it.embed_url,(it.key ?: return@apmap).toByteArray(), false)?.fixBloat()
|
||||||
fixIframe -> Jsoup.parse(it.embed_url).select("IFRAME").attr("SRC")
|
fixIframe -> Jsoup.parse(it.embed_url).select("IFRAME").attr("SRC")
|
||||||
else -> it.embed_url
|
else -> it.embed_url
|
||||||
}
|
}
|
||||||
|
@ -711,10 +711,10 @@ object SoraExtractor : SoraStream() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
when {
|
when {
|
||||||
link.isM3u8 -> link.quality
|
link.type == ExtractorLinkType.M3U8 -> link.quality
|
||||||
else -> getQualityFromName(it.first)
|
else -> getQualityFromName(it.first)
|
||||||
},
|
},
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
@ -1039,10 +1039,10 @@ object SoraExtractor : SoraStream() {
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
when {
|
when {
|
||||||
link.isM3u8 -> link.quality
|
link.type == ExtractorLinkType.M3U8 -> link.quality
|
||||||
else -> it.third
|
else -> it.third
|
||||||
},
|
},
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
@ -1517,7 +1517,9 @@ object SoraExtractor : SoraStream() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val res = app.get("$m4uhdAPI/search/${title.createSlug()}.html").document
|
val req = app.get("$m4uhdAPI/search/${title.createSlug()}.html")
|
||||||
|
val referer = getBaseUrl(req.url)
|
||||||
|
val res = req.document
|
||||||
val scriptData = res.select("div.row div.item").map {
|
val scriptData = res.select("div.row div.item").map {
|
||||||
Triple(
|
Triple(
|
||||||
it.selectFirst("img.imagecover")?.attr("title"),
|
it.selectFirst("img.imagecover")?.attr("title"),
|
||||||
|
@ -1538,7 +1540,7 @@ object SoraExtractor : SoraStream() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val link = fixUrl(script?.third ?: return, m4uhdAPI)
|
val link = fixUrl(script?.third ?: return, referer)
|
||||||
val request = app.get(link)
|
val request = app.get(link)
|
||||||
var cookiesSet = request.headers.filter { it.first == "set-cookie" }
|
var cookiesSet = request.headers.filter { it.first == "set-cookie" }
|
||||||
var xsrf =
|
var xsrf =
|
||||||
|
@ -1558,7 +1560,7 @@ object SoraExtractor : SoraStream() {
|
||||||
?: return
|
?: return
|
||||||
val idepisode = episodeData.select("button").attr("idepisode") ?: return
|
val idepisode = episodeData.select("button").attr("idepisode") ?: return
|
||||||
val requestEmbed = app.post(
|
val requestEmbed = app.post(
|
||||||
"$m4uhdAPI/ajaxtv", data = mapOf(
|
"$referer/ajaxtv", data = mapOf(
|
||||||
"idepisode" to idepisode, "_token" to "$token"
|
"idepisode" to idepisode, "_token" to "$token"
|
||||||
), referer = link, headers = mapOf(
|
), referer = link, headers = mapOf(
|
||||||
"X-Requested-With" to "XMLHttpRequest",
|
"X-Requested-With" to "XMLHttpRequest",
|
||||||
|
@ -1572,14 +1574,16 @@ object SoraExtractor : SoraStream() {
|
||||||
cookiesSet.find { it.second.contains("XSRF-TOKEN") }?.second?.substringAfter("XSRF-TOKEN=")
|
cookiesSet.find { it.second.contains("XSRF-TOKEN") }?.second?.substringAfter("XSRF-TOKEN=")
|
||||||
?.substringBefore(";")
|
?.substringBefore(";")
|
||||||
session =
|
session =
|
||||||
cookiesSet.find { it.second.contains("laravel_session") }?.second?.substringAfter("laravel_session=")
|
cookiesSet.find { it.second.contains("laravel_session") }?.second?.substringAfter(
|
||||||
|
"laravel_session="
|
||||||
|
)
|
||||||
?.substringBefore(";")
|
?.substringBefore(";")
|
||||||
requestEmbed.document.select("div.le-server span").map { it.attr("data") }
|
requestEmbed.document.select("div.le-server span").map { it.attr("data") }
|
||||||
}
|
}
|
||||||
|
|
||||||
m4uData.apmap { data ->
|
m4uData.apmap { data ->
|
||||||
val iframe = app.post(
|
val iframe = app.post(
|
||||||
"$m4uhdAPI/ajax",
|
"$referer/ajax",
|
||||||
data = mapOf(
|
data = mapOf(
|
||||||
"m4u" to data, "_token" to "$token"
|
"m4u" to data, "_token" to "$token"
|
||||||
),
|
),
|
||||||
|
@ -1594,7 +1598,7 @@ object SoraExtractor : SoraStream() {
|
||||||
),
|
),
|
||||||
).document.select("iframe").attr("src")
|
).document.select("iframe").attr("src")
|
||||||
|
|
||||||
loadExtractor(iframe, m4uhdAPI, subtitleCallback, callback)
|
loadExtractor(iframe, referer, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2902,33 +2906,3 @@ object SoraExtractor : SoraStream() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TravelR : GMPlayer() {
|
|
||||||
override val name = "TravelR"
|
|
||||||
override val mainUrl = "https://travel-russia.xyz"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Mwish : Filesim() {
|
|
||||||
override val name = "Mwish"
|
|
||||||
override var mainUrl = "https://mwish.pro"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Animefever : Filesim() {
|
|
||||||
override val name = "Animefever"
|
|
||||||
override var mainUrl = "https://animefever.fun"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Multimovies : Filesim() {
|
|
||||||
override val name = "Multimovies"
|
|
||||||
override var mainUrl = "https://multimovies.cloud"
|
|
||||||
}
|
|
||||||
|
|
||||||
class MultimoviesSB : StreamSB() {
|
|
||||||
override var name = "Multimovies"
|
|
||||||
override var mainUrl = "https://multimovies.website"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Yipsu : Voe() {
|
|
||||||
override val name = "Yipsu"
|
|
||||||
override var mainUrl = "https://yip.su"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,7 @@ open class SoraStream : TmdbProvider() {
|
||||||
const val emoviesAPI = "https://emovies.si"
|
const val emoviesAPI = "https://emovies.si"
|
||||||
const val pobmoviesAPI = "https://pobmovies.cam"
|
const val pobmoviesAPI = "https://pobmovies.cam"
|
||||||
const val fourCartoonAPI = "https://4cartoon.net"
|
const val fourCartoonAPI = "https://4cartoon.net"
|
||||||
const val multimoviesAPI = "https://multimovies.xyz"
|
const val multimoviesAPI = "https://multi-movies.xyz"
|
||||||
const val netmoviesAPI = "https://netmovies.to"
|
const val netmoviesAPI = "https://netmovies.to"
|
||||||
const val momentAPI = "https://moment-explanation-i-244.site"
|
const val momentAPI = "https://moment-explanation-i-244.site"
|
||||||
const val doomoviesAPI = "https://doomovies.net"
|
const val doomoviesAPI = "https://doomovies.net"
|
||||||
|
@ -539,7 +539,7 @@ open class SoraStream : TmdbProvider() {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
invokeM4uhd(
|
if(!res.isAnime) invokeM4uhd(
|
||||||
res.title,
|
res.title,
|
||||||
res.year,
|
res.year,
|
||||||
res.season,
|
res.season,
|
||||||
|
|
|
@ -257,7 +257,7 @@ class SoraStreamLite : SoraStream() {
|
||||||
invokeFwatayako(res.imdbId, res.season, res.episode, callback)
|
invokeFwatayako(res.imdbId, res.season, res.episode, callback)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
invokeM4uhd(
|
if(!res.isAnime) invokeM4uhd(
|
||||||
res.title,
|
res.title,
|
||||||
res.year,
|
res.year,
|
||||||
res.season,
|
res.season,
|
||||||
|
|
|
@ -17,5 +17,6 @@ class SoraStreamPlugin: Plugin() {
|
||||||
registerExtractorAPI(Yipsu())
|
registerExtractorAPI(Yipsu())
|
||||||
registerExtractorAPI(Mwish())
|
registerExtractorAPI(Mwish())
|
||||||
registerExtractorAPI(TravelR())
|
registerExtractorAPI(TravelR())
|
||||||
|
registerExtractorAPI(Playm4u())
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package com.hexated
|
package com.hexated
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.hexated.DumpUtils.queryApi
|
import com.hexated.DumpUtils.queryApi
|
||||||
import com.hexated.SoraStream.Companion.anilistAPI
|
import com.hexated.SoraStream.Companion.anilistAPI
|
||||||
import com.hexated.SoraStream.Companion.base64DecodeAPI
|
import com.hexated.SoraStream.Companion.base64DecodeAPI
|
||||||
|
@ -41,6 +40,7 @@ import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
val bflixChipperKey = base64DecodeAPI("Yjc=ejM=TzA=YTk=WHE=WnU=bXU=RFo=")
|
val bflixChipperKey = base64DecodeAPI("Yjc=ejM=TzA=YTk=WHE=WnU=bXU=RFo=")
|
||||||
const val bflixKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
const val bflixKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
@ -1143,10 +1143,10 @@ suspend fun loadCustomExtractor(
|
||||||
link.url,
|
link.url,
|
||||||
link.referer,
|
link.referer,
|
||||||
when {
|
when {
|
||||||
link.isM3u8 -> link.quality
|
link.type == ExtractorLinkType.M3U8 -> link.quality
|
||||||
else -> quality ?: link.quality
|
else -> quality ?: link.quality
|
||||||
},
|
},
|
||||||
link.isM3u8,
|
link.type,
|
||||||
link.headers,
|
link.headers,
|
||||||
link.extractorData
|
link.extractorData
|
||||||
)
|
)
|
||||||
|
@ -1725,85 +1725,125 @@ object RSAEncryptionHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object AesHelper {
|
// code found on https://stackoverflow.com/a/63701411
|
||||||
|
|
||||||
fun cryptoAESHandler(
|
/**
|
||||||
data: String,
|
* Conforming with CryptoJS AES method
|
||||||
pass: ByteArray,
|
*/
|
||||||
encrypt: Boolean = true,
|
// see https://gist.github.com/thackerronak/554c985c3001b16810af5fc0eb5c358f
|
||||||
padding: String = "AES/CBC/PKCS5PADDING",
|
@Suppress("unused", "FunctionName", "SameParameterValue")
|
||||||
): String? {
|
object CryptoJS {
|
||||||
val parse = AppUtils.tryParseJson<AesData>(data) ?: return null
|
|
||||||
val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: throw ErrorLoadingException("failed to generate key")
|
private const val KEY_SIZE = 256
|
||||||
val cipher = Cipher.getInstance(padding)
|
private const val IV_SIZE = 128
|
||||||
return if (!encrypt) {
|
private const val HASH_CIPHER = "AES/CBC/PKCS7Padding"
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
private const val AES = "AES"
|
||||||
String(cipher.doFinal(base64DecodeArray(parse.ct)))
|
private const val KDF_DIGEST = "MD5"
|
||||||
} else {
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
// Seriously crypto-js, what's wrong with you?
|
||||||
base64Encode(cipher.doFinal(parse.ct.toByteArray()))
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/a/41434590/8166854
|
/**
|
||||||
private fun generateKeyAndIv(
|
* 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,
|
password: ByteArray,
|
||||||
|
keySize: Int,
|
||||||
|
ivSize: Int,
|
||||||
salt: ByteArray,
|
salt: ByteArray,
|
||||||
hashAlgorithm: String = "MD5",
|
resultKey: ByteArray,
|
||||||
keyLength: Int = 32,
|
resultIv: ByteArray
|
||||||
ivLength: Int = 16,
|
): ByteArray {
|
||||||
iterations: Int = 1
|
return EvpKDF(password, keySize, ivSize, salt, 1, KDF_DIGEST, resultKey, resultIv)
|
||||||
): List<ByteArray>? {
|
}
|
||||||
|
|
||||||
val md = MessageDigest.getInstance(hashAlgorithm)
|
@Suppress("NAME_SHADOWING")
|
||||||
val digestLength = md.digestLength
|
private fun EvpKDF(
|
||||||
val targetKeySize = keyLength + ivLength
|
password: ByteArray,
|
||||||
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
keySize: Int,
|
||||||
val generatedData = ByteArray(requiredLength)
|
ivSize: Int,
|
||||||
var generatedLength = 0
|
salt: ByteArray,
|
||||||
|
iterations: Int,
|
||||||
try {
|
hashAlgorithm: String,
|
||||||
md.reset()
|
resultKey: ByteArray,
|
||||||
|
resultIv: ByteArray
|
||||||
while (generatedLength < targetKeySize) {
|
): ByteArray {
|
||||||
if (generatedLength > 0)
|
val keySize = keySize / 32
|
||||||
md.update(
|
val ivSize = ivSize / 32
|
||||||
generatedData,
|
val targetKeySize = keySize + ivSize
|
||||||
generatedLength - digestLength,
|
val derivedBytes = ByteArray(targetKeySize * 4)
|
||||||
digestLength
|
var numberOfDerivedWords = 0
|
||||||
)
|
var block: ByteArray? = null
|
||||||
|
val hash = MessageDigest.getInstance(hashAlgorithm)
|
||||||
md.update(password)
|
while (numberOfDerivedWords < targetKeySize) {
|
||||||
md.update(salt, 0, 8)
|
if (block != null) {
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
hash.update(block)
|
||||||
|
|
||||||
for (i in 1 until iterations) {
|
|
||||||
md.update(generatedData, generatedLength, digestLength)
|
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedLength += digestLength
|
|
||||||
}
|
}
|
||||||
return listOf(
|
hash.update(password)
|
||||||
generatedData.copyOfRange(0, keyLength),
|
block = hash.digest(salt)
|
||||||
generatedData.copyOfRange(keyLength, targetKeySize)
|
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)
|
||||||
)
|
)
|
||||||
} catch (e: DigestException) {
|
numberOfDerivedWords += block.size / 4
|
||||||
return null
|
}
|
||||||
|
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 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
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue