feat(crypto): implement AES-GCM for iOS and enhance PBKDF2/Base64 support

This commit is contained in:
paregi12 2026-05-21 15:56:42 +05:30
parent 127ec3cf4a
commit 73c97bfeb7
3 changed files with 304 additions and 46 deletions

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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
);