Kotlin Native (iOS), using CValuesRef with CCCrypt
Asked Answered
D

1

7

I am working on an AES256 encryption algorithm in a Kotlin Multiplatform project targeting iOS.

I checked some of the existing libraries that implement this in pure Kotlin (such as krypto), but none of them fit the requirements that I have from the rest of the code (which already has implementations in JVM and JS, so cannot be changed).

Coming from an iOS background, I decided to use CommonCrypto. I adapted and ported the code from here but I am stuck on how to pass a ULong by reference to the CCCrypt function and later retrieve its value.

I read very carefully the Kotlin docs on the C Interop and the Objective-C Interop, but I couldn't find any example that would explain how to deal with my case.

In particular, my issue is with the numBytesEncrypted variable (see code below). I need to pass that by reference to the CCCrypt function and then read its value to instantiate the resulting NSData with the correct length. In Objective-C/Swift, I would prefix the variable with & when calling the function.

However, Kotlin doesn't support the & operator. If I understood correctly from the docs, the replacement in Native is CValueRef (docs), so I used the cValue shorthand to get a ref of the correct type (which should be size_t, aka. ULong).

I tried to instantiate the CValueRef in two ways, both seem to be working as far as type-checking goes:

val numBytesEncrypted = cValue<ULongVar>()
// or
val numBytesEncrypted = cValue<ULongVarOf<size_t>>()

And then I used this code to get the value after the function has been executed:

numBytesEncrypted.getPointer(MemScope()).pointed.value // <- this always returns 0.

I tested the rest of the algorithm and it works perfectly (the encrypted value is the expected one and decryption also works), but without knowing the length of the encrypted/decrypted data, I cannot instantiate the NSData (and the resulting ByteArray) correctly.

Here's the full function code, including an extension I wrote (based on code found here) to convert NSData to ByteArray:

private fun process(operation: CCOperation, key: ByteArray, initVector: ByteArray, data: ByteArray): ByteArray = memScoped {
    bzero(key.refTo(0), key.size.convert())

    val dataLength = data.size

    val buffSize = (dataLength + kCCBlockSizeAES128.toInt()).convert<size_t>()
    val buff = platform.posix.malloc(buffSize)

    val numBytesEncrypted = cValue<ULongVarOf<size_t>>() // <- or the other way above

    val status = CCCrypt(
        operation,
        kCCAlgorithmAES128,
        kCCOptionPKCS7Padding,
        key.refTo(0), kCCKeySizeAES256.convert(),
        initVector.refTo(0),
        data.refTo(0), data.size.convert(),
        buff, buffSize,
        numBytesEncrypted
    )

    if (status == kCCSuccess) {
        return NSData.dataWithBytesNoCopy(
                buff, 
                numBytesEncrypted.getPointer(MemScope()).pointed.value // <- this always returns the original value of the variable or 0.
        )
        .toByteArray()
    }

    free(buff)
    return byteArrayOfUnsigned(0)
}

fun NSData.toByteArray(): ByteArray = ByteArray([email protected]()).apply {
    usePinned {
        memcpy(it.addressOf(0), [email protected], [email protected])
    }
}

Also, here's the unit test that I'm using to verify the code:

@Test
fun testAes256() {
    val initVector = ByteArray(16) { _ -> 0x00 }
    val key = ByteArray(32) { _ -> 0x00 }
    val data = ByteArray(1) { _ -> 0x01 }

    val expectedEncrypted = byteArrayOfUnsigned(0xFE, 0x2D, 0xE0, 0xEE, 0xF3, 0x2A, 0x05, 0x10, 0xDC, 0x31, 0x2E, 0xD7, 0x7D, 0x12, 0x93, 0xEB)

    val encrypted = process(kCCEncrypt, key, initVector, data)
    val decrypted = process(kCCDecrypt, key, initVector, encrypted)

    assertArraysEquals(expectedEncrypted, encrypted)
    assertArraysEquals(data, decrypted)
}

private fun assertArraysEquals(expected: ByteArray, actual: ByteArray) {
    if (expected.size != actual.size) asserter.fail("size is different. Expected: ${expected.size}, actual: ${actual.size}")
    (expected.indices).forEach { pos ->
        if (expected[pos] != actual[pos]) asserter.fail("entry at pos $pos different \n expected: ${debug(expected)} \n actual  : ${debug(actual)}")
    }
}

Thank you in advance!

Domination answered 15/7, 2021 at 15:52 Comment(0)
M
0

Change

val numBytesEncrypted = cValue<ULongVarOf<size_t>>()

to

val numBytes = cValue<ULongVar>().ptr

And you may use it in your code

 NSData.dataWithBytesNoCopy(
                buff,
                numBytes.pointed.value
            ).toByteArray()

In the method signature described that dataOutMoved is kotlinx.cinterop.CValuesRef type.

I found that getPointer always alloc memory.

public abstract class CValues<T : CVariable> : CValuesRef<T>() {

    public override fun getPointer(scope: AutofreeScope): CPointer<T> {
        return place(interpretCPointer(scope.alloc(size, align).rawPtr)!!)
    }
val numBytes = cValue<ULongVar>()
println("${numBytes.ptr.rawValue} ${numBytes.ptr.rawValue} ${numBytes.ptr.rawValue}")
0x7fa4a593bc18 0x7fa4a593ae28 0x7fa4a593b5d8


val numBytes = cValue<ULongVar>().ptr
println("${numBytes.rawValue} ${numBytes.rawValue} ${numBytes.rawValue}")
0x7fd5a287bfd8 0x7fd5a287bfd8 0x7fd5a287bfd8
Moorman answered 4/8, 2021 at 10:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.