mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-05 02:50:44 +00:00
feat(crypto): implement AES-GCM for iOS and enhance PBKDF2/Base64 support
This commit is contained in:
parent
127ec3cf4a
commit
73c97bfeb7
3 changed files with 304 additions and 46 deletions
|
|
@ -35,15 +35,54 @@ internal fun pluginPbkdf2(
|
|||
keySizeBits: Int,
|
||||
algorithm: String,
|
||||
): ByteArray {
|
||||
val normalizedAlgo = when (algorithm.uppercase()) {
|
||||
"SHA256" -> "PBKDF2WithHmacSHA256"
|
||||
"SHA1" -> "PBKDF2WithHmacSHA1"
|
||||
else -> "PBKDF2WithHmacSHA256"
|
||||
val prfAlgo = when (algorithm.uppercase()) {
|
||||
"SHA256", "HMACSHA256" -> "HmacSHA256"
|
||||
"SHA1", "HMACSHA1" -> "HmacSHA1"
|
||||
"SHA512", "HMACSHA512" -> "HmacSHA512"
|
||||
"MD5", "HMACMD5" -> "HmacMD5"
|
||||
else -> "HmacSHA256"
|
||||
}
|
||||
val factory = SecretKeyFactory.getInstance(normalizedAlgo)
|
||||
val passChars = password.map { (it.toInt() and 0xFF).toChar() }.toCharArray()
|
||||
val spec = PBEKeySpec(passChars, salt, iterations, keySizeBits)
|
||||
return factory.generateSecret(spec).encoded
|
||||
val mac = Mac.getInstance(prfAlgo)
|
||||
mac.init(SecretKeySpec(password, prfAlgo))
|
||||
|
||||
val hLen = mac.macLength
|
||||
val dkLen = keySizeBits / 8
|
||||
val dk = ByteArray(dkLen)
|
||||
|
||||
val blocks = (dkLen + hLen - 1) / hLen
|
||||
val u = ByteArray(hLen)
|
||||
val t = ByteArray(hLen)
|
||||
|
||||
val blockIndexBytes = ByteArray(4)
|
||||
|
||||
for (i in 1..blocks) {
|
||||
mac.reset()
|
||||
mac.update(salt)
|
||||
blockIndexBytes[0] = (i ushr 24).toByte()
|
||||
blockIndexBytes[1] = (i ushr 16).toByte()
|
||||
blockIndexBytes[2] = (i ushr 8).toByte()
|
||||
blockIndexBytes[3] = i.toByte()
|
||||
mac.update(blockIndexBytes)
|
||||
|
||||
val u1 = mac.doFinal()
|
||||
u1.copyInto(t)
|
||||
u1.copyInto(u)
|
||||
|
||||
for (j in 2..iterations) {
|
||||
mac.reset()
|
||||
val uj = mac.doFinal(u)
|
||||
uj.copyInto(u)
|
||||
for (k in 0 until hLen) {
|
||||
t[k] = (t[k].toInt() xor uj[k].toInt()).toByte()
|
||||
}
|
||||
}
|
||||
|
||||
val offset = (i - 1) * hLen
|
||||
val len = minOf(hLen, dkLen - offset)
|
||||
t.copyInto(dk, destinationOffset = offset, startIndex = 0, endIndex = len)
|
||||
}
|
||||
|
||||
return dk
|
||||
}
|
||||
|
||||
internal fun pluginAesEncrypt(
|
||||
|
|
@ -161,7 +200,13 @@ internal fun pluginBase64Encode(data: String): String =
|
|||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
internal fun pluginBase64Decode(data: String): String {
|
||||
val normalized = data.trim().replace("\n", "").replace("\r", "").replace(" ", "")
|
||||
var normalized = data.trim().replace("\n", "").replace("\r", "").replace(" ", "")
|
||||
// Robust URL-safe base64 decoding fallback
|
||||
normalized = normalized.replace("-", "+").replace("_", "/")
|
||||
val padNeeded = (4 - (normalized.length % 4)) % 4
|
||||
if (padNeeded > 0) {
|
||||
normalized += "=".repeat(padNeeded)
|
||||
}
|
||||
val decoded = Base64.decode(normalized)
|
||||
return decoded.decodeToString()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,30 +13,7 @@ import kotlinx.cinterop.ptr
|
|||
import kotlinx.cinterop.value
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
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
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CCKeyDerivationPBKDF
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCPBKDF2
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCPRFHmacAlgSHA1
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCPRFHmacAlgSHA256
|
||||
import com.nuvio.app.features.plugins.cryptointerop.CCCrypt
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCDecrypt
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCAlgorithmAES
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCOptionECBMode
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCEncrypt
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCOptionPKCS7Padding
|
||||
import com.nuvio.app.features.plugins.cryptointerop.kCCSuccess
|
||||
import com.nuvio.app.features.plugins.cryptointerop.*
|
||||
import platform.Security.SecRandomCopyBytes
|
||||
import platform.Security.kSecRandomDefault
|
||||
|
||||
|
|
@ -86,8 +63,10 @@ internal fun pluginPbkdf2(
|
|||
algorithm: String,
|
||||
): ByteArray {
|
||||
val prf = when (algorithm.uppercase()) {
|
||||
"SHA256" -> kCCPRFHmacAlgSHA256
|
||||
"SHA1" -> kCCPRFHmacAlgSHA1
|
||||
"SHA256", "HMACSHA256" -> kCCPRFHmacAlgSHA256
|
||||
"SHA1", "HMACSHA1" -> kCCPRFHmacAlgSHA1
|
||||
"SHA384", "HMACSHA384" -> kCCPRFHmacAlgSHA384
|
||||
"SHA512", "HMACSHA512" -> kCCPRFHmacAlgSHA512
|
||||
else -> kCCPRFHmacAlgSHA256
|
||||
}
|
||||
|
||||
|
|
@ -130,9 +109,82 @@ internal fun pluginAesEncrypt(
|
|||
): ByteArray {
|
||||
val isGcm = mode.uppercase().contains("GCM")
|
||||
if (isGcm) {
|
||||
throw UnsupportedOperationException("AES-GCM Encrypt is not yet implemented on iOS")
|
||||
var encryptedData: ByteArray? = null
|
||||
memScoped {
|
||||
val cryptorRefVar = alloc<com.nuvio.app.features.plugins.cryptointerop.CCCryptorRefVar>()
|
||||
|
||||
key.usePinned { pinnedKey ->
|
||||
iv.usePinned { pinnedIv ->
|
||||
data.usePinned { pinnedData ->
|
||||
val keyPtr = if (key.isNotEmpty()) pinnedKey.addressOf(0) else null
|
||||
val ivPtr = if (iv.isNotEmpty()) pinnedIv.addressOf(0) else null
|
||||
val dataPtr = if (data.isNotEmpty()) pinnedData.addressOf(0) else null
|
||||
|
||||
val status = CCCryptorCreateWithMode(
|
||||
op = kCCEncrypt,
|
||||
mode = kCCModeGCM,
|
||||
alg = kCCAlgorithmAES,
|
||||
padding = ccNoPadding,
|
||||
iv = ivPtr,
|
||||
key = keyPtr,
|
||||
keyLength = key.size.toULong(),
|
||||
tweak = null,
|
||||
tweakLength = 0UL,
|
||||
numRounds = 0,
|
||||
options = 0U,
|
||||
cryptorRef = cryptorRefVar.ptr
|
||||
)
|
||||
|
||||
if (status != kCCSuccess) {
|
||||
error("CCCryptorCreateWithMode failed with status: $status")
|
||||
}
|
||||
|
||||
val cryptorRef = cryptorRefVar.value ?: error("Cryptor reference was null")
|
||||
|
||||
try {
|
||||
val cipherTextBytes = ByteArray(data.size)
|
||||
cipherTextBytes.usePinned { pinnedCipher ->
|
||||
val cipherPtr = if (data.isNotEmpty()) pinnedCipher.addressOf(0) else null
|
||||
val cryptStatus = CCCryptorGCMEncrypt(
|
||||
cryptorRef = cryptorRef,
|
||||
dataIn = dataPtr,
|
||||
dataInLength = data.size.toULong(),
|
||||
dataOut = cipherPtr
|
||||
)
|
||||
if (cryptStatus != kCCSuccess) {
|
||||
error("CCCryptorGCMEncrypt failed with status: $cryptStatus")
|
||||
}
|
||||
}
|
||||
|
||||
val tagBytes = ByteArray(16)
|
||||
val tagLengthVar = alloc<kotlinx.cinterop.size_tVar>()
|
||||
tagLengthVar.value = 16UL
|
||||
|
||||
tagBytes.usePinned { pinnedTag ->
|
||||
val tagPtr = pinnedTag.addressOf(0)
|
||||
val finalStatus = CCCryptorGCMFinal(
|
||||
cryptorRef = cryptorRef,
|
||||
tag = tagPtr,
|
||||
tagLength = tagLengthVar.ptr
|
||||
)
|
||||
if (finalStatus != kCCSuccess) {
|
||||
error("CCCryptorGCMFinal failed with status: $finalStatus")
|
||||
}
|
||||
}
|
||||
|
||||
encryptedData = cipherTextBytes + tagBytes
|
||||
} finally {
|
||||
CCCryptorRelease(cryptorRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return encryptedData ?: ByteArray(0)
|
||||
}
|
||||
|
||||
val isEcb = mode.uppercase().contains("ECB")
|
||||
val isNoPadding = mode.uppercase().contains("NOPADDING")
|
||||
|
||||
val dataOutAvailable = data.size + 16 // AES block size
|
||||
val dataOut = ByteArray(dataOutAvailable)
|
||||
|
|
@ -142,10 +194,12 @@ internal fun pluginAesEncrypt(
|
|||
memScoped {
|
||||
val dataOutMoved = alloc<kotlinx.cinterop.size_tVar>()
|
||||
|
||||
val options = if (isEcb) {
|
||||
kCCOptionPKCS7Padding or kCCOptionECBMode
|
||||
} else {
|
||||
kCCOptionPKCS7Padding
|
||||
var options = 0U
|
||||
if (isEcb) {
|
||||
options = options or kCCOptionECBMode
|
||||
}
|
||||
if (!isNoPadding) {
|
||||
options = options or kCCOptionPKCS7Padding
|
||||
}
|
||||
|
||||
key.usePinned { pinnedKey ->
|
||||
|
|
@ -189,9 +243,87 @@ internal fun pluginAesDecrypt(
|
|||
): ByteArray {
|
||||
val isGcm = mode.uppercase().contains("GCM")
|
||||
if (isGcm) {
|
||||
throw UnsupportedOperationException("AES-GCM Decrypt is not yet implemented on iOS")
|
||||
require(data.size >= 16) { "Data too short for GCM decryption" }
|
||||
val ciphertextLen = data.size - 16
|
||||
val ciphertext = data.copyOfRange(0, ciphertextLen)
|
||||
val tagBytes = data.copyOfRange(ciphertextLen, data.size)
|
||||
|
||||
var decryptedData: ByteArray? = null
|
||||
|
||||
memScoped {
|
||||
val cryptorRefVar = alloc<com.nuvio.app.features.plugins.cryptointerop.CCCryptorRefVar>()
|
||||
|
||||
key.usePinned { pinnedKey ->
|
||||
iv.usePinned { pinnedIv ->
|
||||
ciphertext.usePinned { pinnedCipher ->
|
||||
tagBytes.usePinned { pinnedTag ->
|
||||
val keyPtr = if (key.isNotEmpty()) pinnedKey.addressOf(0) else null
|
||||
val ivPtr = if (iv.isNotEmpty()) pinnedIv.addressOf(0) else null
|
||||
val cipherPtr = if (ciphertext.isNotEmpty()) pinnedCipher.addressOf(0) else null
|
||||
val tagPtr = pinnedTag.addressOf(0)
|
||||
|
||||
val status = CCCryptorCreateWithMode(
|
||||
op = kCCDecrypt,
|
||||
mode = kCCModeGCM,
|
||||
alg = kCCAlgorithmAES,
|
||||
padding = ccNoPadding,
|
||||
iv = ivPtr,
|
||||
key = keyPtr,
|
||||
keyLength = key.size.toULong(),
|
||||
tweak = null,
|
||||
tweakLength = 0UL,
|
||||
numRounds = 0,
|
||||
options = 0U,
|
||||
cryptorRef = cryptorRefVar.ptr
|
||||
)
|
||||
|
||||
if (status != kCCSuccess) {
|
||||
error("CCCryptorCreateWithMode failed with status: $status")
|
||||
}
|
||||
|
||||
val cryptorRef = cryptorRefVar.value ?: error("Cryptor reference was null")
|
||||
|
||||
try {
|
||||
val plainTextBytes = ByteArray(ciphertextLen)
|
||||
plainTextBytes.usePinned { pinnedPlain ->
|
||||
val plainPtr = if (ciphertextLen > 0) pinnedPlain.addressOf(0) else null
|
||||
val cryptStatus = CCCryptorGCMDecrypt(
|
||||
cryptorRef = cryptorRef,
|
||||
dataIn = cipherPtr,
|
||||
dataInLength = ciphertextLen.toULong(),
|
||||
dataOut = plainPtr
|
||||
)
|
||||
if (cryptStatus != kCCSuccess) {
|
||||
error("CCCryptorGCMDecrypt failed with status: $cryptStatus")
|
||||
}
|
||||
}
|
||||
|
||||
val tagLengthVar = alloc<kotlinx.cinterop.size_tVar>()
|
||||
tagLengthVar.value = 16UL
|
||||
|
||||
val finalStatus = CCCryptorGCMFinal(
|
||||
cryptorRef = cryptorRef,
|
||||
tag = tagPtr,
|
||||
tagLength = tagLengthVar.ptr
|
||||
)
|
||||
if (finalStatus != kCCSuccess) {
|
||||
error("CCCryptorGCMFinal failed with status: $finalStatus (tag verification failed)")
|
||||
}
|
||||
|
||||
decryptedData = plainTextBytes
|
||||
} finally {
|
||||
CCCryptorRelease(cryptorRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return decryptedData ?: ByteArray(0)
|
||||
}
|
||||
|
||||
val isEcb = mode.uppercase().contains("ECB")
|
||||
val isNoPadding = mode.uppercase().contains("NOPADDING")
|
||||
|
||||
val dataOutAvailable = data.size + 16 // AES block size
|
||||
val dataOut = ByteArray(dataOutAvailable)
|
||||
|
|
@ -201,10 +333,12 @@ internal fun pluginAesDecrypt(
|
|||
memScoped {
|
||||
val dataOutMoved = alloc<kotlinx.cinterop.size_tVar>()
|
||||
|
||||
val options = if (isEcb) {
|
||||
kCCOptionPKCS7Padding or kCCOptionECBMode
|
||||
} else {
|
||||
kCCOptionPKCS7Padding
|
||||
var options = 0U
|
||||
if (isEcb) {
|
||||
options = options or kCCOptionECBMode
|
||||
}
|
||||
if (!isNoPadding) {
|
||||
options = options or kCCOptionPKCS7Padding
|
||||
}
|
||||
|
||||
key.usePinned { pinnedKey ->
|
||||
|
|
@ -326,7 +460,12 @@ internal fun pluginBase64Encode(data: String): String =
|
|||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
internal fun pluginBase64Decode(data: String): String {
|
||||
val normalized = data.trim().replace("\n", "").replace("\r", "").replace(" ", "")
|
||||
var normalized = data.trim().replace("\n", "").replace("\r", "").replace(" ", "")
|
||||
normalized = normalized.replace("-", "+").replace("_", "/")
|
||||
val padNeeded = (4 - (normalized.length % 4)) % 4
|
||||
if (padNeeded > 0) {
|
||||
normalized += "=".repeat(padNeeded)
|
||||
}
|
||||
val decoded = Base64.decode(normalized)
|
||||
return decoded.decodeToString()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,3 +112,77 @@ CCCryptorStatus CCCrypt(
|
|||
size_t dataOutAvailable,
|
||||
size_t *dataOutMoved
|
||||
);
|
||||
|
||||
typedef uint32_t CCMode;
|
||||
enum {
|
||||
kCCModeECB = 1,
|
||||
kCCModeCBC = 2,
|
||||
kCCModeCFB = 3,
|
||||
kCCModeOFB = 4,
|
||||
kCCModeCFB8 = 5,
|
||||
kCCModeCTR = 6,
|
||||
kCCModeF8 = 7,
|
||||
kCCModeLRW = 8,
|
||||
kCCModeOFB8 = 9,
|
||||
kCCModeXTS = 10,
|
||||
kCCModeRC4 = 11,
|
||||
kCCModeCFB128 = 12,
|
||||
kCCModeGCM = 13,
|
||||
kCCModeCCM = 14,
|
||||
};
|
||||
|
||||
typedef uint32_t CCPadding;
|
||||
enum {
|
||||
ccNoPadding = 0,
|
||||
ccPKCS7Padding = 1,
|
||||
};
|
||||
|
||||
typedef uint32_t CCModeOptions;
|
||||
|
||||
typedef struct _CCCryptor *CCCryptorRef;
|
||||
|
||||
CCCryptorStatus CCCryptorCreateWithMode(
|
||||
CCOperation op,
|
||||
CCMode mode,
|
||||
CCAlgorithm alg,
|
||||
CCPadding padding,
|
||||
const void *iv,
|
||||
const void *key,
|
||||
size_t keyLength,
|
||||
const void *tweak,
|
||||
size_t tweakLength,
|
||||
int numRounds,
|
||||
CCModeOptions options,
|
||||
CCCryptorRef *cryptorRef
|
||||
);
|
||||
|
||||
CCCryptorStatus CCCryptorGCMAddAAD(
|
||||
CCCryptorRef cryptorRef,
|
||||
const void *aData,
|
||||
size_t aDataLen
|
||||
);
|
||||
|
||||
CCCryptorStatus CCCryptorGCMEncrypt(
|
||||
CCCryptorRef cryptorRef,
|
||||
const void *dataIn,
|
||||
size_t dataInLength,
|
||||
void *dataOut
|
||||
);
|
||||
|
||||
CCCryptorStatus CCCryptorGCMDecrypt(
|
||||
CCCryptorRef cryptorRef,
|
||||
const void *dataIn,
|
||||
size_t dataInLength,
|
||||
void *dataOut
|
||||
);
|
||||
|
||||
CCCryptorStatus CCCryptorGCMFinal(
|
||||
CCCryptorRef cryptorRef,
|
||||
void *tag,
|
||||
size_t *tagLength
|
||||
);
|
||||
|
||||
CCCryptorStatus CCCryptorRelease(
|
||||
CCCryptorRef cryptorRef
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue