diff --git a/README.md b/README.md index 673ee68..de19e49 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Kotlin Multiplatform Crypto is a library for various cryptographic applications. This is an extremely early release, currently only consisting of Blake2b and SHA256 and 512. +API is very opinionated, ment to be used on both encrypting and decrypting side. The idea is that API leaves less room for +errors when using it. + ## Notes & Roadmap **The API will move fast and break often until v1.0** @@ -28,8 +31,6 @@ No. This is an experimental implementation, mostly for expanding personal understanding of cryptography. It's not peer reviewed, not guaranteed to be bug free, and not guaranteed to be secure. -## Supported - ## Hashing functions * Blake2b * SHA512 @@ -37,6 +38,7 @@ It's not peer reviewed, not guaranteed to be bug free, and not guaranteed to be ## Symmetric cipher * AES + * Modes: CBC, CTR More to come. @@ -129,6 +131,58 @@ val sha512 = Sha512() sha512.update("abc") val result = sha512.digest() ``` +### Symmetric encryption + +#### AES + +Aes is available with CBC and CTR mode through `AesCbc` and `AesCtr` classes/objects. +Similarly to hashes you can either use stateless or updateable version. + +Initialization vector, or counter states are chosen by the SDK automaticaly, and returned alongside encrypted data + +##### Stateless AesCbc and AesCtr + +AesCtr + +```kotlin +val keyString = "4278b840fb44aaa757c1bf04acbe1a3e" +val key = AesKey.Aes128Key(keyString) +val plainText = "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710" + +val encryptedDataAndInitializationVector = AesCtr.encrypt(key, plainText.hexStringToUByteArray()) +val decrypted = AesCtr.decrypt( + key, + encryptedDataAndInitializationVector.encryptedData, + encryptedDataAndInitializationVector.initialCounter +) +plainText == decrypted.toHexString() +``` + +AesCbc + +```kotlin + +val keyString = "4278b840fb44aaa757c1bf04acbe1a3e" +val key = AesKey.Aes128Key(keyString) + +val plainText = "3c888bbbb1a8eb9f3e9b87acaad986c466e2f7071c83083b8a557971918850e5" + +val encryptedDataAndInitializationVector = AesCbc.encrypt(key, plainText.hexStringToUByteArray()) +val decrypted = AesCbc.decrypt( + key, + encryptedDataAndInitializationVector.encryptedData, + encryptedDataAndInitializationVector.initilizationVector +) +plainText == decrypted.toHexString() + +``` + + + + + + + diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt index 7369b94..a9a3038 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbc.kt @@ -21,6 +21,12 @@ import com.ionspin.kotlin.crypto.chunked import com.ionspin.kotlin.crypto.xor /** + * Advanced encryption standard with cipher block chaining and PKCS #5 + * + * For bulk encryption/decryption use [AesCbc.encrypt] and [AesCbc.decrypt] + * + * To get an instance of AesCbc and then feed it data sequentially with [addData] use [createEncryptor] and [createDecryptor] + * * Created by Ugljesa Jovanovic * ugljesa.jovanovic@ionspin.com * on 21-Sep-2019 @@ -30,13 +36,39 @@ class AesCbc internal constructor(val aesKey: AesKey, val mode: Mode, initializa companion object { const val BLOCK_BYTES = 16 + /** + * Creates and returns AesCbc instance that can be fed data using [addData]. Once you have submitted all + * data call [encrypt] + */ + fun createEncryptor(aesKey: AesKey) : AesCbc { + return AesCbc(aesKey, Mode.ENCRYPT) + } + /** + * Creates and returns AesCbc instance that can be fed data using [addData]. Once you have submitted all + * data call [decrypt] + */ + fun createDecryptor(aesKey : AesKey) : AesCbc { + return AesCbc(aesKey, Mode.DECRYPT) + } - fun encrypt(aesKey: AesKey, data: Array): Array { + /** + * Bulk encryption, returns encrypted data and a random initialization vector + */ + fun encrypt(aesKey: AesKey, data: Array): EncryptedDataAndInitializationVector { val aesCbc = AesCbc(aesKey, Mode.ENCRYPT) aesCbc.addData(data) return aesCbc.encrypt() } + /** + * Bulk decryption, returns decrypted data + */ + fun decrypt(aesKey: AesKey, data: Array, initialCounter: Array? = null): Array { + val aesCbc = AesCbc(aesKey, Mode.DECRYPT, initialCounter) + aesCbc.addData(data) + return aesCbc.decrypt() + } + private fun padToBlock(unpadded: Array): Array { val paddingSize = 16 - unpadded.size if (unpadded.size == BLOCK_BYTES) { @@ -109,7 +141,11 @@ class AesCbc internal constructor(val aesKey: AesKey, val mode: Mode, initializa } - fun encrypt(): Array { + /** + * Encrypt fed data and return it alongside the randomly chosen initialization vector + * @return Encrypted data and initialization vector + */ + fun encrypt(): EncryptedDataAndInitializationVector { if (bufferCounter > 0) { val lastBlockPadded = padToBlock(buffer) if (lastBlockPadded.size > BLOCK_BYTES) { @@ -120,9 +156,16 @@ class AesCbc internal constructor(val aesKey: AesKey, val mode: Mode, initializa output += consumeBlock(lastBlockPadded) } } - return output.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes } + return EncryptedDataAndInitializationVector( + output.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes }, + iv + ) } + /** + * Decrypt data + * @return Decrypted data + */ fun decrypt(): Array { val removePaddingCount = output.last().last() @@ -169,4 +212,25 @@ class AesCbc internal constructor(val aesKey: AesKey, val mode: Mode, initializa } +} + +@ExperimentalUnsignedTypes +data class EncryptedDataAndInitializationVector(val encryptedData : Array, val initilizationVector : Array) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as EncryptedDataAndInitializationVector + + if (!encryptedData.contentEquals(other.encryptedData)) return false + if (!initilizationVector.contentEquals(other.initilizationVector)) return false + + return true + } + + override fun hashCode(): Int { + var result = encryptedData.contentHashCode() + result = 31 * result + initilizationVector.contentHashCode() + return result + } } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt index 23e63df..a966dde 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt @@ -21,9 +21,17 @@ import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.modular.ModularBigInteger import com.ionspin.kotlin.crypto.SRNG import com.ionspin.kotlin.crypto.chunked +import com.ionspin.kotlin.crypto.symmetric.AesCtr.Companion.encrypt import com.ionspin.kotlin.crypto.xor /** + * + * Advanced encryption standard with counter mode + * + * For bulk encryption/decryption use [AesCtr.encrypt] and [AesCtr.decrypt] + * + * To get an instance of AesCtr and then feed it data sequentially with [addData] use [createEncryptor] and [createDecryptor] + * * Created by Ugljesa Jovanovic * ugljesa.jovanovic@ionspin.com * on 22-Sep-2019 @@ -34,38 +42,38 @@ class AesCtr internal constructor(val aesKey: AesKey, val mode: Mode, initialCou companion object { const val BLOCK_BYTES = 16 - val modularCreator = ModularBigInteger.creatorForModulo(BigInteger.ONE.shl(129) - 1) - - fun encrypt(aesKey: AesKey, data: Array): Array { - val aesCbc = AesCbc(aesKey, Mode.ENCRYPT) - aesCbc.addData(data) - return aesCbc.encrypt() + val modularCreator = ModularBigInteger.creatorForModulo(BigInteger.ONE.shl(128) - 1) + /** + * Creates and returns AesCtr instance that can be fed data using [addData]. Once you have submitted all + * data call [encrypt] + */ + fun createEncryptor(aesKey: AesKey) : AesCtr { + return AesCtr(aesKey, Mode.ENCRYPT) + } + /** + * Creates and returns AesCtr instance that can be fed data using [addData]. Once you have submitted all + * data call [decrypt] + */ + fun createDecryptor(aesKey : AesKey) : AesCtr { + return AesCtr(aesKey, Mode.DECRYPT) + } + /** + * Bulk encryption, returns encrypted data and a random initial counter + */ + fun encrypt(aesKey: AesKey, data: Array): EncryptedDataAndInitialCounter { + val aesCtr = AesCtr(aesKey, Mode.ENCRYPT) + aesCtr.addData(data) + return aesCtr.encrypt() + } + /** + * Bulk decryption, returns decrypted data + */ + fun decrypt(aesKey: AesKey, data: Array, initialCounter: Array? = null): Array { + val aesCtr = AesCtr(aesKey, Mode.DECRYPT, initialCounter) + aesCtr.addData(data) + return aesCtr.decrypt() } - private fun padToBlock(unpadded: Array): Array { - val paddingSize = 16 - unpadded.size - if (unpadded.size == BLOCK_BYTES) { - return unpadded - } - - if (unpadded.size == BLOCK_BYTES) { - return Array(BLOCK_BYTES) { - BLOCK_BYTES.toUByte() - } - } - - if (unpadded.size > BLOCK_BYTES) { - throw IllegalStateException("Block larger than 128 bytes") - } - - return Array(BLOCK_BYTES) { - when (it) { - in unpadded.indices -> unpadded[it] - else -> paddingSize.toUByte() - } - } - - } } var currentOutput: Array = arrayOf() @@ -115,34 +123,32 @@ class AesCtr internal constructor(val aesKey: AesKey, val mode: Mode, initialCou } } - - fun encrypt(): Array { + /** + * Encrypt fed data and return it alongside the randomly chosen initial counter state + * @return Encrypted data and initial counter state + */ + fun encrypt(): EncryptedDataAndInitialCounter { if (bufferCounter > 0) { - val lastBlockPadded = padToBlock(buffer) - if (lastBlockPadded.size > BLOCK_BYTES) { - val chunks = lastBlockPadded.chunked(BLOCK_BYTES) - output += consumeBlock(chunks[0], blockCounter) - blockCounter += 1 - output += consumeBlock(chunks[1], blockCounter) - } else { - output += consumeBlock(lastBlockPadded, blockCounter) - } + output += consumeBlock(buffer, blockCounter) } - return output.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes } + return EncryptedDataAndInitialCounter( + output.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes }, + counterStart + ) } - + /** + * Decrypt data + * @return Decrypted data + */ fun decrypt(): Array { - val removePaddingCount = output.last().last() - val removedPadding = if (removePaddingCount > 0U && removePaddingCount < 16U) { - output.last().dropLast(removePaddingCount.toInt() and 0x7F) - } else { - output.last().toList() + if (bufferCounter > 0) { + output += consumeBlock(buffer, blockCounter) } - val preparedOutput = output.dropLast(1).toTypedArray() + removedPadding.toTypedArray() //JS compiler freaks out here if we don't supply exact type - val reversed : List> = preparedOutput.reversed() as List> - val folded : Array = reversed.foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> - acc + arrayOfUBytes } + val reversed: List> = output.reversed() as List> + val folded: Array = reversed.foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> + acc + arrayOfUBytes + } return folded } @@ -163,4 +169,25 @@ class AesCtr internal constructor(val aesKey: AesKey, val mode: Mode, initialCou } +} + +@ExperimentalUnsignedTypes +data class EncryptedDataAndInitialCounter(val encryptedData : Array, val initialCounter : Array) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as EncryptedDataAndInitializationVector + + if (!encryptedData.contentEquals(other.encryptedData)) return false + if (!initialCounter.contentEquals(other.initilizationVector)) return false + + return true + } + + override fun hashCode(): Int { + var result = encryptedData.contentHashCode() + result = 31 * result + initialCounter.contentHashCode() + return result + } } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt index 12c3370..98b56a7 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt @@ -31,43 +31,67 @@ class AesCbcTest { @Test fun testCbcEncryption() { - val key = "4278b840fb44aaa757c1bf04acbe1a3e" - val iv = "57f02a5c5339daeb0a2908a06ac6393f" - val plaintext = "3c888bbbb1a8eb9f3e9b87acaad986c466e2f7071c83083b8a557971918850e5" - val expectedCipherText = "479c89ec14bc98994e62b2c705b5014e175bd7832e7e60a1e92aac568a861eb7" - val aesCbc = AesCbc(AesKey.Aes128Key(key), mode = Mode.ENCRYPT, initializationVector = iv.hexStringToUByteArray()) - aesCbc.addData(plaintext.hexStringToUByteArray()) - val encrypted = aesCbc.encrypt() - println("Encrypted: ${encrypted.toHexString()}") assertTrue { - expectedCipherText == encrypted.toHexString() + val key = "4278b840fb44aaa757c1bf04acbe1a3e" + val iv = "57f02a5c5339daeb0a2908a06ac6393f" + val plaintext = "3c888bbbb1a8eb9f3e9b87acaad986c466e2f7071c83083b8a557971918850e5" + val expectedCipherText = "479c89ec14bc98994e62b2c705b5014e175bd7832e7e60a1e92aac568a861eb7" + val aesCbc = + AesCbc(AesKey.Aes128Key(key), mode = Mode.ENCRYPT, initializationVector = iv.hexStringToUByteArray()) + aesCbc.addData(plaintext.hexStringToUByteArray()) + val encrypted = aesCbc.encrypt() + println("Encrypted: ${encrypted.encryptedData.toHexString()}") + + expectedCipherText == encrypted.encryptedData.toHexString() && + iv == encrypted.initilizationVector.toHexString() } + assertTrue { + val keyString = "4278b840fb44aaa757c1bf04acbe1a3e" + val key = AesKey.Aes128Key(keyString) + + val plainText = "3c888bbbb1a8eb9f3e9b87acaad986c466e2f7071c83083b8a557971918850e5" + + val encryptedDataAndInitializationVector = AesCbc.encrypt(key, plainText.hexStringToUByteArray()) + val decrypted = AesCbc.decrypt( + key, + encryptedDataAndInitializationVector.encryptedData, + encryptedDataAndInitializationVector.initilizationVector + ) + plainText == decrypted.toHexString() + } + + } @Test fun testCbcDecryption() { - val key = "4278b840fb44aaa757c1bf04acbe1a3e" - val iv = "57f02a5c5339daeb0a2908a06ac6393f" - val cipherText = "479c89ec14bc98994e62b2c705b5014e175bd7832e7e60a1e92aac568a861eb7" - val expectedPlainText = "3c888bbbb1a8eb9f3e9b87acaad986c466e2f7071c83083b8a557971918850e5" - val aesCbc = AesCbc(AesKey.Aes128Key(key), mode = Mode.DECRYPT, initializationVector = iv.hexStringToUByteArray()) - aesCbc.addData(cipherText.hexStringToUByteArray()) - val decrypted = aesCbc.decrypt() - println("Decrypted: ${decrypted.toHexString()}") assertTrue { + val key = "4278b840fb44aaa757c1bf04acbe1a3e" + val iv = "57f02a5c5339daeb0a2908a06ac6393f" + val cipherText = "479c89ec14bc98994e62b2c705b5014e175bd7832e7e60a1e92aac568a861eb7" + val expectedPlainText = "3c888bbbb1a8eb9f3e9b87acaad986c466e2f7071c83083b8a557971918850e5" + val aesCbc = + AesCbc(AesKey.Aes128Key(key), mode = Mode.DECRYPT, initializationVector = iv.hexStringToUByteArray()) + aesCbc.addData(cipherText.hexStringToUByteArray()) + val decrypted = aesCbc.decrypt() + println("Decrypted: ${decrypted.toHexString()}") + + expectedPlainText == decrypted.toHexString() + } + + assertTrue { + val key = "4278b840fb44aaa757c1bf04acbe1a3e" + val iv = "57f02a5c5339daeb0a2908a06ac6393f" + val cipherText = "479c89ec14bc98994e62b2c705b5014e175bd7832e7e60a1e92aac568a861eb7" + val expectedPlainText = "3c888bbbb1a8eb9f3e9b87acaad986c466e2f7071c83083b8a557971918850e5" + val decrypted = AesCbc.decrypt(AesKey.Aes128Key(key), cipherText.hexStringToUByteArray(), iv.hexStringToUByteArray()) + println("Decrypted: ${decrypted.toHexString()}") + expectedPlainText == decrypted.toHexString() } } - - - - - - - - } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt index 6f80b71..977766f 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt @@ -31,45 +31,67 @@ class AesCtrTest { @Test fun testCtrEncryption() { - val key = "2b7e151628aed2a6abf7158809cf4f3c" - val ic = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" - val plaintext = "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710" - val expectedCipherText = "874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee" - val aesCbc = AesCtr(AesKey.Aes128Key(key), mode = Mode.ENCRYPT, initialCounter = ic.hexStringToUByteArray()) - aesCbc.addData( - plaintext.hexStringToUByteArray() - ) - val encrypted = aesCbc.encrypt() - println("Encrypted: ${encrypted.toHexString()}") assertTrue { - expectedCipherText == encrypted.toHexString() + val key = "2b7e151628aed2a6abf7158809cf4f3c" + val ic = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + val plaintext = + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710" + val expectedCipherText = + "874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee" + val aesCtr = AesCtr(AesKey.Aes128Key(key), mode = Mode.ENCRYPT, initialCounter = ic.hexStringToUByteArray()) + aesCtr.addData( + plaintext.hexStringToUByteArray() + ) + val encrypted = aesCtr.encrypt() + println("Encrypted: ${encrypted.encryptedData.toHexString()}") + + expectedCipherText == encrypted.encryptedData.toHexString() && + ic == encrypted.initialCounter.toHexString() + } + + assertTrue { + val keyString = "4278b840fb44aaa757c1bf04acbe1a3e" + val key = AesKey.Aes128Key(keyString) + val plainText = "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710" + + val encryptedDataAndInitializationVector = AesCtr.encrypt(key, plainText.hexStringToUByteArray()) + val decrypted = AesCtr.decrypt( + key, + encryptedDataAndInitializationVector.encryptedData, + encryptedDataAndInitializationVector.initialCounter + ) + plainText == decrypted.toHexString() } } @Test fun testCtrDecryption() { - val key = "2b7e151628aed2a6abf7158809cf4f3c" - val ic = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" - val cipherText = "874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee" - val expectedPlainText = "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710" - val aesCbc = AesCtr(AesKey.Aes128Key(key), mode = Mode.DECRYPT, initialCounter = ic.hexStringToUByteArray()) - aesCbc.addData(cipherText.hexStringToUByteArray()) - val decrypted = aesCbc.decrypt() - println("Decrypted: ${decrypted.toHexString()}") assertTrue { + val key = "2b7e151628aed2a6abf7158809cf4f3c" + val ic = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + val cipherText = + "874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee" + val expectedPlainText = + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710" + val aesCtr = AesCtr(AesKey.Aes128Key(key), mode = Mode.DECRYPT, initialCounter = ic.hexStringToUByteArray()) + aesCtr.addData(cipherText.hexStringToUByteArray()) + val decrypted = aesCtr.decrypt() + println("Decrypted: ${decrypted.toHexString()}") + expectedPlainText == decrypted.toHexString() + } + + assertTrue { + val key = "2b7e151628aed2a6abf7158809cf4f3c" + val ic = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + val cipherText = + "874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee" + val expectedPlainText = + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710" + val decrypted = AesCtr.decrypt(AesKey.Aes128Key(key), cipherText.hexStringToUByteArray(), ic.hexStringToUByteArray()) + println("Decrypted: ${decrypted.toHexString()}") expectedPlainText == decrypted.toHexString() } } - - - - - - - - - - } \ No newline at end of file