Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 167 additions & 48 deletions lib/src/appleMain/kotlin/com/icure/kryptom/crypto/IosAesService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@ package com.icure.kryptom.crypto
import com.icure.kryptom.crypto.AesService.Companion.IV_BYTE_LENGTH
import com.icure.kryptom.crypto.AesService.Companion.aesEncryptedSizeFor
import com.icure.kryptom.utils.PlatformMethodException
import kotlinx.cinterop.MemScope
import kotlinx.cinterop.Pinned
import kotlinx.cinterop.ULongVar
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.usePinned
import kotlinx.cinterop.value
import platform.CoreCrypto.CCCrypt
import platform.CoreCrypto.CCCryptorCreateWithMode
import platform.CoreCrypto.CCCryptorFinal
import platform.CoreCrypto.CCCryptorRefVar
import platform.CoreCrypto.CCCryptorRelease
import platform.CoreCrypto.CCCryptorUpdate
import platform.CoreCrypto.kCCAlgorithmAES
import platform.CoreCrypto.kCCDecrypt
import platform.CoreCrypto.kCCEncrypt
import platform.CoreCrypto.kCCKeySizeAES128
import platform.CoreCrypto.kCCKeySizeAES256
import platform.CoreCrypto.kCCModeCBC
import platform.CoreCrypto.kCCModeCTR
import platform.CoreCrypto.kCCOptionPKCS7Padding
import platform.CoreCrypto.kCCSuccess

Expand All @@ -36,83 +44,188 @@ object IosAesService : AesService {
)

