mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat(crypto): Implement cross-platform cryptographic functions and integrate with native interop
This commit is contained in:
parent
d7efa1ff17
commit
fa7c4b3881
9 changed files with 553 additions and 106 deletions
|
|
@ -92,10 +92,21 @@ kotlin {
|
|||
}
|
||||
}
|
||||
|
||||
listOf(
|
||||
val iosTargets = listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
)
|
||||
|
||||
iosTargets.forEach { iosTarget ->
|
||||
iosTarget.compilations.getByName("main") {
|
||||
cinterops {
|
||||
create("commoncrypto") {
|
||||
defFile(project.file("src/nativeInterop/cinterop/commoncrypto.def"))
|
||||
compilerOpts("-I${project.projectDir}/src/nativeInterop/cinterop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "ComposeApp"
|
||||
isStatic = true
|
||||
|
|
|
|||
|
|
@ -2,21 +2,19 @@ package com.nuvio.app.features.addons
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.android.Android
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.request.accept
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.isSuccess
|
||||
import com.nuvio.app.core.network.IPv4FirstDns
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.net.Proxy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.text.Charsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
actual object AddonStorage {
|
||||
private const val preferencesName = "nuvio_addons"
|
||||
|
|
@ -45,95 +43,156 @@ actual object AddonStorage {
|
|||
}
|
||||
}
|
||||
|
||||
private val addonHttpClient = HttpClient(Android) {
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 10_000
|
||||
connectTimeoutMillis = 10_000
|
||||
socketTimeoutMillis = 10_000
|
||||
private val addonHttpClient = OkHttpClient.Builder()
|
||||
.dns(IPv4FirstDns())
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.proxy(Proxy.NO_PROXY)
|
||||
.build()
|
||||
|
||||
private val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||
private const val maxResponseBodyBytes = 256 * 1024
|
||||
private const val truncationSuffix = "\n...[truncated]"
|
||||
|
||||
private fun requestAllowsBody(method: String): Boolean =
|
||||
when (method.uppercase()) {
|
||||
"POST", "PUT", "PATCH", "DELETE" -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun Map<String, String>.withoutAcceptEncoding(): Map<String, String> =
|
||||
entries
|
||||
.filterNot { (key, _) -> key.equals("Accept-Encoding", ignoreCase = true) }
|
||||
.associate { (key, value) -> key to value }
|
||||
|
||||
private fun Map<String, String>.getHeaderIgnoreCase(name: String): String? =
|
||||
entries.firstOrNull { (key, _) -> key.equals(name, ignoreCase = true) }?.value
|
||||
|
||||
private data class LimitedReadResult(
|
||||
val bytes: ByteArray,
|
||||
val truncated: Boolean,
|
||||
)
|
||||
|
||||
private fun readAtMostBytes(stream: InputStream, maxBytes: Int): LimitedReadResult {
|
||||
val out = ByteArrayOutputStream(minOf(maxBytes, 16 * 1024))
|
||||
val buffer = ByteArray(8 * 1024)
|
||||
var remaining = maxBytes
|
||||
var truncated = false
|
||||
|
||||
while (remaining > 0) {
|
||||
val read = stream.read(buffer, 0, minOf(buffer.size, remaining))
|
||||
if (read <= 0) break
|
||||
out.write(buffer, 0, read)
|
||||
remaining -= read
|
||||
}
|
||||
|
||||
if (remaining == 0) {
|
||||
truncated = stream.read() != -1
|
||||
}
|
||||
|
||||
return LimitedReadResult(out.toByteArray(), truncated)
|
||||
}
|
||||
|
||||
private fun readResponseBodyLimited(body: ResponseBody?): String {
|
||||
if (body == null) return ""
|
||||
val charset = body.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8
|
||||
val readResult = body.byteStream().use { stream ->
|
||||
readAtMostBytes(stream, maxResponseBodyBytes)
|
||||
}
|
||||
|
||||
val decoded = try {
|
||||
String(readResult.bytes, charset)
|
||||
} catch (_: Exception) {
|
||||
String(readResult.bytes, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
return if (readResult.truncated) {
|
||||
decoded + truncationSuffix
|
||||
} else {
|
||||
decoded
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeTextRequest(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: Map<String, String> = emptyMap(),
|
||||
body: String = "",
|
||||
): String = withContext(Dispatchers.IO) {
|
||||
val normalizedMethod = method.uppercase()
|
||||
val sanitizedHeaders = headers.withoutAcceptEncoding()
|
||||
val builder = Request.Builder().url(url)
|
||||
sanitizedHeaders.forEach { (key, value) ->
|
||||
builder.header(key, value)
|
||||
}
|
||||
|
||||
val request = if (requestAllowsBody(normalizedMethod)) {
|
||||
val contentType = sanitizedHeaders.getHeaderIgnoreCase("Content-Type")
|
||||
?: if (normalizedMethod == "POST") "application/x-www-form-urlencoded" else "application/json"
|
||||
// Preserve exact media type and avoid implicit charset rewriting used in signed APIs like MovieBox.
|
||||
val requestBody = body.toByteArray(Charsets.UTF_8).toRequestBody(contentType.toMediaType())
|
||||
builder.method(normalizedMethod, requestBody)
|
||||
} else {
|
||||
builder.method(normalizedMethod, null)
|
||||
}.build()
|
||||
|
||||
addonHttpClient.newCall(request).execute().use { response ->
|
||||
val payload = readResponseBodyLimited(response.body)
|
||||
if (!response.isSuccessful) {
|
||||
error("Request failed with HTTP ${response.code}")
|
||||
}
|
||||
if (payload.isBlank()) {
|
||||
throw IllegalStateException("Empty response body")
|
||||
}
|
||||
payload
|
||||
}
|
||||
expectSuccess = false
|
||||
}
|
||||
|
||||
actual suspend fun httpGetText(url: String): String =
|
||||
addonHttpClient
|
||||
.get(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
}
|
||||
.let { response ->
|
||||
val payload = response.bodyAsText()
|
||||
if (!response.status.isSuccess()) {
|
||||
error("Request failed with HTTP ${response.status.value}")
|
||||
}
|
||||
if (payload.isBlank()) {
|
||||
throw IllegalStateException("Empty response body")
|
||||
}
|
||||
payload
|
||||
}
|
||||
executeTextRequest(
|
||||
method = "GET",
|
||||
url = url,
|
||||
headers = mapOf("Accept" to "application/json"),
|
||||
)
|
||||
|
||||
actual suspend fun httpPostJson(url: String, body: String): String =
|
||||
addonHttpClient
|
||||
.post(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody(body)
|
||||
}
|
||||
.let { response ->
|
||||
val payload = response.bodyAsText()
|
||||
if (!response.status.isSuccess()) {
|
||||
error("Request failed with HTTP ${response.status.value}")
|
||||
}
|
||||
if (payload.isBlank()) {
|
||||
throw IllegalStateException("Empty response body")
|
||||
}
|
||||
payload
|
||||
}
|
||||
executeTextRequest(
|
||||
method = "POST",
|
||||
url = url,
|
||||
headers = mapOf(
|
||||
"Accept" to "application/json",
|
||||
"Content-Type" to "application/json",
|
||||
),
|
||||
body = body,
|
||||
)
|
||||
|
||||
actual suspend fun httpGetTextWithHeaders(
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
): String =
|
||||
addonHttpClient
|
||||
.get(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
headers.forEach { (key, value) ->
|
||||
header(key, value)
|
||||
}
|
||||
}
|
||||
.let { response ->
|
||||
val payload = response.bodyAsText()
|
||||
if (!response.status.isSuccess()) {
|
||||
error("Request failed with HTTP ${response.status.value}")
|
||||
}
|
||||
if (payload.isBlank()) {
|
||||
throw IllegalStateException("Empty response body")
|
||||
}
|
||||
payload
|
||||
}
|
||||
executeTextRequest(
|
||||
method = "GET",
|
||||
url = url,
|
||||
headers = mapOf("Accept" to "application/json") + headers,
|
||||
)
|
||||
|
||||
actual suspend fun httpPostJsonWithHeaders(
|
||||
url: String,
|
||||
body: String,
|
||||
headers: Map<String, String>,
|
||||
): String =
|
||||
addonHttpClient
|
||||
.post(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
headers.forEach { (key, value) ->
|
||||
header(key, value)
|
||||
}
|
||||
setBody(body)
|
||||
}
|
||||
.let { response ->
|
||||
val payload = response.bodyAsText()
|
||||
if (!response.status.isSuccess()) {
|
||||
error("Request failed with HTTP ${response.status.value}")
|
||||
}
|
||||
if (payload.isBlank()) {
|
||||
throw IllegalStateException("Empty response body")
|
||||
}
|
||||
payload
|
||||
}
|
||||
executeTextRequest(
|
||||
method = "POST",
|
||||
url = url,
|
||||
headers = mapOf(
|
||||
"Accept" to "application/json",
|
||||
"Content-Type" to "application/json",
|
||||
) + headers,
|
||||
body = body,
|
||||
)
|
||||
|
||||
actual suspend fun httpRequestRaw(
|
||||
method: String,
|
||||
|
|
@ -141,25 +200,34 @@ actual suspend fun httpRequestRaw(
|
|||
headers: Map<String, String>,
|
||||
body: String,
|
||||
): RawHttpResponse =
|
||||
addonHttpClient
|
||||
.request {
|
||||
url(url)
|
||||
this.method = HttpMethod.parse(method.uppercase())
|
||||
headers.forEach { (key, value) ->
|
||||
header(key, value)
|
||||
}
|
||||
if (this.method == HttpMethod.Post || this.method == HttpMethod.Put || this.method == HttpMethod.Patch) {
|
||||
setBody(body)
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val normalizedMethod = method.uppercase()
|
||||
val sanitizedHeaders = headers.withoutAcceptEncoding()
|
||||
val builder = Request.Builder().url(url)
|
||||
sanitizedHeaders.forEach { (key, value) ->
|
||||
builder.header(key, value)
|
||||
}
|
||||
.let { response ->
|
||||
|
||||
val request = if (requestAllowsBody(normalizedMethod)) {
|
||||
val contentType = sanitizedHeaders.getHeaderIgnoreCase("Content-Type")
|
||||
?: if (normalizedMethod == "POST") "application/x-www-form-urlencoded" else "application/json"
|
||||
val requestBody = body.toByteArray(Charsets.UTF_8).toRequestBody(contentType.toMediaType())
|
||||
builder.method(normalizedMethod, requestBody)
|
||||
} else {
|
||||
builder.method(normalizedMethod, null)
|
||||
}.build()
|
||||
|
||||
addonHttpClient.newCall(request).execute().use { response ->
|
||||
RawHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
url = response.call.request.url.toString(),
|
||||
body = response.bodyAsText(),
|
||||
headers = response.headers.entries().associate { (name, values) ->
|
||||
name.lowercase() to values.joinToString(",")
|
||||
status = response.code,
|
||||
statusText = response.message,
|
||||
url = response.request.url.toString(),
|
||||
body = readResponseBodyLimited(response.body),
|
||||
headers = response.headers.toMultimap().mapValues { (_, values) ->
|
||||
values.joinToString(",")
|
||||
}.mapKeys { (name, _) ->
|
||||
name.lowercase()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
internal actual fun pluginDigestHex(algorithm: String, data: String): String {
|
||||
val normalized = algorithm.uppercase()
|
||||
val digest = MessageDigest.getInstance(normalized).digest(data.encodeToByteArray())
|
||||
return digest.joinToString(separator = "") { byte ->
|
||||
byte.toUByte().toString(16).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun pluginHmacHex(algorithm: String, key: String, data: String): String {
|
||||
val normalized = when (algorithm.uppercase()) {
|
||||
"SHA1" -> "HmacSHA1"
|
||||
"SHA256" -> "HmacSHA256"
|
||||
"SHA512" -> "HmacSHA512"
|
||||
"MD5" -> "HmacMD5"
|
||||
else -> error("Unsupported HMAC algorithm: $algorithm")
|
||||
}
|
||||
val mac = Mac.getInstance(normalized)
|
||||
mac.init(SecretKeySpec(key.encodeToByteArray(), normalized))
|
||||
val digest = mac.doFinal(data.encodeToByteArray())
|
||||
return digest.joinToString(separator = "") { byte ->
|
||||
byte.toUByte().toString(16).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
internal expect fun pluginDigestHex(algorithm: String, data: String): String
|
||||
|
||||
internal expect fun pluginHmacHex(algorithm: String, key: String, data: String): String
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
internal fun pluginBase64Encode(data: String): String =
|
||||
Base64.encode(data.encodeToByteArray())
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
internal fun pluginBase64Decode(data: String): String {
|
||||
val normalized = data.trim().replace("\n", "").replace("\r", "").replace(" ", "")
|
||||
val decoded = Base64.decode(normalized)
|
||||
return decoded.decodeToString()
|
||||
}
|
||||
|
||||
internal fun pluginUtf8ToHex(value: String): String =
|
||||
value.encodeToByteArray().joinToString(separator = "") { byte ->
|
||||
byte.toUByte().toString(16).padStart(2, '0')
|
||||
}
|
||||
|
||||
internal fun pluginHexToUtf8(hex: String): String {
|
||||
val normalized = hex.trim().lowercase()
|
||||
.replace(" ", "")
|
||||
.removePrefix("0x")
|
||||
if (normalized.isEmpty()) return ""
|
||||
|
||||
val evenHex = if (normalized.length % 2 == 0) normalized else "0$normalized"
|
||||
val out = ByteArray(evenHex.length / 2)
|
||||
for (index in out.indices) {
|
||||
val part = evenHex.substring(index * 2, index * 2 + 2)
|
||||
out[index] = part.toInt(16).toByte()
|
||||
}
|
||||
return out.decodeToString()
|
||||
}
|
||||
|
|
@ -120,6 +120,51 @@ internal object PluginRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
function("__crypto_digest_hex") { args ->
|
||||
val algorithm = args.getOrNull(0)?.toString() ?: "SHA256"
|
||||
val data = args.getOrNull(1)?.toString() ?: ""
|
||||
runCatching {
|
||||
pluginDigestHex(algorithm, data)
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
function("__crypto_hmac_hex") { args ->
|
||||
val algorithm = args.getOrNull(0)?.toString() ?: "SHA256"
|
||||
val key = args.getOrNull(1)?.toString() ?: ""
|
||||
val data = args.getOrNull(2)?.toString() ?: ""
|
||||
runCatching {
|
||||
pluginHmacHex(algorithm, key, data)
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
function("__crypto_base64_encode") { args ->
|
||||
val data = args.getOrNull(0)?.toString() ?: ""
|
||||
runCatching {
|
||||
pluginBase64Encode(data)
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
function("__crypto_base64_decode") { args ->
|
||||
val data = args.getOrNull(0)?.toString() ?: ""
|
||||
runCatching {
|
||||
pluginBase64Decode(data)
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
function("__crypto_utf8_to_hex") { args ->
|
||||
val data = args.getOrNull(0)?.toString() ?: ""
|
||||
runCatching {
|
||||
pluginUtf8ToHex(data)
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
function("__crypto_hex_to_utf8") { args ->
|
||||
val data = args.getOrNull(0)?.toString() ?: ""
|
||||
runCatching {
|
||||
pluginHexToUtf8(data)
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
function("__parse_url") { args ->
|
||||
parseUrl(args.getOrNull(0)?.toString() ?: "")
|
||||
}
|
||||
|
|
@ -607,6 +652,149 @@ internal object PluginRuntime {
|
|||
this._params = sorted;
|
||||
};
|
||||
|
||||
function __hexToWords(hex) {
|
||||
var words = [];
|
||||
for (var i = 0; i < hex.length; i += 8) {
|
||||
var chunk = hex.substring(i, i + 8);
|
||||
while (chunk.length < 8) chunk += '0';
|
||||
words.push(parseInt(chunk, 16) | 0);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
function __wordsToHex(words, sigBytes) {
|
||||
var hex = '';
|
||||
for (var i = 0; i < sigBytes; i++) {
|
||||
var word = words[i >>> 2] || 0;
|
||||
var byte = (word >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
var part = byte.toString(16);
|
||||
if (part.length < 2) part = '0' + part;
|
||||
hex += part;
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
function __wordArrayToHex(value) {
|
||||
if (!value) return '';
|
||||
if (typeof value.__hex === 'string') return value.__hex.toLowerCase();
|
||||
if (Array.isArray(value.words) && typeof value.sigBytes === 'number') {
|
||||
return __wordsToHex(value.words, value.sigBytes);
|
||||
}
|
||||
return __crypto_utf8_to_hex(String(value));
|
||||
}
|
||||
|
||||
function __buildWordArray(hex, utf8Override) {
|
||||
var normalizedHex = (hex || '').toLowerCase();
|
||||
if (normalizedHex.length % 2 !== 0) normalizedHex = '0' + normalizedHex;
|
||||
var wordArray = {
|
||||
__hex: normalizedHex,
|
||||
__utf8: utf8Override !== undefined ? utf8Override : __crypto_hex_to_utf8(normalizedHex),
|
||||
sigBytes: normalizedHex.length / 2,
|
||||
words: __hexToWords(normalizedHex),
|
||||
toString: function(encoder) {
|
||||
if (!encoder || encoder === CryptoJS.enc.Hex) return this.__hex;
|
||||
if (encoder === CryptoJS.enc.Utf8) return this.__utf8;
|
||||
if (encoder === CryptoJS.enc.Base64) return __crypto_base64_encode(this.__utf8);
|
||||
return this.__hex;
|
||||
},
|
||||
clamp: function() {
|
||||
return this;
|
||||
},
|
||||
concat: function(other) {
|
||||
var otherHex = __wordArrayToHex(other);
|
||||
this.__hex += otherHex;
|
||||
this.__utf8 = __crypto_hex_to_utf8(this.__hex);
|
||||
this.sigBytes = this.__hex.length / 2;
|
||||
this.words = __hexToWords(this.__hex);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
return wordArray;
|
||||
}
|
||||
|
||||
function __wordArrayFromHex(hex) {
|
||||
return __buildWordArray(hex, undefined);
|
||||
}
|
||||
|
||||
function __wordArrayFromUtf8(text) {
|
||||
var utf8 = text == null ? '' : String(text);
|
||||
return __buildWordArray(__crypto_utf8_to_hex(utf8), utf8);
|
||||
}
|
||||
|
||||
function __wordArrayFromBase64(base64) {
|
||||
return __wordArrayFromUtf8(__crypto_base64_decode(base64 || ''));
|
||||
}
|
||||
|
||||
function __normalizeWordArrayInput(value) {
|
||||
if (value && typeof value === 'object' && typeof value.__utf8 === 'string') {
|
||||
return value.__utf8;
|
||||
}
|
||||
if (value && typeof value === 'object' && typeof value.__hex === 'string') {
|
||||
return __crypto_hex_to_utf8(value.__hex);
|
||||
}
|
||||
if (value && typeof value === 'object' && Array.isArray(value.words) && typeof value.sigBytes === 'number') {
|
||||
return __crypto_hex_to_utf8(__wordsToHex(value.words, value.sigBytes));
|
||||
}
|
||||
if (value == null) return '';
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function __cryptoHashWordArray(algorithm, message) {
|
||||
var utf8 = __normalizeWordArrayInput(message);
|
||||
var hex = __crypto_digest_hex(algorithm, utf8);
|
||||
return __wordArrayFromHex(hex);
|
||||
}
|
||||
|
||||
function __cryptoHmacWordArray(algorithm, message, key) {
|
||||
var utf8Message = __normalizeWordArrayInput(message);
|
||||
var utf8Key = __normalizeWordArrayInput(key);
|
||||
var hex = __crypto_hmac_hex(algorithm, utf8Key, utf8Message);
|
||||
return __wordArrayFromHex(hex);
|
||||
}
|
||||
|
||||
var CryptoJS = {
|
||||
enc: {
|
||||
Hex: {
|
||||
stringify: function(wordArray) {
|
||||
return __wordArrayToHex(wordArray);
|
||||
},
|
||||
parse: function(hexStr) {
|
||||
return __wordArrayFromHex(hexStr || '');
|
||||
}
|
||||
},
|
||||
Utf8: {
|
||||
stringify: function(wordArray) {
|
||||
if (wordArray && typeof wordArray.__utf8 === 'string') return wordArray.__utf8;
|
||||
if (wordArray && typeof wordArray.__hex === 'string') return __crypto_hex_to_utf8(wordArray.__hex);
|
||||
return __normalizeWordArrayInput(wordArray);
|
||||
},
|
||||
parse: function(text) {
|
||||
return __wordArrayFromUtf8(text);
|
||||
}
|
||||
},
|
||||
Base64: {
|
||||
stringify: function(wordArray) {
|
||||
if (wordArray && typeof wordArray.__utf8 === 'string') {
|
||||
return __crypto_base64_encode(wordArray.__utf8);
|
||||
}
|
||||
return __crypto_base64_encode(__normalizeWordArrayInput(wordArray));
|
||||
},
|
||||
parse: function(base64) {
|
||||
return __wordArrayFromBase64(base64);
|
||||
}
|
||||
}
|
||||
},
|
||||
MD5: function(message) { return __cryptoHashWordArray('MD5', message); },
|
||||
SHA1: function(message) { return __cryptoHashWordArray('SHA1', message); },
|
||||
SHA256: function(message) { return __cryptoHashWordArray('SHA256', message); },
|
||||
SHA512: function(message) { return __cryptoHashWordArray('SHA512', message); },
|
||||
HmacMD5: function(message, key) { return __cryptoHmacWordArray('MD5', message, key); },
|
||||
HmacSHA1: function(message, key) { return __cryptoHmacWordArray('SHA1', message, key); },
|
||||
HmacSHA256: function(message, key) { return __cryptoHmacWordArray('SHA256', message, key); },
|
||||
HmacSHA512: function(message, key) { return __cryptoHmacWordArray('SHA512', message, key); }
|
||||
};
|
||||
globalThis.CryptoJS = CryptoJS;
|
||||
|
||||
var cheerio = {
|
||||
load: function(html) {
|
||||
var docId = __cheerio_load(html);
|
||||
|
|
@ -743,6 +931,9 @@ internal object PluginRuntime {
|
|||
if (moduleName === 'cheerio' || moduleName === 'cheerio-without-node-native' || moduleName === 'react-native-cheerio') {
|
||||
return cheerio;
|
||||
}
|
||||
if (moduleName === 'crypto-js') {
|
||||
return CryptoJS;
|
||||
}
|
||||
throw new Error("Module '" + moduleName + "' is not available");
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.refTo
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_MD5
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_MD5_DIGEST_LENGTH
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_SHA1
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_SHA1_DIGEST_LENGTH
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_SHA256
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_SHA256_DIGEST_LENGTH
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_SHA512
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CC_SHA512_DIGEST_LENGTH
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CCHmac
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCHmacAlgMD5
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCHmacAlgSHA1
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCHmacAlgSHA256
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCHmacAlgSHA512
|
||||
|
||||
private fun UByteArray.toHex(): String = joinToString(separator = "") { byte ->
|
||||
byte.toString(16).padStart(2, '0')
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal actual fun pluginDigestHex(algorithm: String, data: String): String {
|
||||
val normalized = algorithm.uppercase()
|
||||
val input = data.encodeToByteArray()
|
||||
val output = UByteArray(
|
||||
when (normalized) {
|
||||
"MD5" -> CC_MD5_DIGEST_LENGTH.toInt()
|
||||
"SHA1" -> CC_SHA1_DIGEST_LENGTH.toInt()
|
||||
"SHA256" -> CC_SHA256_DIGEST_LENGTH.toInt()
|
||||
"SHA512" -> CC_SHA512_DIGEST_LENGTH.toInt()
|
||||
else -> error("Unsupported digest algorithm: $algorithm")
|
||||
},
|
||||
)
|
||||
|
||||
when (normalized) {
|
||||
"MD5" -> CC_MD5(input.refTo(0), input.size.toUInt(), output.refTo(0))
|
||||
"SHA1" -> CC_SHA1(input.refTo(0), input.size.toUInt(), output.refTo(0))
|
||||
"SHA256" -> CC_SHA256(input.refTo(0), input.size.toUInt(), output.refTo(0))
|
||||
"SHA512" -> CC_SHA512(input.refTo(0), input.size.toUInt(), output.refTo(0))
|
||||
}
|
||||
|
||||
return output.toHex()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal actual fun pluginHmacHex(algorithm: String, key: String, data: String): String {
|
||||
val normalized = algorithm.uppercase()
|
||||
val keyBytes = key.encodeToByteArray()
|
||||
val input = data.encodeToByteArray()
|
||||
|
||||
val (alg, outputSize) = when (normalized) {
|
||||
"MD5" -> kCCHmacAlgMD5 to CC_MD5_DIGEST_LENGTH.toInt()
|
||||
"SHA1" -> kCCHmacAlgSHA1 to CC_SHA1_DIGEST_LENGTH.toInt()
|
||||
"SHA256" -> kCCHmacAlgSHA256 to CC_SHA256_DIGEST_LENGTH.toInt()
|
||||
"SHA512" -> kCCHmacAlgSHA512 to CC_SHA512_DIGEST_LENGTH.toInt()
|
||||
else -> error("Unsupported HMAC algorithm: $algorithm")
|
||||
}
|
||||
|
||||
val output = UByteArray(outputSize)
|
||||
CCHmac(
|
||||
alg,
|
||||
keyBytes.refTo(0),
|
||||
keyBytes.size.toULong(),
|
||||
input.refTo(0),
|
||||
input.size.toULong(),
|
||||
output.refTo(0),
|
||||
)
|
||||
|
||||
return output.toHex()
|
||||
}
|
||||
3
composeApp/src/nativeInterop/cinterop/commoncrypto.def
Normal file
3
composeApp/src/nativeInterop/cinterop/commoncrypto.def
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
language = C
|
||||
headers = commoncrypto_shim.h
|
||||
package = com.nuvio.app.features.plugins.cryptointerop
|
||||
33
composeApp/src/nativeInterop/cinterop/commoncrypto_shim.h
Normal file
33
composeApp/src/nativeInterop/cinterop/commoncrypto_shim.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef uint32_t CC_LONG;
|
||||
|
||||
enum {
|
||||
CC_MD5_DIGEST_LENGTH = 16,
|
||||
CC_SHA1_DIGEST_LENGTH = 20,
|
||||
CC_SHA256_DIGEST_LENGTH = 32,
|
||||
CC_SHA512_DIGEST_LENGTH = 64,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
kCCHmacAlgSHA1 = 0,
|
||||
kCCHmacAlgMD5 = 1,
|
||||
kCCHmacAlgSHA256 = 2,
|
||||
kCCHmacAlgSHA384 = 3,
|
||||
kCCHmacAlgSHA512 = 4,
|
||||
} CCHmacAlgorithm;
|
||||
|
||||
unsigned char *CC_MD5(const void *data, CC_LONG len, unsigned char *md);
|
||||
unsigned char *CC_SHA1(const void *data, CC_LONG len, unsigned char *md);
|
||||
unsigned char *CC_SHA256(const void *data, CC_LONG len, unsigned char *md);
|
||||
unsigned char *CC_SHA512(const void *data, CC_LONG len, unsigned char *md);
|
||||
|
||||
void CCHmac(
|
||||
CCHmacAlgorithm algorithm,
|
||||
const void *key,
|
||||
size_t keyLength,
|
||||
const void *data,
|
||||
size_t dataLength,
|
||||
void *macOut
|
||||
);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#Kotlin
|
||||
kotlin.code.style=official
|
||||
kotlin.daemon.jvmargs=-Xmx3072M
|
||||
kotlin.mpp.enableCInteropCommonization=true
|
||||
|
||||
#Gradle
|
||||
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
|
||||
|
|
|
|||
Loading…
Reference in a new issue