From 73c97bfeb708365f020e72d1cae2e810132942dd Mon Sep 17 00:00:00 2001 From: paregi12 Date: Thu, 21 May 2026 15:56:42 +0530 Subject: [PATCH] feat(crypto): implement AES-GCM for iOS and enhance PBKDF2/Base64 support --- .../features/plugins/PluginCrypto.android.kt | 63 +++++- .../app/features/plugins/PluginCrypto.ios.kt | 213 +++++++++++++++--- .../cinterop/commoncrypto_shim.h | 74 ++++++ 3 files changed, 304 insertions(+), 46 deletions(-) diff --git a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.android.kt b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.android.kt index b1d4fedc5..ca9d08b7f 100644 --- a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.android.kt +++ b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.android.kt @@ -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() } diff --git a/composeApp/src/iosFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.ios.kt b/composeApp/src/iosFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.ios.kt index d61a00e10..cfb647651 100644 --- a/composeApp/src/iosFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.ios.kt +++ b/composeApp/src/iosFull/kotlin/com/nuvio/app/features/plugins/PluginCrypto.ios.kt @@ -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() + + 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() + 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() - 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() + + 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() + 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() - 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() } diff --git a/composeApp/src/nativeInterop/cinterop/commoncrypto_shim.h b/composeApp/src/nativeInterop/cinterop/commoncrypto_shim.h index b2620dc11..255a0fd71 100644 --- a/composeApp/src/nativeInterop/cinterop/commoncrypto_shim.h +++ b/composeApp/src/nativeInterop/cinterop/commoncrypto_shim.h @@ -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 +); +