override suspend fun encrypt(data: ByteArray, key: AesKey<*>, iv: ByteArray?): ByteArray {
require (key.algorithm == AesAlgorithm.CbcWithPkcs7Padding) {
"Unsupported aes algorithm: ${key.algorithm}"
}
if (iv != null) require(iv.size == IV_BYTE_LENGTH) {
"Initialization vector must be $IV_BYTE_LENGTH bytes long (got ${iv.size})."
}
val generatedIv = iv ?: IosStrongRandom.randomBytes(IV_BYTE_LENGTH)
val outBytes = generatedIv.copyOf(IV_BYTE_LENGTH + aesEncryptedSizeFor(data.size))
memScoped {
val dataOutMoved = alloc<ULongVar>()
val operationResult = data.usePinned { pinnedData ->
return memScoped {
data.usePinned { pinnedData ->
generatedIv.usePinned { pinnedIv ->
outBytes.usePinned { pinnedOut ->
// TODO if in future we need to support anything other than CBC we will need to use `CCCryptorCreateWithMode`
key.rawKey.usePinned { pinnedKey ->
CCCrypt(
val cryptor = alloc<CCCryptorRefVar>()
CCCryptorCreateWithMode(
kCCEncrypt,
getCcMode(key.algorithm),
kCCAlgorithmAES,
kCCOptionPKCS7Padding,
pinnedIv.addressOf(0),
pinnedKey.addressOf(0),
validateAndGetKeySize(key),
pinnedIv.addressOf(0),
pinnedData.addressOf(0),
data.size.toULong(),
pinnedOut.addressOf(IV_BYTE_LENGTH),
(outBytes.size - IV_BYTE_LENGTH).toULong(),
dataOutMoved.ptr
)
null,
0.toULong(),
0,
0.toUInt(),
cryptor.ptr
).also {
if (it != kCCSuccess) throw PlatformMethodException(
"CCCryptorCreateWithMode (encrypt) failed with error code: $it",
it
)
}
try {
val dataOutMoved = alloc<ULongVar>()
var totalMoved = 0
CCCryptorUpdate(
cryptor.value,
pinnedData.addressOf(0),
data.size.toULong(),
pinnedOut.addressOf(IV_BYTE_LENGTH),
(outBytes.size - IV_BYTE_LENGTH).toULong(),
dataOutMoved.ptr
).also {
if (it != kCCSuccess) throw PlatformMethodException(
"CCCryptorUpdate (encrypt) failed with error code: $it",
it
)
}
totalMoved += dataOutMoved.value.toInt()
CCCryptorFinal(
cryptor.value,
pinnedOut.addressOf(IV_BYTE_LENGTH + totalMoved),
(outBytes.size - IV_BYTE_LENGTH - totalMoved).toULong(),
dataOutMoved.ptr
).also {
if (it != kCCSuccess) throw PlatformMethodException(
"CCCryptorFinal (encrypt) failed with error code: $it",
it
)
}
totalMoved += dataOutMoved.value.toInt()
if (totalMoved != (outBytes.size - IV_BYTE_LENGTH)) throw PlatformMethodException(
"Expected ${outBytes.size - IV_BYTE_LENGTH} encrypted bytes but got $totalMoved",
null
)
outBytes
} finally {
CCCryptorRelease(cryptor.value)
}
}
}
}
}
if (operationResult != kCCSuccess) throw PlatformMethodException(
"Encryption failed with error code $operationResult",
operationResult
)
if (dataOutMoved.value != (outBytes.size - IV_BYTE_LENGTH).toULong()) throw PlatformMethodException(
"Expected ${outBytes.size - IV_BYTE_LENGTH} encrypted bytes but got ${dataOutMoved.value}",
null
)
}
return outBytes
}

override suspend fun decrypt(ivAndEncryptedData: ByteArray, key: AesKey<*>): ByteArray {
require (key.algorithm == AesAlgorithm.CbcWithPkcs7Padding) {
"Unsupported aes algorithm: ${key.algorithm}"
return memScoped {
ivAndEncryptedData.usePinned { pinnedIvAndEncryptedData ->
key.rawKey.usePinned { pinnedKey ->
doDecrypt(
algorithm = key.algorithm,
pinnedEncryptedData = pinnedIvAndEncryptedData,
encryptedDataOffset = IV_BYTE_LENGTH,
encryptedDataSize = ivAndEncryptedData.size - IV_BYTE_LENGTH,
pinnedIv = pinnedIvAndEncryptedData,
pinnedKey = pinnedKey,
keySize = validateAndGetKeySize(key)
)
}
}
}
val outBytes = ByteArray(ivAndEncryptedData.size - IV_BYTE_LENGTH)
}

override suspend fun decrypt(
encryptedData: ByteArray,
key: AesKey<*>,
iv: ByteArray
): ByteArray {
require(iv.size == IV_BYTE_LENGTH) { "IV must be 16 bytes long" }
return memScoped {
val dataOutMoved = alloc<ULongVar>()
val operationResult = outBytes.usePinned { pinnedOutBytes ->
ivAndEncryptedData.usePinned { pinnedIvAndEncryptedData ->
encryptedData.usePinned { pinnedEncryptedData ->
iv.usePinned { pinnedIv ->
key.rawKey.usePinned { pinnedKey ->
// TODO if in future we need to support anything other than CBC we will need to use `CCCryptorCreateWithMode`
CCCrypt(
kCCDecrypt,
kCCAlgorithmAES,
kCCOptionPKCS7Padding,
pinnedKey.addressOf(0),
validateAndGetKeySize(key),
pinnedIvAndEncryptedData.addressOf(0),
pinnedIvAndEncryptedData.addressOf(IV_BYTE_LENGTH),
(ivAndEncryptedData.size - IV_BYTE_LENGTH).toULong(),
pinnedOutBytes.addressOf(0),
outBytes.size.toULong(),
dataOutMoved.ptr
doDecrypt(
algorithm = key.algorithm,
pinnedEncryptedData = pinnedEncryptedData,
encryptedDataOffset = 0,
encryptedDataSize = encryptedData.size,
pinnedIv = pinnedIv,
pinnedKey = pinnedKey,
keySize = validateAndGetKeySize(key)
)
}
}
}
}
}

private fun MemScope.doDecrypt(
algorithm: AesAlgorithm,
pinnedEncryptedData: Pinned<ByteArray>,
encryptedDataOffset: Int,
encryptedDataSize: Int,
pinnedIv: Pinned<ByteArray>,
pinnedKey: Pinned<ByteArray>,
keySize: ULong,
): ByteArray {
val cryptor = alloc<CCCryptorRefVar>()
CCCryptorCreateWithMode(
kCCDecrypt,
getCcMode(algorithm),
kCCAlgorithmAES,
kCCOptionPKCS7Padding,
pinnedIv.addressOf(0),
pinnedKey.addressOf(0),
keySize,
null,
0.toULong(),
0,
0.toUInt(),
cryptor.ptr
).also {
// Refer to Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/CommonCrypto/CommonCryptoError.h
if (operationResult != kCCSuccess) throw PlatformMethodException(
"Decryption failed with error code: $operationResult",
operationResult
if (it != kCCSuccess) throw PlatformMethodException(
"CCCryptorCreateWithMode (decrypt) failed with error code: $it",
it
)
outBytes.copyOf(dataOutMoved.value.toInt())
}
return try {
val outBuffer = ByteArray(encryptedDataSize)
val dataOutMoved = alloc<ULongVar>()
var totalMoved = 0
outBuffer.usePinned { pinnedOut ->
CCCryptorUpdate(
cryptor.value,
pinnedEncryptedData.addressOf(encryptedDataOffset),
encryptedDataSize.toULong(),
pinnedOut.addressOf(0),
encryptedDataSize.toULong(),
dataOutMoved.ptr
).also {
if (it != kCCSuccess) throw PlatformMethodException(
"CCCryptorUpdate (decrypt) failed with error code: $it",
it
)
}
totalMoved += dataOutMoved.value.toInt()
CCCryptorFinal(
cryptor.value,
pinnedOut.addressOf(totalMoved),
(outBuffer.size - totalMoved).toULong(),
dataOutMoved.ptr
).also {
if (it != kCCSuccess) throw PlatformMethodException(
"CCCryptorFinal (decrypt) failed with error code: $it",
it
)
}
totalMoved += dataOutMoved.value.toInt()
}
outBuffer.copyOf(totalMoved)
} finally {
CCCryptorRelease(cryptor.value)
}
}

Expand All @@ -122,4 +235,10 @@ object IosAesService : AesService {
AesService.KeySize.Aes256.byteSize -> kCCKeySizeAES256.toULong()
else -> throw IllegalArgumentException("Invalid size for key: ${key.rawKey.size}")
}

private fun getCcMode(
algorithm: AesAlgorithm
): UInt = when (algorithm) {
AesAlgorithm.CbcWithPkcs7Padding -> kCCModeCBC
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,9 @@ interface AesService {
* @throws IllegalArgumentException if the key is invalid (for example if the size is not good for an aes key).
*/
suspend fun decrypt(ivAndEncryptedData: ByteArray, key: AesKey<*>): ByteArray

/**
* Version of decrypt where the encrypted data and iv are provided separately
*/
suspend fun decrypt(encryptedData: ByteArray, key: AesKey<*>, iv: ByteArray): ByteArray
}
33 changes: 26 additions & 7 deletions lib/src/jsMain/kotlin/com/icure/kryptom/crypto/JsAesService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.icure.kryptom.crypto

import com.icure.kryptom.crypto.AesService.Companion.IV_BYTE_LENGTH
import com.icure.kryptom.js.jsCrypto
import com.icure.kryptom.js.toArrayBuffer
import com.icure.kryptom.js.toByteArray
Expand Down Expand Up @@ -63,13 +64,31 @@ object JsAesService : AesService {

override suspend fun decrypt(ivAndEncryptedData: ByteArray, key: AesKey<*>): ByteArray {
val buffer = ivAndEncryptedData.toArrayBuffer()
val iv = buffer.slice(0, AesService.IV_BYTE_LENGTH)
val data = buffer.slice(AesService.IV_BYTE_LENGTH)
return jsCrypto.subtle.decrypt(encryptionParam(key.algorithm, iv), key.cryptoKey, data).await().toByteArray()
val iv = buffer.slice(0, IV_BYTE_LENGTH)
val encryptedData = buffer.slice(IV_BYTE_LENGTH)
return doDecrypt(encryptedData, key, iv)
}

private fun encryptionParam(algorithm: AesAlgorithm, iv: ArrayBuffer) = json(
"name" to subtleAlgorithmNameFor(algorithm),
"iv" to iv
)
override suspend fun decrypt(
encryptedData: ByteArray,
key: AesKey<*>,
iv: ByteArray
): ByteArray {
require(iv.size == IV_BYTE_LENGTH) { "IV must be 16 bytes long" }
return doDecrypt(encryptedData.toArrayBuffer(), key, iv.toArrayBuffer())
}

private suspend fun doDecrypt(
encryptedData: ArrayBuffer,
key: AesKey<*>,
iv: ArrayBuffer
): ByteArray {
return jsCrypto.subtle.decrypt(encryptionParam(key.algorithm, iv), key.cryptoKey, encryptedData).await().toByteArray()
}

private fun encryptionParam(algorithm: AesAlgorithm, iv: ArrayBuffer) =
json(
"name" to subtleAlgorithmNameFor(algorithm),
"iv" to iv
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ private class AesServiceAdapter(

override suspend fun decrypt(ivAndEncryptedData: ByteArray, key: AesKey<*>): ByteArray =
service.decrypt(ivAndEncryptedData, key.toExternal()).await()

override suspend fun decrypt(
encryptedData: ByteArray,
key: AesKey<*>,
iv: ByteArray
): ByteArray =
service.decrypt(iv + encryptedData, key.toExternal()).await()
}

private class XAesServiceAdapter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,20 @@ object JvmAesService : AesService {

override suspend fun decrypt(ivAndEncryptedData: ByteArray, key: AesKey<*>): ByteArray {
val iv = ivAndEncryptedData.sliceArray(0 until IV_BYTE_LENGTH)
val data = ivAndEncryptedData.sliceArray(IV_BYTE_LENGTH until ivAndEncryptedData.size)
return getCipher(key.algorithm).apply { init(Cipher.DECRYPT_MODE, key.key, IvParameterSpec(iv)) }.doFinal(data)
val encryptedData = ivAndEncryptedData.sliceArray(IV_BYTE_LENGTH until ivAndEncryptedData.size)
return decrypt(
encryptedData,
key,
iv
)
}

override suspend fun decrypt(
encryptedData: ByteArray,
key: AesKey<*>,
iv: ByteArray
): ByteArray {
require(iv.size == IV_BYTE_LENGTH) { "IV must be 16 bytes long" }
return getCipher(key.algorithm).apply { init(Cipher.DECRYPT_MODE, key.key, IvParameterSpec(iv)) }.doFinal(encryptedData)
}
}
Loading