From 8f9cda310bf5cfb25b5c40545ab1425eb07001b2 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 20 May 2026 03:15:27 -0600 Subject: [PATCH 1/2] Migrate crypto from javax to cryptography-kotlin in library --- gradle/libs.versions.toml | 4 + library/build.gradle.kts | 1 + .../cloudstream3/extractors/ByseSX.kt | 27 ++-- .../cloudstream3/extractors/Rabbitstream.kt | 40 +++--- .../cloudstream3/extractors/VidStack.kt | 25 ++-- .../extractors/helper/AesHelper.kt | 92 ++++++------- .../extractors/helper/CryptoJSHelper.kt | 121 +++++++++--------- .../extractors/helper/GogoHelper.kt | 42 +++--- .../cloudstream3/utils/M3u8Helper.kt | 30 ++--- 9 files changed, 191 insertions(+), 191 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a97145c3f81..80539ccafb9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ colorpicker = "6b46b49" conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything constraintlayout = "2.2.1" coreKtx = "1.18.0" +cryptography = "0.6.0" desugar_jdk_libs_nio = "2.1.5" dokkaGradlePlugin = "2.2.0" espressoCore = "3.7.0" @@ -69,6 +70,8 @@ conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } core = { module = "androidx.test:core" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography" } +cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography" } databinding = { module = "androidx.databinding:viewbinding", version.ref = "androidGradlePlugin" } desugar_jdk_libs_nio = { module = "com.android.tools:desugar_jdk_libs_nio", version.ref = "desugar_jdk_libs_nio" } espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } @@ -128,6 +131,7 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref [bundles] coil = ["coil", "coil-network-okhttp"] +cryptography = ["cryptography-core", "cryptography-provider-optimal"] lifecycle = ["lifecycle-livedata-ktx", "lifecycle-viewmodel-ktx"] media3 = ["media3-cast", "media3-common", "media3-container", "media3-datasource-cronet", "media3-datasource-okhttp", "media3-exoplayer", "media3-exoplayer-dash", "media3-exoplayer-hls", "media3-session", "media3-ui"] navigation = ["navigation-fragment-ktx", "navigation-ui-ktx"] diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 073e49e6483..1288559f4e7 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -62,6 +62,7 @@ kotlin { implementation(libs.rhino) // Run JavaScript implementation(libs.newpipeextractor) implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit + implementation(libs.bundles.cryptography) // Cryptography } } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt index 38d35da2eda..1265e61c77a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt @@ -8,13 +8,13 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES import java.net.URI import java.nio.charset.StandardCharsets -import javax.crypto.Cipher -import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.SecretKeySpec -class Bysezejataos : ByseSX() { +class Bysezejataos : ByseSX() { override var name = "Bysezejataos" override var mainUrl = "https://bysezejataos.com" } @@ -39,6 +39,8 @@ open class ByseSX : ExtractorApi() { override var mainUrl = "https://byse.sx" override val requiresReferer = true + private val aesGcm = CryptographyProvider.Default.get(AES.GCM) + private fun b64UrlDecode(s: String): ByteArray { val fixed = s.replace('-', '+').replace('_', '/') val pad = (4 - fixed.length % 4) % 4 @@ -83,23 +85,22 @@ open class ByseSX : ExtractorApi() { return p1 + p2 } + @OptIn(DelicateCryptographyApi::class) private fun decryptPlayback(playback: Playback): String? { val keyBytes = buildAesKey(playback) val ivBytes = b64UrlDecode(playback.iv) val cipherBytes = b64UrlDecode(playback.payload) - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - val spec = GCMParameterSpec(128, ivBytes) - val secretKey = SecretKeySpec(keyBytes, "AES") - cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + val aesKey = aesGcm.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes) + // 128-bit GCM tag (default) + val cipher = aesKey.cipher() + val plainBytes = cipher.decryptWithIvBlocking(ivBytes, cipherBytes) - val plainBytes = cipher.doFinal(cipherBytes) var jsonStr = String(plainBytes, StandardCharsets.UTF_8) - if (jsonStr.startsWith("\uFEFF")) jsonStr = jsonStr.substring(1) val root = try { - tryParseJson((jsonStr)) + tryParseJson(jsonStr) } catch (_: Exception) { return null } @@ -107,7 +108,6 @@ open class ByseSX : ExtractorApi() { return root?.sources?.firstOrNull()?.url } - override suspend fun getUrl( url: String, referer: String?, @@ -116,8 +116,7 @@ open class ByseSX : ExtractorApi() { ) { val refererUrl = getBaseUrl(url) val playbackRoot = getPlayback(url) ?: return - val streamUrl = decryptPlayback(playbackRoot.playback) ?: return - + val streamUrl = decryptPlayback(playbackRoot.playback) ?: return val headers = mapOf("Referer" to refererUrl) M3u8Helper.generateM3u8( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index 98598dd2865..1a6a1ce9bb0 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -12,11 +12,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES +import dev.whyoleg.cryptography.algorithms.MD5 import java.nio.charset.StandardCharsets -import java.security.MessageDigest -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec class Megacloud : Rabbitstream() { override val name = "Megacloud" @@ -63,7 +63,6 @@ class Megacloud : Rabbitstream() { return indexPairs } - } class Dokicloud : Rabbitstream() { @@ -80,6 +79,10 @@ open class Rabbitstream : ExtractorApi() { open val embed = "ajax/embed-4" open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt" + private val aesCbc = CryptographyProvider.Default.get(AES.CBC) + @OptIn(DelicateCryptographyApi::class) + private val md5Hasher = CryptographyProvider.Default.get(MD5).hasher() + override suspend fun getUrl( url: String, referer: String?, @@ -123,8 +126,6 @@ open class Rabbitstream : ExtractorApi() { ) ) } - - } open suspend fun extractRealKey(sources: String): Pair { @@ -141,8 +142,8 @@ open class Rabbitstream : ExtractorApi() { private fun decrypt(input: String, key: String): String { return decryptSourceUrl( generateKey( - base64DecodeArray(input).copyOfRange(8, 16), - key.toByteArray() + salt = base64DecodeArray(input).copyOfRange(8, 16), + secret = key.toByteArray() ), input ) } @@ -157,20 +158,18 @@ open class Rabbitstream : ExtractorApi() { return currentKey } - private fun md5(input: ByteArray): ByteArray { - return MessageDigest.getInstance("MD5").digest(input) - } + private fun md5(input: ByteArray): ByteArray = + md5Hasher.hashBlocking(input) + @OptIn(DelicateCryptographyApi::class) private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String { val cipherData = base64DecodeArray(sourceUrl) val encrypted = cipherData.copyOfRange(16, cipherData.size) - val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding") - aesCBC.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(decryptionKey.copyOfRange(0, 32), "AES"), - IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) - ) - val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found") + val keyBytes = decryptionKey.copyOfRange(0, 32) + val ivBytes = decryptionKey.copyOfRange(32, decryptionKey.size) + + val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes) + val decryptedData = aesKey.cipher(padding = true).decryptWithIvBlocking(ivBytes, encrypted) return String(decryptedData, StandardCharsets.UTF_8) } @@ -196,5 +195,4 @@ open class Rabbitstream : ExtractorApi() { @JsonProperty("encrypted") val encrypted: Boolean? = null, @JsonProperty("tracks") val tracks: List? = emptyList(), ) - -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt index 846fd851db9..2c18b34db45 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt @@ -10,10 +10,10 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.fixUrl import com.lagradost.cloudstream3.utils.newExtractorLink +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES import java.net.URI -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec class Server1uns : VidStack() { override var name = "Vidstack" @@ -32,8 +32,7 @@ open class VidStack : ExtractorApi() { referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit - ) - { + ) { val headers = mapOf("User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0") val hash = url.substringAfterLast("#").substringAfter("/") val baseurl = getBaseUrl(url) @@ -93,16 +92,16 @@ open class VidStack : ExtractorApi() { } object AesHelper { - private const val TRANSFORMATION = "AES/CBC/PKCS5PADDING" + private val aesCbc = CryptographyProvider.Default.get(AES.CBC) + @OptIn(DelicateCryptographyApi::class) fun decryptAES(inputHex: String, key: String, iv: String): String { - val cipher = Cipher.getInstance(TRANSFORMATION) - val secretKey = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "AES") - val ivSpec = IvParameterSpec(iv.toByteArray(Charsets.UTF_8)) - - cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) - val decryptedBytes = cipher.doFinal(inputHex.hexToByteArray()) - return String(decryptedBytes, Charsets.UTF_8) + val keyBytes = key.toByteArray(Charsets.UTF_8) + val ivBytes = iv.toByteArray(Charsets.UTF_8) + val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes) + val cipher = aesKey.cipher(padding = true) + val decrypted = cipher.decryptWithIvBlocking(ivBytes, inputHex.hexToByteArray()) + return String(decrypted, Charsets.UTF_8) } private fun String.hexToByteArray(): ByteArray { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt index 9c5ceb8c0b3..f6262251587 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt @@ -4,37 +4,42 @@ import com.fasterxml.jackson.annotation.JsonProperty 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 +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES +import dev.whyoleg.cryptography.algorithms.MD5 object AesHelper { - private const val HASH = "AES/CBC/PKCS5PADDING" - private const val KDF = "MD5" + private val provider = CryptographyProvider.Default + private val aesCbc = provider.get(AES.CBC) + @OptIn(DelicateCryptographyApi::class) + private val md5Hasher = provider.get(MD5).hasher() + @OptIn(DelicateCryptographyApi::class) fun cryptoAESHandler( data: String, pass: ByteArray, encrypt: Boolean = true, - padding: String = HASH, + // padding parameter kept for API compatibility; PKCS7 is always used + @Suppress("UNUSED_PARAMETER") padding: String = "AES/CBC/PKCS5PADDING", ): String? { val parse = AppUtils.tryParseJson(data) ?: return null val (key, iv) = generateKeyAndIv( - pass, - parse.s.hexToByteArray(), + password = pass, + salt = parse.s.hexToByteArray(), ivLength = parse.iv.length / 2, saltLength = parse.s.length / 2 ) ?: return null - val cipher = Cipher.getInstance(padding) + + val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, key) + val cipher = aesKey.cipher(padding = true) + return if (!encrypt) { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - String(cipher.doFinal(base64DecodeArray(parse.ct))) + val plainBytes = cipher.decryptWithIvBlocking(iv, base64DecodeArray(parse.ct)) + String(plainBytes) } else { - cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - base64Encode(cipher.doFinal(parse.ct.toByteArray())) + base64Encode(cipher.encryptWithIvBlocking(iv, parse.ct.toByteArray())) } } @@ -42,45 +47,43 @@ object AesHelper { fun generateKeyAndIv( password: ByteArray, salt: ByteArray, - hashAlgorithm: String = KDF, keyLength: Int = 32, ivLength: Int, saltLength: Int, - iterations: Int = 1 - ): Pair? { - - 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() + iterations: Int = 1, + ): Pair? { + return try { + val digestLength = 16 // MD5 digest is always 16 bytes + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 while (generatedLength < targetKeySize) { - if (generatedLength > 0) - md.update( - generatedData, - generatedLength - digestLength, - digestLength - ) - - md.update(password) - md.update(salt, 0, saltLength) - md.digest(generatedData, generatedLength, digestLength) + val hashFn = md5Hasher.createHashFunction() + if (generatedLength > 0) { + // update(source, startIndex, endIndex) — endIndex is exclusive + hashFn.update(generatedData, generatedLength - digestLength, generatedLength) + } + hashFn.update(password) + hashFn.update(salt, 0, saltLength) + val digest = hashFn.hashToByteArray() + digest.copyInto(generatedData, generatedLength) for (i in 1 until iterations) { - md.update(generatedData, generatedLength, digestLength) - md.digest(generatedData, generatedLength, digestLength) + val iterFn = md5Hasher.createHashFunction() + iterFn.update(generatedData, generatedLength, generatedLength + digestLength) + val iterDigest = iterFn.hashToByteArray() + iterDigest.copyInto(generatedData, generatedLength) } generatedLength += digestLength } - return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize) - } catch (e: DigestException) { - return null + + generatedData.copyOfRange(0, keyLength) to + generatedData.copyOfRange(keyLength, targetKeySize) + } catch (_: Exception) { + null } } @@ -96,5 +99,4 @@ object AesHelper { @JsonProperty("iv") val iv: String, @JsonProperty("s") val s: String ) - -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt index 6ad0524e8c5..3fcf7c6ec6b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt @@ -2,11 +2,11 @@ package com.lagradost.cloudstream3.extractors.helper import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.base64Encode -import java.security.MessageDigest -import java.security.SecureRandom -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec -import javax.crypto.spec.IvParameterSpec +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES +import dev.whyoleg.cryptography.algorithms.MD5 +import dev.whyoleg.cryptography.random.CryptographyRandom import kotlin.math.min /** @@ -16,36 +16,36 @@ import kotlin.math.min @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" + private const val KEY_SIZE = 256 + private const val IV_SIZE = 128 // Seriously crypto-js, what's wrong with you? - private const val APPEND = "Salted__" + private const val APPEND = "Salted__" + + private val provider = CryptographyProvider.Default + private val aesCbc = provider.get(AES.CBC) + @OptIn(DelicateCryptographyApi::class) + private val md5Hasher = provider.get(MD5).hasher() /** * Encrypt * @param password passphrase * @param plainText plain string */ + @OptIn(DelicateCryptographyApi::class) fun encrypt(password: String, plainText: String): String { val saltBytes = generateSalt(8) - val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_SIZE / 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 aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, key) + val cipher = aesKey.cipher(padding = true) + val cipherText = cipher.encryptWithIvBlocking(iv, plainText.toByteArray()) - 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) + // Create CryptoJS-like encrypted: "Salted__" || salt || ciphertext + val sBytes = APPEND.toByteArray() + val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size) sBytes.copyInto(destination = b, destinationOffset = 0) saltBytes.copyInto(destination = b, destinationOffset = sBytes.size) cipherText.copyInto(destination = b, destinationOffset = sBytes.size + saltBytes.size) @@ -59,53 +59,62 @@ object CryptoJS { * @param password passphrase * @param cipherText encrypted string */ + @OptIn(DelicateCryptographyApi::class) fun decrypt(password: String, cipherText: String): String { - val ctBytes = base64DecodeArray(cipherText) - val saltBytes = ctBytes.copyOfRange(8, 16) + val ctBytes = base64DecodeArray(cipherText) + val saltBytes = ctBytes.copyOfRange(8, 16) val cipherTextBytes = ctBytes.copyOfRange(16, ctBytes.size) val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_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) + val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, key) + val cipher = aesKey.cipher(padding = true) + val plainText = cipher.decryptWithIvBlocking(iv, 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) - } + private fun evpkdf( + password: ByteArray, + keySize: Int, + ivSize: Int, + salt: ByteArray, + resultKey: ByteArray, + resultIv: ByteArray, + ): ByteArray = evpkdf(password, keySize, ivSize, salt, 1, 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) + private fun evpkdf( + password: ByteArray, + keySize: Int, + ivSize: Int, + salt: ByteArray, + iterations: Int, + 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) + var block: ByteArray? = null while (numberOfDerivedWords < targetKeySize) { - if (block != null) { - hash.update(block) - } + val hashFn = md5Hasher.createHashFunction() + if (block != null) hashFn.update(block) + hashFn.update(password) + hashFn.update(salt) + block = hashFn.hashToByteArray() - hash.update(password) - block = hash.digest(salt) - hash.reset() - - // Iterations for (i in 1 until iterations) { - block = hash.digest(block!!) - hash.reset() + val iterFn = md5Hasher.createHashFunction() + iterFn.update(block!!) + block = iterFn.hashToByteArray() } - block!!.copyInto( + block.copyInto( destination = derivedBytes, destinationOffset = numberOfDerivedWords * 4, startIndex = 0, @@ -116,14 +125,10 @@ object CryptoJS { } derivedBytes.copyInto(destination = resultKey, destinationOffset = 0, startIndex = 0, endIndex = keySize * 4) - derivedBytes.copyInto(destination = resultIv, destinationOffset = 0, startIndex = keySize * 4, endIndex = (keySize * 4) + (ivSize * 4)) - + derivedBytes.copyInto(destination = resultIv, destinationOffset = 0, startIndex = keySize * 4, endIndex = (keySize + ivSize) * 4) return derivedBytes // key + iv } - private fun generateSalt(length: Int): ByteArray { - return ByteArray(length).apply { - SecureRandom().nextBytes(this) - } - } + private fun generateSalt(length: Int): ByteArray = + CryptographyRandom.nextBytes(length) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt index a16d419438e..9a576ab6a17 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt @@ -12,18 +12,20 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.newExtractorLink +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES import org.jsoup.nodes.Document import java.net.URI -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec object GogoHelper { + private val aesCbc = CryptographyProvider.Default.get(AES.CBC) + /** * @param id base64Decode(show_id) + IV * @return the encryption key - * */ + */ private fun getKey(id: String): String? { return safe { id.map { @@ -34,22 +36,23 @@ object GogoHelper { // https://github.com/saikou-app/saikou/blob/45d0a99b8a72665a29a1eadfb38c506b842a29d7/app/src/main/java/ani/saikou/parsers/anime/extractors/GogoCDN.kt#L97 // No Licence on the function + @OptIn(DelicateCryptographyApi::class) private fun cryptoHandler( string: String, iv: String, secretKeyString: String, encrypt: Boolean = true ): String { - //println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") - val ivParameterSpec = IvParameterSpec(iv.toByteArray()) - val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES") - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + val ivBytes = iv.toByteArray() + val keyBytes = secretKeyString.toByteArray() + val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes) + val cipher = aesKey.cipher(padding = true) + return if (!encrypt) { - cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec) - String(cipher.doFinal(base64DecodeArray(string))) + val plainBytes = cipher.decryptWithIvBlocking(ivBytes, base64DecodeArray(string)) + String(plainBytes) } else { - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec) - base64Encode(cipher.doFinal(string.toByteArray())) + base64Encode(cipher.encryptWithIvBlocking(ivBytes, string.toByteArray())) } } @@ -61,7 +64,7 @@ object GogoHelper { * @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off * @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey() * @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value - * */ + */ suspend fun extractVidstream( iframeUrl: String, mainApiName: String, @@ -95,8 +98,7 @@ object GogoHelper { val encryptRequestData = if (isUsingAdaptiveData) { // Only fetch the document if necessary val realDocument = document ?: app.get(iframeUrl).document - val dataEncrypted = - realDocument.select("script[data-name='episode']").attr("data-value") + val dataEncrypted = realDocument.select("script[data-name='episode']").attr("data-value") val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false) "id=$encryptedId&alias=$id&" + headers.substringAfter("&") } else { @@ -137,12 +139,8 @@ object GogoHelper { } } - sources.source?.forEach { - invokeGogoSource(it, callback) - } - sources.sourceBk?.forEach { - invokeGogoSource(it, callback) - } + sources.source?.forEach { invokeGogoSource(it, callback) } + sources.sourceBk?.forEach { invokeGogoSource(it, callback) } } data class GogoSources( @@ -160,4 +158,4 @@ object GogoHelper { data class GogoJsonData( @JsonProperty("data") val data: String? = null ) -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 23226418b48..411b2740714 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -3,11 +3,11 @@ package com.lagradost.cloudstream3.utils import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.app +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.DelicateCryptographyApi +import dev.whyoleg.cryptography.algorithms.AES import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec import kotlin.math.pow /** backwards api surface */ @@ -41,6 +41,8 @@ class M3u8Helper { object M3u8Helper2 { private val TAG = "M3u8Helper" + private val aesCbc = CryptographyProvider.Default.get(AES.CBC) + suspend fun generateM3u8( source: String, streamUrl: String, @@ -77,7 +79,6 @@ object M3u8Helper2 { Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways - //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { val split = url.split("/") @@ -98,6 +99,7 @@ object M3u8Helper2 { return toBytes16Big(index + 1) } + @OptIn(DelicateCryptographyApi::class) fun getDecrypted( secretKey: ByteArray, data: ByteArray, @@ -105,11 +107,8 @@ object M3u8Helper2 { index: Int, ): ByteArray { val ivKey = if (iv.isEmpty()) defaultIv(index) else iv - val c = Cipher.getInstance("AES/CBC/PKCS5Padding") - val skSpec = SecretKeySpec(secretKey, "AES") - val ivSpec = IvParameterSpec(ivKey) - c.init(Cipher.DECRYPT_MODE, skSpec, ivSpec) - return c.doFinal(data) + val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, secretKey) + return aesKey.cipher(padding = true).decryptWithIvBlocking(ivKey, data) } private fun getParentLink(uri: String): String { @@ -129,10 +128,7 @@ object M3u8Helper2 { ): List { val list = mutableListOf() val response = app.get(m3u8.streamUrl, headers = m3u8.headers, verify = false).text - val parsed = HlsPlaylistParser.parse( - m3u8.streamUrl, - response, - ) + val parsed = HlsPlaylistParser.parse(m3u8.streamUrl, response) var anyFound = false if (parsed != null) { @@ -280,11 +276,8 @@ object M3u8Helper2 { if (variants.isEmpty()) { throw IllegalStateException( - if (requireAudio) { - "M3u8 contains no video with audio" - } else { - "M3u8 contains no video" - } + if (requireAudio) "M3u8 contains no video with audio" + else "M3u8 contains no video" ) } @@ -311,6 +304,7 @@ object M3u8Helper2 { depth = depth - 1 ) } + // This is already a "Media Segments" file // Encryption, this is because crunchy uses it From fd3c4e939e388f49cb5af224580e5ef619cc84c2 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 22 May 2026 22:27:14 -0600 Subject: [PATCH 2/2] Fix --- .../com/lagradost/cloudstream3/extractors/VidStack.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt index 2c18b34db45..d0b92ca27ff 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt @@ -96,12 +96,12 @@ object AesHelper { @OptIn(DelicateCryptographyApi::class) fun decryptAES(inputHex: String, key: String, iv: String): String { - val keyBytes = key.toByteArray(Charsets.UTF_8) - val ivBytes = iv.toByteArray(Charsets.UTF_8) + val keyBytes = key.encodeToByteArray() + val ivBytes = iv.encodeToByteArray() val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes) val cipher = aesKey.cipher(padding = true) val decrypted = cipher.decryptWithIvBlocking(ivBytes, inputHex.hexToByteArray()) - return String(decrypted, Charsets.UTF_8) + return decrypted.decodeToString() } private fun String.hexToByteArray(): ByteArray {