diff --git a/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Api.kt b/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Api.kt index a50eea9..8292a9d 100644 --- a/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Api.kt +++ b/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Api.kt @@ -62,8 +62,8 @@ interface EncryptionApi { } interface AuthenticatedEncryption { - fun encrypt(key: UByteArray, nonce: UByteArray, message: UByteArray, additionalData: UByteArray) : UByteArray - fun decrypt(key: UByteArray, nonce: UByteArray, cipherText: UByteArray, additionalData: UByteArray) : UByteArray + fun encrypt(key: UByteArray, nonce: UByteArray, message: UByteArray, additionalData: UByteArray = ubyteArrayOf()) : UByteArray + fun decrypt(key: UByteArray, nonce: UByteArray, cipherText: UByteArray, additionalData: UByteArray = ubyteArrayOf()) : UByteArray } @@ -75,11 +75,11 @@ data class MultipartEncryptionHeader(val nonce: UByteArray) class InvalidTagException : RuntimeException("Tag mismatch! Encrypted data is corrupted or tampered with.") interface MultipartAuthenticatedDecryption { - fun decryptPartialData(data: EncryptedDataPart, additionalData: UByteArray) : DecryptedDataPart + fun decryptPartialData(data: EncryptedDataPart, additionalData: UByteArray = ubyteArrayOf()) : DecryptedDataPart } interface MultipartAuthenticatedEncryption { - fun encryptPartialData(data: UByteArray, additionalData: UByteArray) : EncryptedDataPart + fun encryptPartialData(data: UByteArray, additionalData: UByteArray = ubyteArrayOf()) : EncryptedDataPart fun startEncryption() : MultipartEncryptionHeader } diff --git a/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/HashFunction.kt b/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/HashFunction.kt index 25f6f9c..50bc267 100644 --- a/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/HashFunction.kt +++ b/multiplatform-crypto-api/src/commonMain/kotlin/com/ionspin/kotlin/crypto/hash/HashFunction.kt @@ -43,3 +43,7 @@ fun String.encodeToUByteArray() : UByteArray{ return encodeToByteArray().asUByteArray() } +fun UByteArray.decodeToString() : String { + return this.asByteArray().contentToString() +} + diff --git a/multiplatform-crypto-delegated/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/DelegatedXChaCha20Poly1305.kt b/multiplatform-crypto-delegated/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/DelegatedXChaCha20Poly1305.kt index 3e5a955..645b429 100644 --- a/multiplatform-crypto-delegated/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/DelegatedXChaCha20Poly1305.kt +++ b/multiplatform-crypto-delegated/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/DelegatedXChaCha20Poly1305.kt @@ -7,7 +7,7 @@ package com.ionspin.kotlin.crypto.authenticated * on 14-Jun-2020 */ expect class XChaCha20Poly1305Delegated internal constructor() { - internal constructor(key: UByteArray, testState : UByteArray, testHeader: UByteArray) + internal constructor(key: UByteArray, testState : UByteArray, testHeader: UByteArray, isDecryptor: Boolean) companion object { fun encrypt(key: UByteArray, nonce: UByteArray, message: UByteArray, additionalData: UByteArray) : UByteArray fun decrypt(key: UByteArray, nonce: UByteArray, ciphertext: UByteArray, additionalData: UByteArray) : UByteArray diff --git a/multiplatform-crypto-delegated/src/commonTest/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Test.kt b/multiplatform-crypto-delegated/src/commonTest/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Test.kt index 68ccc3e..f553169 100644 --- a/multiplatform-crypto-delegated/src/commonTest/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Test.kt +++ b/multiplatform-crypto-delegated/src/commonTest/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Test.kt @@ -211,8 +211,8 @@ class XChaCha20Poly1305Test { 0xDEU, 0xFBU, 0x5CU, 0x7FU, 0x1CU, 0x26U, 0x32U, 0x2CU, 0x51U, 0xF6U, 0xEFU, 0xC6U, 0x34U, 0xC4U, 0xACU, 0x6CU, 0xE8U, 0xF9U, 0x4BU, 0xABU, 0xA3U, ) - val encryptor = XChaCha20Poly1305Delegated(key, state, header) - val decryptor = XChaCha20Poly1305Delegated(key, state, header) + val encryptor = XChaCha20Poly1305Delegated(key, state, header, false) + val decryptor = XChaCha20Poly1305Delegated(key, state, header, true) val data = UByteArray(100) { 0U } val result = encryptor.encrypt(data) val decrypted = decryptor.decrypt(result) @@ -228,7 +228,7 @@ class XChaCha20Poly1305Test { val messedUpTag = result.copyOf() messedUpTag[messedUpTag.size - 2] = 0U assertFails { - val decryptorForWrongTag = XChaCha20Poly1305Delegated(key, state, header) + val decryptorForWrongTag = XChaCha20Poly1305Delegated(key, state, header, true) val plaintext = decryptorForWrongTag.decrypt(messedUpTag) println("Decrypted with wrong tag -----------") plaintext.hexColumsPrint() diff --git a/multiplatform-crypto-delegated/src/commonTest/kotlin/com/ionspin/kotlin/crypto/highlevel/EncryptionTest.kt b/multiplatform-crypto-delegated/src/commonTest/kotlin/com/ionspin/kotlin/crypto/highlevel/EncryptionTest.kt new file mode 100644 index 0000000..a1638d8 --- /dev/null +++ b/multiplatform-crypto-delegated/src/commonTest/kotlin/com/ionspin/kotlin/crypto/highlevel/EncryptionTest.kt @@ -0,0 +1,54 @@ +package com.ionspin.kotlin.crypto.highlevel + +import com.ionspin.kotlin.crypto.Crypto +import com.ionspin.kotlin.crypto.Initializer +import com.ionspin.kotlin.crypto.SymmetricKey +import com.ionspin.kotlin.crypto.hash.decodeToString +import com.ionspin.kotlin.crypto.hash.encodeToUByteArray +import com.ionspin.kotlin.crypto.util.hexColumsPrint +import com.ionspin.kotlin.crypto.util.testBlocking +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 09-Jul-2020 + */ +class EncryptionTest { + @Test + fun testMultipartEncryption() = testBlocking { + Initializer.initialize() + val plaintext = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Vestibulum maximus tincidunt urna. " + + "Nullam sit amet erat id arcu porttitor varius ut at metus. " + + "Nunc sit amet felis vel velit ornare gravida. " + + "Curabitur tellus lacus, pulvinar a diam at tincidunt.").encodeToUByteArray() + val additionalData = "Additional data 1".encodeToUByteArray() + val keyValue = UByteArray(32) { it.toUByte() } + val key = SymmetricKey(keyValue) + val encryptor = Crypto.Encryption.createMultipartEncryptor(key) + val header = encryptor.startEncryption() + val ciphertext1 = encryptor.encryptPartialData(plaintext.sliceArray(0 until 100), additionalData) + val ciphertext2 = encryptor.encryptPartialData(plaintext.sliceArray(100 until 200)) + val ciphertext3 = encryptor.encryptPartialData(plaintext.sliceArray(200 until 250)) + //decrypt + val decryptor = Crypto.Encryption.createMultipartDecryptor(key, header) + println("Initialized") + val plaintext1 = decryptor.decryptPartialData(ciphertext1, additionalData) + val plaintext2 = decryptor.decryptPartialData(ciphertext2) + val plaintext3 = decryptor.decryptPartialData(ciphertext3) + + val combinedPlaintext = plaintext1.data + plaintext2.data + plaintext3.data + println("---- Plaintext -----") + plaintext.hexColumsPrint() + println("---- Plaintext result -----") + combinedPlaintext.hexColumsPrint() + assertTrue { + plaintext.contentEquals(combinedPlaintext) + } + + + + } +} diff --git a/multiplatform-crypto-delegated/src/jsMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt b/multiplatform-crypto-delegated/src/jsMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt index a2c1b3e..9abb4e1 100644 --- a/multiplatform-crypto-delegated/src/jsMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt +++ b/multiplatform-crypto-delegated/src/jsMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt @@ -54,41 +54,65 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { } var state : dynamic = null + var isInitialized = false + var isEncryptor = false actual fun initializeForEncryption(key: UByteArray) : UByteArray { println("Initializaing for encryption") val stateAndHeader = getSodium().crypto_secretstream_xchacha20poly1305_init_push(key.toUInt8Array()) - val state = stateAndHeader.state - val header = stateAndHeader.header + state = stateAndHeader.state + val header = stateAndHeader.header as Uint8Array console.log(state) console.log(header) println("Done initializaing for encryption") - return header + isInitialized = true + isEncryptor = true + return header.toUByteArray() } actual fun initializeForDecryption(key: UByteArray, header: UByteArray) { - + println("Initializing for decryption") + header.hexColumsPrint() + state = getSodium().crypto_secretstream_xchacha20poly1305_init_pull(header.toUInt8Array(), key.toUInt8Array()) + console.log(state) + isInitialized = true + isEncryptor = false } internal actual constructor( key: UByteArray, testState: UByteArray, - testHeader: UByteArray + testHeader: UByteArray, + isDecryptor: Boolean ) : this() { state = getSodium().crypto_secretstream_xchacha20poly1305_init_pull(testHeader.toUInt8Array(), key.toUInt8Array()) console.log(state) println("Done initializaing test state") + isInitialized = true + isEncryptor = !isDecryptor } actual fun encrypt(data: UByteArray, additionalData: UByteArray): UByteArray { + if (!isInitialized) { + throw RuntimeException("Not initalized!") + } + if (!isEncryptor) { + throw RuntimeException("Initialized as decryptor, attempted to use as encryptor") + } val encrypted = getSodium().crypto_secretstream_xchacha20poly1305_push(state, data.toUInt8Array(), additionalData.toUInt8Array(), 0U) return encrypted.toUByteArray() } actual fun decrypt(data: UByteArray, additionalData: UByteArray): UByteArray { + if (!isInitialized) { + throw RuntimeException("Not initalized!") + } + if (isEncryptor) { + throw RuntimeException("Initialized as encryptor, attempted to use as decryptor") + } val decryptedWithTag = getSodium().crypto_secretstream_xchacha20poly1305_pull(state, data.toUInt8Array(), additionalData.toUInt8Array()) val decrypted = decryptedWithTag.message as Uint8Array - val validTag = decryptedWithTag.tag as UInt + val validTag = decryptedWithTag.tag if (validTag != 0U) { println("Tag validation failed") diff --git a/multiplatform-crypto-delegated/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt b/multiplatform-crypto-delegated/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt index dcbd5b2..62b69f7 100644 --- a/multiplatform-crypto-delegated/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt +++ b/multiplatform-crypto-delegated/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt @@ -60,23 +60,42 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { val state : SecretStream.State = SecretStream.State() val sodium = SodiumJava() + var isInitialized = false + var isEncryptor = false + internal actual constructor( key: UByteArray, testState: UByteArray, - testHeader: UByteArray + testHeader: UByteArray, + isDecryptor: Boolean ) : this() { state.k = testState.sliceArray(0 until 32).toByteArray() state.nonce = testState.sliceArray(32 until 44).toByteArray() + isInitialized = true + isEncryptor = !isDecryptor } actual fun initializeForEncryption(key: UByteArray) : UByteArray { - TODO() + val header = UByteArray(24) + sodium.crypto_secretstream_xchacha20poly1305_init_push(state, header.asByteArray(), key.asByteArray()) + isInitialized = true + isEncryptor = true + return header } actual fun initializeForDecryption(key: UByteArray, header: UByteArray) { + sodium.crypto_secretstream_xchacha20poly1305_init_pull(state, header.asByteArray(), key.asByteArray()) + isInitialized = true + isEncryptor = false } actual fun encrypt(data: UByteArray, additionalData: UByteArray): UByteArray { + if (!isInitialized) { + throw RuntimeException("Not initalized!") + } + if (!isEncryptor) { + throw RuntimeException("Initialized as decryptor, attempted to use as encryptor") + } val ciphertext = ByteArray(1 + data.size + 16) sodium.crypto_secretstream_xchacha20poly1305_push( state, ciphertext, null, @@ -88,6 +107,12 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { } actual fun decrypt(data: UByteArray, additionalData: UByteArray): UByteArray { + if (!isInitialized) { + throw RuntimeException("Not initalized!") + } + if (isEncryptor) { + throw RuntimeException("Initialized as encryptor, attempted to use as decryptor") + } val plaintext = ByteArray(data.size - 17) val validTag = sodium.crypto_secretstream_xchacha20poly1305_pull( diff --git a/multiplatform-crypto-delegated/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt b/multiplatform-crypto-delegated/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt index d3eedeb..8f6761f 100644 --- a/multiplatform-crypto-delegated/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt +++ b/multiplatform-crypto-delegated/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Delegated.kt @@ -69,10 +69,14 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { val header = UByteArray(crypto_secretstream_xchacha20poly1305_HEADERBYTES.toInt()) { 0U } + var isInitialized = false + var isEncryptor = false + actual internal constructor( key: UByteArray, testState: UByteArray, - testHeader: UByteArray + testHeader: UByteArray, + isDecryptor: Boolean ) : this() { val pointer = state.ptr.reinterpret() for (i in 0 until crypto_secretstream_xchacha20poly1305_state.size.toInt()) { @@ -85,6 +89,8 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { testHeader.copyInto(header) header.hexColumsPrint() println("header after setting-----------") + isInitialized = true + isEncryptor = !isDecryptor } @@ -103,7 +109,7 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { } actual fun initializeForDecryption(key: UByteArray, header: UByteArray) { - + crypto_secretstream_xchacha20poly1305_init_pull(state.ptr, header.toCValues(), key.toCValues()) } @@ -116,8 +122,8 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { null, data.toCValues(), data.size.convert(), - null, - 0U, + additionalData.toCValues(), + additionalData.size.convert(), 0U, ) println("Encrypt partial") @@ -141,6 +147,7 @@ actual class XChaCha20Poly1305Delegated internal actual constructor() { additionalData.size.convert() ) plaintextPinned.unpin() + println("tag: $validTag") if (validTag != 0) { println("Tag validation failed") throw InvalidTagException() diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Pure.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Pure.kt index 301c8cb..9e1a16c 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Pure.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/authenticated/XChaCha20Poly1305Pure.kt @@ -95,7 +95,6 @@ class XChaCha20Poly1305Pure(val key: UByteArray, val nonce: UByteArray) { } fun streamEncrypt(data: UByteArray, additionalData: UByteArray, tag : UByte) : UByteArray { - val result = UByteArray(1 + data.size + 16) //Tag marker, ciphertext, mac //get encryption state val block = UByteArray(64) { 0U } ChaCha20Pure.xorWithKeystream(calcKey, calcNonce, block, 0U).copyInto(block) // This is equivalent to the first 64 bytes of keystream @@ -107,6 +106,9 @@ class XChaCha20Poly1305Pure(val key: UByteArray, val nonce: UByteArray) { } block[0] = tag ChaCha20Pure.xorWithKeystream(calcKey, calcNonce, block, 1U).copyInto(block) // This just xors block[0] with keystream + println("encrypt block going into poly ----") + block.hexColumsPrint() + println("encrypt block going into poly end ----") processPolyBytes(poly1305, block) // but updates the mac with the full block! // In libsodium c code, it now sets the first byte to be a tag, we'll just save it for now val encryptedTag = block[0] @@ -121,11 +123,57 @@ class XChaCha20Poly1305Pure(val key: UByteArray, val nonce: UByteArray) { val finalMac = additionalData.size.toULong().toLittleEndianUByteArray() + (ciphertext.size + 64).toULong().toLittleEndianUByteArray() processPolyBytes(poly1305, finalMac) val mac = poly1305.finalizeMac(polyBuffer.sliceArray(0 until polyBufferByteCounter)) + //TODO process state + println("Ciphertext ---------") + (ubyteArrayOf(encryptedTag) + ciphertext + mac).hexColumsPrint() + println("Ciphertext end ---------") return ubyteArrayOf(encryptedTag) + ciphertext + mac } fun streamDecrypt(data: UByteArray, additionalData: UByteArray, tag: UByte) : UByteArray { - TODO() + val block = UByteArray(64) { 0U } + ChaCha20Pure.xorWithKeystream(calcKey, calcNonce, block, 0U).copyInto(block) // This is equivalent to the first 64 bytes of keystream + val poly1305 = Poly1305(block) + block.overwriteWithZeroes() + if (additionalData.isNotEmpty()) { + val additionalDataPadded = additionalData + UByteArray(16 - additionalData.size % 16) { 0U } + processPolyBytes(poly1305, additionalDataPadded) + } + block[0] = data[0] + ChaCha20Pure.xorWithKeystream(calcKey, calcNonce, block, 1U).copyInto(block)// get the keystream xored with zeroes, but also decrypteg tag marker + val tag = block[0] //get the decrypted tag + block[0] = data[0] // this brings it back to state that is delivered to poly in encryption function + println("Decrypted tag $tag") + println("decrypt block going into poly ----") + block.hexColumsPrint() + println("decrypt block going into poly end ----") + processPolyBytes(poly1305, block) + // Next we update the poly1305 with ciphertext and padding, BUT the padding in libsodium is not correctly calculated, so it doesn't + // pad correctly. https://github.com/jedisct1/libsodium/issues/976 + // We want to use libsodium in delegated flavour, so we will use the same incorrect padding here. + // From security standpoint there are no obvious drawbacks, as padding was initially added to decrease implementation complexity. + val ciphertext = data.sliceArray(1 until data.size - 16) + processPolyBytes(poly1305, ciphertext + UByteArray(((16U + ciphertext.size.toUInt() - block.size.toUInt()) % 16U).toInt()) { 0U } ) + val plaintext = ChaCha20Pure.xorWithKeystream(calcKey, calcNonce, ciphertext, 2U) + val finalMac = additionalData.size.toULong().toLittleEndianUByteArray() + (ciphertext.size + 64).toULong().toLittleEndianUByteArray() + processPolyBytes(poly1305, finalMac) + val mac = poly1305.finalizeMac(polyBuffer.sliceArray(0 until polyBufferByteCounter)) + println("--- mac") + mac.hexColumsPrint() + println("--- mac end") + val expectedMac = data.sliceArray(data.size - 16 until data.size) + println("--- expectedMac") + expectedMac.hexColumsPrint() + println("--- expectedMac end") + + //TODO process state + println("Plaintext ---------") + plaintext.hexColumsPrint() + println("Plaintext end ---------") + if (expectedMac.contentEquals(mac).not()){ + throw InvalidTagException() + } + return plaintext } diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/highlevel/EncryptionTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/highlevel/EncryptionTest.kt new file mode 100644 index 0000000..7b277f3 --- /dev/null +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/highlevel/EncryptionTest.kt @@ -0,0 +1,53 @@ +package com.ionspin.kotlin.crypto.highlevel + +import com.ionspin.kotlin.crypto.Crypto +import com.ionspin.kotlin.crypto.SymmetricKey +import com.ionspin.kotlin.crypto.hash.decodeToString +import com.ionspin.kotlin.crypto.hash.encodeToUByteArray +import com.ionspin.kotlin.crypto.util.hexColumsPrint +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 09-Jul-2020 + */ +class EncryptionTest { + @Test + fun testMultipartEncryption() { + val plaintext = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Vestibulum maximus tincidunt urna. " + + "Nullam sit amet erat id arcu porttitor varius ut at metus. " + + "Nunc sit amet felis vel velit ornare gravida. " + + "Curabitur tellus lacus, pulvinar a diam at tincidunt.").encodeToUByteArray() + plaintext.hexColumsPrint() + val additionalData = "Additional data 1".encodeToUByteArray() +// val additionalData = ubyteArrayOf() + val keyValue = UByteArray(32) { it.toUByte() } + val key = SymmetricKey(keyValue) + val encryptor = Crypto.Encryption.createMultipartEncryptor(key) + val header = encryptor.startEncryption() + val ciphertext1 = encryptor.encryptPartialData(plaintext.sliceArray(0 until 100), additionalData) + val ciphertext2 = encryptor.encryptPartialData(plaintext.sliceArray(100 until 200)) + val ciphertext3 = encryptor.encryptPartialData(plaintext.sliceArray(200 until 250)) + //decrypt + val decryptor = Crypto.Encryption.createMultipartDecryptor(key, header) + println("Initialized") + val plaintext1 = decryptor.decryptPartialData(ciphertext1, additionalData) + val plaintext2 = decryptor.decryptPartialData(ciphertext2) + val plaintext3 = decryptor.decryptPartialData(ciphertext3) + + val combinedPlaintext = plaintext1.data + plaintext2.data + plaintext3.data + println("---- Plaintext -----") + plaintext.hexColumsPrint() + println("---- Plaintext result -----") + combinedPlaintext.hexColumsPrint() + assertTrue { + plaintext.contentEquals(combinedPlaintext) + } + + + + } +}