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 3f6dc9fee1d..e23f55e9c2a 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 @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper +import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.base64Encode import dev.whyoleg.cryptography.CryptographyProvider @@ -14,7 +15,8 @@ import kotlin.math.min */ // see https://gist.github.com/thackerronak/554c985c3001b16810af5fc0eb5c358f @Suppress("unused", "FunctionName", "SameParameterValue") -object CryptoJS { +@Prerelease +object CryptoJSHelper { private const val KEY_SIZE = 256 private const val IV_SIZE = 128 @@ -32,15 +34,15 @@ object CryptoJS { * @param plainText plain string */ @OptIn(DelicateCryptographyApi::class) - fun encrypt(password: String, plainText: String): String { + suspend 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.encodeToByteArray(), KEY_SIZE, IV_SIZE, saltBytes, 1, key, iv) - val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, key) + val aesKey = aesCbc.keyDecoder().decodeFromByteArray(AES.Key.Format.RAW, key) val cipher = aesKey.cipher(padding = true) - val cipherText = cipher.encryptWithIvBlocking(iv, plainText.encodeToByteArray()) + val cipherText = cipher.encryptWithIv(iv, plainText.encodeToByteArray()) // Create CryptoJS-like encrypted: "Salted__" || salt || ciphertext val sBytes = APPEND.encodeToByteArray() @@ -58,6 +60,132 @@ object CryptoJS { * @param password passphrase * @param cipherText encrypted string */ + @OptIn(DelicateCryptographyApi::class) + suspend fun decrypt(password: String, cipherText: String): String { + 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) + evpkdf(password.encodeToByteArray(), KEY_SIZE, IV_SIZE, saltBytes, 1, key, iv) + + val aesKey = aesCbc.keyDecoder().decodeFromByteArray(AES.Key.Format.RAW, key) + val cipher = aesKey.cipher(padding = true) + val plainText = cipher.decryptWithIv(iv, cipherTextBytes) + return plainText.decodeToString() + } + + 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 + + while (numberOfDerivedWords < targetKeySize) { + val hashFn = md5Hasher.createHashFunction() + if (block != null) hashFn.update(block) + hashFn.update(password) + hashFn.update(salt) + block = hashFn.hashToByteArray() + + for (i in 1 until iterations) { + val iterFn = md5Hasher.createHashFunction() + iterFn.update(block!!) + block = iterFn.hashToByteArray() + } + + block.copyInto( + destination = derivedBytes, + destinationOffset = numberOfDerivedWords * 4, + startIndex = 0, + endIndex = min(block.size, (targetKeySize - numberOfDerivedWords) * 4), + ) + + numberOfDerivedWords += block.size / 4 + } + + derivedBytes.copyInto( + destination = resultKey, + destinationOffset = 0, + startIndex = 0, + endIndex = keySize * 4, + ) + + derivedBytes.copyInto( + destination = resultIv, + destinationOffset = 0, + startIndex = keySize * 4, + endIndex = (keySize + ivSize) * 4, + ) + + return derivedBytes // key + iv + } + + private fun generateSalt(length: Int): ByteArray = + CryptographyRandom.nextBytes(length) +} + +// Deprecate after next stable +/* @Deprecated( + message = "Renamed to CryptoJSHelper", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("CryptoJSHelper"), +) */ +@Suppress("unused", "FunctionName", "SameParameterValue") +object CryptoJS { + // Use this after next stable, once CryptoJSHelper is no longer @Prerelease, + // and delete everything else in this object: + /* + fun encrypt(password: String, plainText: String): String = + runBlocking { CryptoJSHelper.encrypt(password, plainText) } + + fun decrypt(password: String, cipherText: String): String = + runBlocking { CryptoJSHelper.decrypt(password, cipherText) } + */ + + 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 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 encrypt(password: String, plainText: String): String { + val saltBytes = generateSalt(8) + val key = ByteArray(KEY_SIZE / 8) + val iv = ByteArray(IV_SIZE / 8) + evpkdf(password.encodeToByteArray(), KEY_SIZE, IV_SIZE, saltBytes, 1, key, iv) + + val aesKey = aesCbc.keyDecoder().decodeFromByteArrayBlocking(AES.Key.Format.RAW, key) + val cipher = aesKey.cipher(padding = true) + val cipherText = cipher.encryptWithIvBlocking(iv, plainText.encodeToByteArray()) + + // Create CryptoJS-like encrypted: "Salted__" || salt || ciphertext + val sBytes = APPEND.encodeToByteArray() + 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) + + return base64Encode(b) + } + @OptIn(DelicateCryptographyApi::class) fun decrypt(password: String, cipherText: String): String { val ctBytes = base64DecodeArray(cipherText) @@ -107,14 +235,26 @@ object CryptoJS { destination = derivedBytes, destinationOffset = numberOfDerivedWords * 4, startIndex = 0, - endIndex = min(block.size, (targetKeySize - numberOfDerivedWords) * 4) + endIndex = min(block.size, (targetKeySize - numberOfDerivedWords) * 4), ) numberOfDerivedWords += block.size / 4 } - derivedBytes.copyInto(destination = resultKey, destinationOffset = 0, startIndex = 0, endIndex = keySize * 4) - derivedBytes.copyInto(destination = resultIv, destinationOffset = 0, startIndex = keySize * 4, endIndex = (keySize + ivSize) * 4) + derivedBytes.copyInto( + destination = resultKey, + destinationOffset = 0, + startIndex = 0, + endIndex = keySize * 4, + ) + + derivedBytes.copyInto( + destination = resultIv, + destinationOffset = 0, + startIndex = keySize * 4, + endIndex = (keySize + ivSize) * 4, + ) + return derivedBytes // key + iv }