diff --git a/cryptography-core/api/cryptography-core.api b/cryptography-core/api/cryptography-core.api index 5ec6874f..9f477a1f 100644 --- a/cryptography-core/api/cryptography-core.api +++ b/cryptography-core/api/cryptography-core.api @@ -114,6 +114,8 @@ public final class dev/whyoleg/cryptography/algorithms/AES$CMAC$Companion : dev/ } public abstract interface class dev/whyoleg/cryptography/algorithms/AES$CMAC$Key : dev/whyoleg/cryptography/algorithms/AES$Key { + public abstract fun cipherWithIv (Z)Ldev/whyoleg/cryptography/operations/AesCmacWithIvCipher; + public static synthetic fun cipherWithIv$default (Ldev/whyoleg/cryptography/algorithms/AES$CMAC$Key;ZILjava/lang/Object;)Ldev/whyoleg/cryptography/operations/AesCmacWithIvCipher; public abstract fun signatureGenerator ()Ldev/whyoleg/cryptography/operations/SignatureGenerator; public abstract fun signatureVerifier ()Ldev/whyoleg/cryptography/operations/SignatureVerifier; } @@ -741,6 +743,17 @@ public abstract interface class dev/whyoleg/cryptography/materials/key/KeyGenera public abstract fun generateKeyBlocking ()Ldev/whyoleg/cryptography/materials/key/Key; } +public abstract interface class dev/whyoleg/cryptography/operations/AesCmacWithIvCipher : dev/whyoleg/cryptography/operations/AesCmacWithIvEncryptor { +} + +public abstract interface class dev/whyoleg/cryptography/operations/AesCmacWithIvEncryptor { + public fun encryptWithIv ([B[BLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun encryptWithIvBlocking ([B[B)[B + public abstract fun initialize ()V + public fun process ([B[BLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun processBlocking ([B[B)[B +} + public abstract interface class dev/whyoleg/cryptography/operations/AuthenticatedCipher : dev/whyoleg/cryptography/operations/AuthenticatedDecryptor, dev/whyoleg/cryptography/operations/AuthenticatedEncryptor, dev/whyoleg/cryptography/operations/Cipher { } diff --git a/cryptography-core/api/cryptography-core.klib.api b/cryptography-core/api/cryptography-core.klib.api index b853e00c..c7f8dc7a 100644 --- a/cryptography-core/api/cryptography-core.klib.api +++ b/cryptography-core/api/cryptography-core.klib.api @@ -35,6 +35,7 @@ abstract interface <#A: dev.whyoleg.cryptography.algorithms/AES.Key> dev.whyoleg open fun (): dev.whyoleg.cryptography/CryptographyAlgorithmId // dev.whyoleg.cryptography.algorithms/AES.CMAC.id.|(){}[0] abstract interface Key : dev.whyoleg.cryptography.algorithms/AES.Key { // dev.whyoleg.cryptography.algorithms/AES.CMAC.Key|null[0] + abstract fun cipherWithIv(kotlin/Boolean = ...): dev.whyoleg.cryptography.operations/AesCmacWithIvCipher // dev.whyoleg.cryptography.algorithms/AES.CMAC.Key.cipherWithIv|cipherWithIv(kotlin.Boolean){}[0] abstract fun signatureGenerator(): dev.whyoleg.cryptography.operations/SignatureGenerator // dev.whyoleg.cryptography.algorithms/AES.CMAC.Key.signatureGenerator|signatureGenerator(){}[0] abstract fun signatureVerifier(): dev.whyoleg.cryptography.operations/SignatureVerifier // dev.whyoleg.cryptography.algorithms/AES.CMAC.Key.signatureVerifier|signatureVerifier(){}[0] } @@ -606,6 +607,16 @@ abstract interface dev.whyoleg.cryptography.materials.key/KeyFormat { // dev.why abstract fun toString(): kotlin/String // dev.whyoleg.cryptography.materials.key/KeyFormat.toString|toString(){}[0] } +abstract interface dev.whyoleg.cryptography.operations/AesCmacWithIvCipher : dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor // dev.whyoleg.cryptography.operations/AesCmacWithIvCipher|null[0] + +abstract interface dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor { // dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor|null[0] + abstract fun encryptWithIvBlocking(kotlin/ByteArray, kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor.encryptWithIvBlocking|encryptWithIvBlocking(kotlin.ByteArray;kotlin.ByteArray){}[0] + abstract fun initialize() // dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor.initialize|initialize(){}[0] + abstract fun processBlocking(kotlin/ByteArray, kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor.processBlocking|processBlocking(kotlin.ByteArray;kotlin.ByteArray){}[0] + open suspend fun encryptWithIv(kotlin/ByteArray, kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor.encryptWithIv|encryptWithIv(kotlin.ByteArray;kotlin.ByteArray){}[0] + open suspend fun process(kotlin/ByteArray, kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/AesCmacWithIvEncryptor.process|process(kotlin.ByteArray;kotlin.ByteArray){}[0] +} + abstract interface dev.whyoleg.cryptography.operations/AuthenticatedCipher : dev.whyoleg.cryptography.operations/AuthenticatedDecryptor, dev.whyoleg.cryptography.operations/AuthenticatedEncryptor, dev.whyoleg.cryptography.operations/Cipher // dev.whyoleg.cryptography.operations/AuthenticatedCipher|null[0] abstract interface dev.whyoleg.cryptography.operations/AuthenticatedDecryptor : dev.whyoleg.cryptography.operations/Decryptor { // dev.whyoleg.cryptography.operations/AuthenticatedDecryptor|null[0] diff --git a/cryptography-core/src/commonMain/kotlin/algorithms/AES.kt b/cryptography-core/src/commonMain/kotlin/algorithms/AES.kt index b092ed25..6f3be337 100644 --- a/cryptography-core/src/commonMain/kotlin/algorithms/AES.kt +++ b/cryptography-core/src/commonMain/kotlin/algorithms/AES.kt @@ -70,6 +70,7 @@ public interface AES : CryptographyAlgorithm { public interface Key : AES.Key { public fun signatureGenerator(): SignatureGenerator public fun signatureVerifier(): SignatureVerifier + public fun cipherWithIv(padding: Boolean = true): AesCmacWithIvCipher } } diff --git a/cryptography-core/src/commonMain/kotlin/operations/AesCmacWithIvCipher.kt b/cryptography-core/src/commonMain/kotlin/operations/AesCmacWithIvCipher.kt new file mode 100644 index 00000000..5a9321d8 --- /dev/null +++ b/cryptography-core/src/commonMain/kotlin/operations/AesCmacWithIvCipher.kt @@ -0,0 +1,23 @@ +package dev.whyoleg.cryptography.operations + +import dev.whyoleg.cryptography.* + +@SubclassOptInRequired(CryptographyProviderApi::class) +public interface AesCmacWithIvCipher : AesCmacWithIvEncryptor + +@SubclassOptInRequired(CryptographyProviderApi::class) +public interface AesCmacWithIvEncryptor { + public fun initialize() + + @DelicateCryptographyApi + public suspend fun process(input: ByteArray, iv: ByteArray): ByteArray = processBlocking(input, iv) + + @DelicateCryptographyApi + public fun processBlocking(input: ByteArray, iv: ByteArray): ByteArray + + @DelicateCryptographyApi + public suspend fun encryptWithIv(iv: ByteArray, plaintext: ByteArray): ByteArray = encryptWithIvBlocking(iv, plaintext) + + @DelicateCryptographyApi + public fun encryptWithIvBlocking(iv: ByteArray, plaintext: ByteArray): ByteArray +} \ No newline at end of file diff --git a/cryptography-providers/base/api/cryptography-provider-base.api b/cryptography-providers/base/api/cryptography-provider-base.api index c1c6f324..58b072db 100644 --- a/cryptography-providers/base/api/cryptography-provider-base.api +++ b/cryptography-providers/base/api/cryptography-provider-base.api @@ -141,6 +141,8 @@ public abstract interface class dev/whyoleg/cryptography/providers/base/operatio } public abstract interface class dev/whyoleg/cryptography/providers/base/operations/CipherFunction { + public fun initialize ()V + public fun process ([B[B)[B public abstract fun transform ([BII)[B public static synthetic fun transform$default (Ldev/whyoleg/cryptography/providers/base/operations/CipherFunction;[BIIILjava/lang/Object;)[B public abstract fun transformedSink (Lkotlinx/io/RawSink;)Lkotlinx/io/RawSink; diff --git a/cryptography-providers/base/api/cryptography-provider-base.klib.api b/cryptography-providers/base/api/cryptography-provider-base.klib.api index 900d5746..7cef4259 100644 --- a/cryptography-providers/base/api/cryptography-provider-base.klib.api +++ b/cryptography-providers/base/api/cryptography-provider-base.klib.api @@ -84,6 +84,8 @@ abstract interface dev.whyoleg.cryptography.providers.base.operations/CipherFunc abstract fun transform(kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...): kotlin/ByteArray // dev.whyoleg.cryptography.providers.base.operations/CipherFunction.transform|transform(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] abstract fun transformedSink(kotlinx.io/RawSink): kotlinx.io/RawSink // dev.whyoleg.cryptography.providers.base.operations/CipherFunction.transformedSink|transformedSink(kotlinx.io.RawSink){}[0] abstract fun transformedSource(kotlinx.io/RawSource): kotlinx.io/RawSource // dev.whyoleg.cryptography.providers.base.operations/CipherFunction.transformedSource|transformedSource(kotlinx.io.RawSource){}[0] + open fun initialize() // dev.whyoleg.cryptography.providers.base.operations/CipherFunction.initialize|initialize(){}[0] + open fun process(kotlin/ByteArray, kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.providers.base.operations/CipherFunction.process|process(kotlin.ByteArray;kotlin.ByteArray){}[0] } abstract class dev.whyoleg.cryptography.providers.base.algorithms/BaseHkdf : dev.whyoleg.cryptography.algorithms/HKDF { // dev.whyoleg.cryptography.providers.base.algorithms/BaseHkdf|null[0] diff --git a/cryptography-providers/base/src/commonMain/kotlin/operations/BaseCipherFunction.kt b/cryptography-providers/base/src/commonMain/kotlin/operations/BaseCipherFunction.kt index 7b635eac..6927be34 100644 --- a/cryptography-providers/base/src/commonMain/kotlin/operations/BaseCipherFunction.kt +++ b/cryptography-providers/base/src/commonMain/kotlin/operations/BaseCipherFunction.kt @@ -14,6 +14,8 @@ public interface CipherFunction { public fun transform(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size): ByteArray public fun transformedSource(source: RawSource): RawSource public fun transformedSink(sink: RawSink): RawSink + public fun initialize() {} + public fun process(input: ByteArray, iv: ByteArray): ByteArray { return byteArrayOf() } } // TODO: test with different input/output sizes diff --git a/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkAesCmac.kt b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkAesCmac.kt index 058ddf7e..f1997c0f 100644 --- a/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkAesCmac.kt +++ b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkAesCmac.kt @@ -31,9 +31,11 @@ private class JdkAesCmacKey( ) : AES.CMAC.Key, JdkEncodableKey(key) { private val algorithm = "AESCMAC" private val signature = JdkMacSignature(state, key, algorithm) + private val ivCipher = JdkAesCmacWithIvCipher(state = state, key = key, ivSize = 16, "AES/CBC/NoPadding") override fun signatureGenerator(): SignatureGenerator = signature override fun signatureVerifier(): SignatureVerifier = signature + override fun cipherWithIv(padding: Boolean): AesCmacWithIvCipher = ivCipher override fun encodeToByteArrayBlocking(format: AES.Key.Format): ByteArray = when (format) { AES.Key.Format.JWK -> error("$format is not supported") diff --git a/cryptography-providers/jdk/src/jvmMain/kotlin/operations/JdkAesCmacWithIvFunction.kt b/cryptography-providers/jdk/src/jvmMain/kotlin/operations/JdkAesCmacWithIvFunction.kt new file mode 100644 index 00000000..c28f5f76 --- /dev/null +++ b/cryptography-providers/jdk/src/jvmMain/kotlin/operations/JdkAesCmacWithIvFunction.kt @@ -0,0 +1,293 @@ +package dev.whyoleg.cryptography.providers.jdk.operations + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.base.checkBounds +import dev.whyoleg.cryptography.providers.base.operations.BaseCipherFunction +import dev.whyoleg.cryptography.providers.jdk.* +import javax.crypto.spec.IvParameterSpec + +internal class JdkAesCmacWithIvCipher( + val state: JdkCryptographyState, + val key: JSecretKey, + val ivSize: Int, + val algorithm: String, +) : AesCmacWithIvCipher { + lateinit var cipherFunction: JdkAesCmacWithIvFunction + + @OptIn(ExperimentalStdlibApi::class) + override fun initialize() { + cipherFunction = JdkAesCmacWithIvFunction(algorithm = algorithm, key = key, state = state) + cipherFunction.initialize() + } + + @DelicateCryptographyApi + override fun processBlocking(input: ByteArray, iv: ByteArray): ByteArray = cipherFunction.process(input, iv) + + @DelicateCryptographyApi + override fun encryptWithIvBlocking(iv: ByteArray, plaintext: ByteArray): ByteArray = cipherFunction.transform(plaintext) +} + +@OptIn(CryptographyProviderApi::class) +@CryptographyProviderApi +internal open class JdkAesCmacWithIvFunction( + state: JdkCryptographyState, + private val key: JSecretKey, + algorithm: String, +) : BaseCipherFunction() { + + private lateinit var poly: ByteArray + internal val iv: ByteArray = ByteArray(16) // AES block size + private var k1 = ByteArray(16) + private var k2 = ByteArray(16) + private val buf = ByteArray(16) + private var bufOff = 0 + private val mac = ByteArray(16) + private val cipherPool = state.cipher(algorithm) + + @CryptographyProviderApi + override val blockSize: Int = iv.size + + @OptIn(ExperimentalStdlibApi::class) + @CryptographyProviderApi + override fun initialize() { + require(iv.size == blockSize) { "IV size must match block size" } + poly = lookupPoly(blockSize) + val zeroes = ByteArray(blockSize) + val l = ByteArray(blockSize) + transformToByteArray(zeroes, 0, blockSize).copyInto(l, 0, 0, blockSize) + k1 = doubleLu(l) + k2 = doubleLu(k1) + reset() + } + + @CryptographyProviderApi + override fun maxOutputSize(inputSize: Int): Int { + val blockSize = blockSize + if (blockSize == 1) return inputSize + return inputSize + blockSize + } + + @CryptographyProviderApi + override fun close() { + } + + @OptIn(ExperimentalStdlibApi::class) + @CryptographyProviderApi + override fun transform(source: ByteArray, startIndex: Int, endIndex: Int): ByteArray { + update(source, startIndex, endIndex - startIndex) + source.copyInto(buf, destinationOffset = 0, startIndex = startIndex, endIndex = startIndex + bufOff) + + val lu: ByteArray + if (bufOff == blockSize) { + lu = k1 + } else { + addISO7816d4Padding(buf, bufOff) + lu = k2 + } + + // XOR buffer with LU + for (i in mac.indices) { + buf[i] = (buf[i].toInt() xor lu[i].toInt()).toByte() + } + + // XOR buffer with IV + for (i in 0 until blockSize) { + buf[i] = (buf[i].toInt() xor iv[i].toInt()).toByte() + } + + // Process block + transformToByteArray(buf, 0, blockSize).copyInto(mac, 0, 0, blockSize) + + // Update IV + mac.copyInto(iv, 0, 0, iv.size) + reset() + + return mac + } + + @CryptographyProviderApi + override fun transformIntoByteArray( + source: ByteArray, + destination: ByteArray, + destinationOffset: Int, + startIndex: Int, + endIndex: Int, + ): Int { + checkBounds(source.size, startIndex, endIndex) + checkBounds(destination.size, destinationOffset, destinationOffset + maxOutputSize(endIndex - startIndex)) + + val inputLength = endIndex - startIndex + require(inputLength % blockSize == 0) { "Input length must be a multiple of the block size" } + + // Use the underlying cipher to perform the actual transformation + return cipherPool.use { cipherInstance -> + cipherInstance.init(JCipher.ENCRYPT_MODE, key, IvParameterSpec(iv)) + cipherInstance.update( + source, // input + startIndex, // inputOffset + inputLength, // inputLen + destination, // output + destinationOffset // outputOffset + ) + } + } + + @CryptographyProviderApi + override fun finalizeIntoByteArray(destination: ByteArray, destinationOffset: Int): Int { + checkBounds(destination.size, destinationOffset, destinationOffset + maxOutputSize(0)) + + // Use the underlying cipher to perform the final transformation + return cipherPool.use { cipherInstance -> + cipherInstance.init(JCipher.ENCRYPT_MODE, key, IvParameterSpec(iv)) + cipherInstance.doFinal( + destination, // output + destinationOffset // outputOffset + ) + } + } + + @OptIn(ExperimentalStdlibApi::class) + private fun update(input: ByteArray, inputOffset: Int, length: Int) { + var inOff = inputOffset + var len = length + require(len >= 0) { "Can't have a negative input length!" } + + val blockSize = buf.size + val gapLen = blockSize - bufOff + + if (len > gapLen) { + input.copyInto(buf, bufOff, inOff, inOff + gapLen) + + super.transform(buf, 0, blockSize).copyInto(mac, 0, 0, blockSize) + + bufOff = 0 + len -= gapLen + inOff += gapLen + + while (len > blockSize) { + super.transform(input, inOff, inOff + blockSize).copyInto(mac, 0, 0, blockSize) + + len -= blockSize + inOff += blockSize + } + } + + input.copyInto(buf, bufOff, inOff, inOff + len) + bufOff += len + } + + @OptIn(ExperimentalStdlibApi::class) + @CryptographyProviderApi + override fun process(input: ByteArray, iv: ByteArray): ByteArray { + update(input, 0, input.size) + + val lu: ByteArray + if (bufOff == blockSize) { + lu = k1 + } else { + addISO7816d4Padding(buf, bufOff) + lu = k2 + } + + // XOR buffer with LU + for (i in mac.indices) { + buf[i] = (buf[i].toInt() xor lu[i].toInt()).toByte() + } + + // XOR buffer with IV + for (i in 0 until blockSize) { + buf[i] = (buf[i].toInt() xor iv[i].toInt()).toByte() + } + + // Process block + transformToByteArray(buf, 0, blockSize).copyInto(mac, 0, 0, blockSize) + + // Update IV + mac.copyInto(iv, 0, 0, iv.size) + reset() + + return mac + } + + fun reset() {/* + * clean the buffer. + */ + for (i in buf.indices) { + buf[i] = 0 + } + + bufOff = 0 + + // reset the underlying cipher + resetCipher(iv, iv.copyOf(iv.size)) + } + + fun resetCipher(iv: ByteArray, originalIv: ByteArray) { + // Reset the IV to the original value + originalIv.copyInto(iv, 0, 0, originalIv.size) + // Clear any intermediate buffers (if applicable) + iv.fill(0) + } + + private fun addISO7816d4Padding(buffer: ByteArray, offset: Int) { + buffer[offset] = 0x80.toByte() // Add the 0x80 byte + for (i in offset + 1 until buffer.size) { + buffer[i] = 0x00 // Fill the rest with 0x00 + } + } + + private fun doubleLu(input: ByteArray): ByteArray { + val ret = ByteArray(input.size) + val carry = shiftLeft(input, ret) + + /* + * NOTE: This construction is an attempt at a constant-time implementation. + */ + val mask = (-carry) and 0xff + ret[input.size - 3] = (ret[input.size - 3].toInt() xor (poly[1].toInt() and mask)).toByte() + ret[input.size - 2] = (ret[input.size - 2].toInt() xor (poly[2].toInt() and mask)).toByte() + ret[input.size - 1] = (ret[input.size - 1].toInt() xor (poly[3].toInt() and mask)).toByte() + return ret + } + + private fun shiftLeft(block: ByteArray, output: ByteArray): Int { + var i = block.size + var bit = 0 + while (--i >= 0) { + val b = block[i].toInt() and 0xff + output[i] = ((b shl 1) or bit).toByte() + bit = (b ushr 7) and 1 + } + return bit + } + + private fun lookupPoly(blockSizeLength: Int): ByteArray { + val xor = when (blockSizeLength * 8) { + 64 -> 0x1B + 128 -> 0x87 + 160 -> 0x2D + 192 -> 0x87 + 224 -> 0x309 + 256 -> 0x425 + 320 -> 0x1B + 384 -> 0x100D + 448 -> 0x851 + 512 -> 0x125 + 768 -> 0xA0011 + 1024 -> 0x80043 + 2048 -> 0x86001 + else -> throw IllegalArgumentException("Unknown block size for CMAC: " + (blockSizeLength * 8)) + } + return intToBigEndian(xor) + } + + private fun intToBigEndian(value: Int): ByteArray { + return byteArrayOf( + (value shr 24).toByte(), + (value shr 16).toByte(), + (value shr 8).toByte(), + value.toByte() + ) + } +} \ No newline at end of file diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3AesCmac.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3AesCmac.kt index c2896896..1115042c 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3AesCmac.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3AesCmac.kt @@ -10,6 +10,8 @@ import dev.whyoleg.cryptography.operations.* import dev.whyoleg.cryptography.providers.base.* import dev.whyoleg.cryptography.providers.openssl3.internal.* import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* +import dev.whyoleg.cryptography.operations.AesCmacWithIvCipher +import dev.whyoleg.cryptography.providers.openssl3.operations.AesCmacWithIvCipherFunction import kotlinx.cinterop.* import kotlin.experimental.* import kotlin.native.ref.* @@ -34,6 +36,7 @@ internal object Openssl3AesCmac : AES.CMAC, Openssl3Aes() { private val signature = AesCmacSignature(algorithm = algorithm, key = key) override fun signatureGenerator(): SignatureGenerator = signature override fun signatureVerifier(): SignatureVerifier = signature + override fun cipherWithIv(padding: Boolean): AesCmacWithIvCipher = AesCmacWithIvCipher(algorithm = algorithm, key = key) } } @@ -139,4 +142,40 @@ private class AesCmacSignature( } } } +} + +@OptIn(ExperimentalStdlibApi::class) +@ExperimentalNativeApi +private class AesCmacWithIvCipher( + private val algorithm: String, + private val key: ByteArray, +) : AesCmacWithIvCipher { + + private lateinit var cipherFunction: AesCmacWithIvCipherFunction + + override fun initialize() { + val cipher = when (algorithm) { + "AES-128-CBC" -> EVP_CIPHER_fetch(null, "AES-128-CBC", null) + "AES-192-CBC" -> EVP_CIPHER_fetch(null, "AES-192-CBC", null) + "AES-256-CBC" -> EVP_CIPHER_fetch(null, "AES-256-CBC", null) + else -> error("Unsupported algorithm: $algorithm") + } + + cipherFunction = AesCmacWithIvCipherFunction( + cipher = cipher, + key = key, + iv = ByteArray(16), // AES block size + ivStartIndex = 0, + encrypt = true + ) as AesCmacWithIvCipherFunction + cipherFunction.initialize() + } + + override fun processBlocking(input: ByteArray, iv: ByteArray): ByteArray { + return cipherFunction.process(input, iv) + } + + override fun encryptWithIvBlocking(iv: ByteArray, plaintext: ByteArray): ByteArray { + return cipherFunction.transform(plaintext) + } } \ No newline at end of file diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3AesCmacWithIvFunction.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3AesCmacWithIvFunction.kt new file mode 100644 index 00000000..80ad9c75 --- /dev/null +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3AesCmacWithIvFunction.kt @@ -0,0 +1,366 @@ +package dev.whyoleg.cryptography.providers.openssl3.operations + +import dev.whyoleg.cryptography.providers.base.checkBounds +import dev.whyoleg.cryptography.providers.base.operations.BaseCipherFunction +import dev.whyoleg.cryptography.providers.base.operations.CipherFunction +import dev.whyoleg.cryptography.providers.base.refToU +import dev.whyoleg.cryptography.providers.base.safeAddressOfU +import dev.whyoleg.cryptography.providers.openssl3.internal.Resource +import dev.whyoleg.cryptography.providers.openssl3.internal.SafeCloseAction +import dev.whyoleg.cryptography.providers.openssl3.internal.SafeCloseable +import dev.whyoleg.cryptography.providers.openssl3.internal.checkError +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CIPHER +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CIPHER_CTX +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CIPHER_CTX_ctrl +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CIPHER_CTX_free +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CIPHER_CTX_get_block_size +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CIPHER_CTX_get_iv_length +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CIPHER_CTX_new +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CTRL_INIT +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CipherFinal +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CipherInit_ex2 +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.EVP_CipherUpdate +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.IntVar +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.usePinned +import kotlinx.cinterop.value + +internal fun AES_CMAC_WITH_IV_CIPHER_CTX( + cipher: CPointer?, + key: ByteArray, + iv: ByteArray?, + ivStartIndex: Int, + encrypt: Boolean, + init: (CPointer?) -> Unit = {}, +): Resource?> { + val context = checkError(EVP_CIPHER_CTX_new()) + val resource = Resource?>(context, ::EVP_CIPHER_CTX_free) + try { + checkError( + EVP_CipherInit_ex2( + ctx = context, + cipher = cipher, + key = key.refToU(0), + iv = iv?.refToU(ivStartIndex), + params = null, + enc = if (encrypt) 1 else 0 + ) + ) + init(context) + } catch (cause: Throwable) { + resource.close() + throw cause + } + return resource +} + +internal fun AesCmacWithIvCipherFunction( + cipher: CPointer?, + key: ByteArray, + iv: ByteArray, + ivStartIndex: Int, + encrypt: Boolean, + init: (CPointer?) -> Unit = {}, +): CipherFunction { + return AesCmacWithIvCipherFunction(AES_CMAC_WITH_IV_CIPHER_CTX(cipher, key, iv, ivStartIndex, encrypt, init)) +} + +@OptIn(ExperimentalStdlibApi::class) +internal open class AesCmacWithIvCipherFunction( + protected val context: Resource?>, +) : BaseCipherFunction() { + + private lateinit var poly: ByteArray + internal val iv: ByteArray = context.access().let { ctx -> + val ivSize = EVP_CIPHER_CTX_get_iv_length(ctx) + ByteArray(ivSize).also { ivArray -> + checkError(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_INIT, 0, ivArray.refToU(0))) + } + } + private var k1 = ByteArray(16) + private var k2 = ByteArray(16) + private val buf = ByteArray(16) + private var bufOff = 0 + private val mac = ByteArray(16) + + private val closeable = SafeCloseable(SafeCloseAction(context, AutoCloseable::close)) + override val blockSize: Int + get() = checkError(EVP_CIPHER_CTX_get_block_size(context.access())) + + override fun initialize() { + require(iv.size == blockSize) { "IV size must match block size" } + + poly = lookupPoly(blockSize) + + // Initialize zeroes array + val zeroes = ByteArray(blockSize) + + // Process zeroes to compute L + val l = ByteArray(blockSize) + transformToByteArray(zeroes, 0, blockSize).copyInto(l, 0, 0, blockSize) + + // Compute k1 and k2 + k1 = doubleLu(l) + k2 = doubleLu(k1) + + reset() + } + + override fun maxOutputSize(inputSize: Int): Int { + val blockSize = blockSize + if (blockSize == 1) return inputSize + return inputSize + blockSize + } + + override fun process(input: ByteArray, iv: ByteArray): ByteArray { + update(input, 0, input.size) + + val lu: ByteArray + if (bufOff == blockSize) { + lu = k1 + } else { + addISO7816d4Padding(buf, bufOff) + lu = k2 + } + + // XOR buffer with LU + for (i in mac.indices) { + buf[i] = (buf[i].toInt() xor lu[i].toInt()).toByte() + } + + // XOR buffer with IV + for (i in 0 until blockSize) { + buf[i] = (buf[i].toInt() xor iv[i].toInt()).toByte() + } + + // Process block + transformToByteArray(buf, 0, blockSize).copyInto(mac, 0, 0, blockSize) + + // Update IV + mac.copyInto(iv, 0, 0, iv.size) + reset() + + return mac + } + + override fun transform(source: ByteArray, startIndex: Int, endIndex: Int): ByteArray { + update(source, startIndex, endIndex - startIndex) + + // Copy source into buffer + source.copyInto(buf, destinationOffset = 0, startIndex = startIndex, endIndex = startIndex + bufOff) + + val lu: ByteArray + if (bufOff == blockSize) { + lu = k1 + } else { + addISO7816d4Padding(buf, bufOff) + lu = k2 + } + + // XOR buffer with LU + for (i in mac.indices) { + buf[i] = (buf[i].toInt() xor lu[i].toInt()).toByte() + } + + // XOR buffer with IV + for (i in 0 until blockSize) { + buf[i] = (buf[i].toInt() xor iv[i].toInt()).toByte() + } + + // Process block + transformToByteArray(buf, 0, blockSize).copyInto(mac, 0, 0, blockSize) + + // Update IV + mac.copyInto(iv, 0, 0, iv.size) + reset() + + return mac + } + + @OptIn(UnsafeNumber::class) + override fun transformIntoByteArray( + source: ByteArray, + destination: ByteArray, + destinationOffset: Int, + startIndex: Int, + endIndex: Int, + ): Int { + checkBounds(source.size, startIndex, endIndex) + checkBounds(destination.size, destinationOffset, destinationOffset + maxOutputSize(endIndex - startIndex)) + + val context = context.access() + val blockSize = blockSize // Ensure block size is retrieved from the cipher context + + return memScoped { + val dataOutMoved = alloc() + source.usePinned { sourcePinned -> + destination.usePinned { destinationPinned -> + val inputLength = endIndex - startIndex + require(inputLength % blockSize == 0) { "Input length must be a multiple of the block size" } + checkError( + EVP_CipherUpdate( + ctx = context, + out = destinationPinned.safeAddressOfU(destinationOffset), + outl = dataOutMoved.ptr, + `in` = sourcePinned.safeAddressOfU(startIndex), + inl = inputLength.convert(), + ) + ) + } + } + dataOutMoved.value.convert() + } + } + + override fun finalizeIntoByteArray(destination: ByteArray, destinationOffset: Int): Int { + checkBounds(destination.size, destinationOffset, destinationOffset + maxOutputSize(0)) + + val context = context.access() + + return memScoped { + val dataOutMoved = alloc() + destination.usePinned { destinationPinned -> + checkError( + EVP_CipherFinal( + ctx = context, + outm = destinationPinned.safeAddressOfU(destinationOffset), + outl = dataOutMoved.ptr + ) + ) + } + dataOutMoved.value.convert() + } + } + + override fun close() { + closeable.close() + } + + fun reset() {/* + * clean the buffer. + */ + for (i in buf.indices) { + buf[i] = 0 + } + + bufOff = 0 + + // reset the underlying cipher + resetCipher(context.access(), iv, iv.copyOf(iv.size)) + } + + fun resetCipher(context: CPointer?, iv: ByteArray, originalIv: ByteArray) { + // Reset the IV to the original value + originalIv.copyInto(iv, 0, 0, originalIv.size) + // Clear any intermediate buffers (if applicable) + iv.fill(0) + + // Reinitialize the cipher context with the original IV + checkError( + EVP_CipherInit_ex2( + ctx = context, + cipher = null, // Reuse the existing cipher + key = null, // Reuse the existing key + iv = iv.refToU(0), + params = null, + enc = -1 // Reuse the current encryption/decryption mode + ) + ) + } + + private fun update(input: ByteArray, inputOffset: Int, length: Int) { + var inOff = inputOffset + var len = length + require(len >= 0) { "Can't have a negative input length!" } + + val blockSize = buf.size + val gapLen = blockSize - bufOff + + if (len > gapLen) { + input.copyInto(buf, bufOff, inOff, inOff + gapLen) + + super.transform(buf, 0, blockSize).copyInto(mac, 0, 0, blockSize) + + bufOff = 0 + len -= gapLen + inOff += gapLen + + while (len > blockSize) { + super.transform(input, inOff, inOff + blockSize).copyInto(mac, 0, 0, blockSize) + + len -= blockSize + inOff += blockSize + } + } + + input.copyInto(buf, bufOff, inOff, inOff + len) + + bufOff += len + } + + private fun addISO7816d4Padding(buffer: ByteArray, offset: Int) { + buffer[offset] = 0x80.toByte() // Add the 0x80 byte + for (i in offset + 1 until buffer.size) { + buffer[i] = 0x00 // Fill the rest with 0x00 + } + } + + private fun doubleLu(input: ByteArray): ByteArray { + val ret = ByteArray(input.size) + val carry = shiftLeft(input, ret) + + /* + * NOTE: This construction is an attempt at a constant-time implementation. + */ + val mask = (-carry) and 0xff + ret[input.size - 3] = (ret[input.size - 3].toInt() xor (poly[1].toInt() and mask)).toByte() + ret[input.size - 2] = (ret[input.size - 2].toInt() xor (poly[2].toInt() and mask)).toByte() + ret[input.size - 1] = (ret[input.size - 1].toInt() xor (poly[3].toInt() and mask)).toByte() + return ret + } + + private fun shiftLeft(block: ByteArray, output: ByteArray): Int { + var i = block.size + var bit = 0 + while (--i >= 0) { + val b = block[i].toInt() and 0xff + output[i] = ((b shl 1) or bit).toByte() + bit = (b ushr 7) and 1 + } + return bit + } + + private fun lookupPoly(blockSizeLength: Int): ByteArray { + val xor = when (blockSizeLength * 8) { + 64 -> 0x1B + 128 -> 0x87 + 160 -> 0x2D + 192 -> 0x87 + 224 -> 0x309 + 256 -> 0x425 + 320 -> 0x1B + 384 -> 0x100D + 448 -> 0x851 + 512 -> 0x125 + 768 -> 0xA0011 + 1024 -> 0x80043 + 2048 -> 0x86001 + else -> throw IllegalArgumentException("Unknown block size for CMAC: " + (blockSizeLength * 8)) + } + return intToBigEndian(xor) + } + + private fun intToBigEndian(value: Int): ByteArray { + return byteArrayOf( + (value shr 24).toByte(), + (value shr 16).toByte(), + (value shr 8).toByte(), + value.toByte() + ) + } +}