From 1345125252f17d679cf7ee083a748b78d920602a Mon Sep 17 00:00:00 2001 From: Ugljesa Jovanovic Date: Sat, 21 Sep 2019 22:20:05 +0200 Subject: [PATCH] Added aes cbc --- multiplatform-crypto/build.gradle.kts | 1 - .../kotlin/com/ionspin/kotlin/crypto/Util.kt | 16 ++ .../ionspin/kotlin/crypto/symmetric/AesCbc.kt | 174 +++++++++++------- .../com/ionspin/kotlin/crypto/SRNGTest.kt} | 12 +- .../kotlin/crypto/symmetric/AesCbcTest.kt | 67 +++++++ .../kotlin/com/ionspin/kotlin/crypto/SRNG.kt | 3 +- .../kotlin/com/ionspin/kotlin/crypto/SRNG.kt | 8 +- .../kotlin/com/ionspin/kotlin/crypto/SRNG.kt | 15 +- 8 files changed, 226 insertions(+), 70 deletions(-) rename multiplatform-crypto/src/{iosCommonMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt => commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt} (73%) create mode 100644 multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt diff --git a/multiplatform-crypto/build.gradle.kts b/multiplatform-crypto/build.gradle.kts index c3f51e7..ea57cb1 100644 --- a/multiplatform-crypto/build.gradle.kts +++ b/multiplatform-crypto/build.gradle.kts @@ -150,7 +150,6 @@ kotlin { val nativeMain by creating { dependsOn(commonMain) dependencies { - implementation(Deps.Native.coroutines) } } val nativeTest by creating { diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Util.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Util.kt index 64e3783..1b222ce 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Util.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Util.kt @@ -71,4 +71,20 @@ infix fun Array.xor(other : Array) : Array { throw RuntimeException("Operands of different sizes are not supported yet") } return this.copyOf().mapIndexed { index, it -> it xor other[index] }.toTypedArray() +} + +@ExperimentalUnsignedTypes +fun String.hexStringToUByteArray() : Array { + return this.chunked(2).map { it.toUByte(16) }.toTypedArray() +} + +@ExperimentalUnsignedTypes +fun Array.toHexString() : String { + return this.joinToString(separator = "") { + if (it <= 0x0FU) { + "0${it.toString(16)}" + } else { + it.toString(16) + } + } } \ No newline at end of file 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 cdbfd30..01a1597 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 @@ -17,6 +17,7 @@ package com.ionspin.kotlin.crypto.symmetric import com.ionspin.kotlin.crypto.SRNG +import com.ionspin.kotlin.crypto.chunked import com.ionspin.kotlin.crypto.xor /** @@ -25,86 +26,133 @@ import com.ionspin.kotlin.crypto.xor * on 21-Sep-2019 */ @ExperimentalUnsignedTypes -class AesCbc (val aesKey: AesKey) { +class AesCbc internal constructor(val aesKey: AesKey, val mode: Mode, initializationVector: Array? = null) { companion object { - val BLOCK_BYTES = 16 + const val BLOCK_BYTES = 16 + + fun encrypt(aesKey: AesKey, data: Array): Array { + val aesCbc = AesCbc(aesKey, Mode.ENCRYPT) + aesCbc.addData(data) + return aesCbc.encrypt() + } + + 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() - val iv = SRNG.getRandomBytes(16) + var currentOutput: Array = arrayOf() + var previousEncrypted: Array = arrayOf() + val iv = initializationVector ?: SRNG.getRandomBytes(16) val output = MutableList>(0) { arrayOf() } - val buffer : Array = UByteArray(16) { 0U }.toTypedArray() + var buffer: Array = UByteArray(16) { 0U }.toTypedArray() var bufferCounter = 0 + fun addData(data: Array) { + //Padding + when { + bufferCounter + data.size < BLOCK_BYTES -> appendToBuffer(data, bufferCounter) + bufferCounter + data.size >= BLOCK_BYTES -> { + val chunked = data.chunked(BLOCK_BYTES) + chunked.forEach { chunk -> + if (bufferCounter + chunk.size < BLOCK_BYTES) { + appendToBuffer(chunk, bufferCounter) + } else { + chunk.copyInto( + destination = buffer, + destinationOffset = bufferCounter, + startIndex = 0, + endIndex = BLOCK_BYTES - bufferCounter + ) + output += consumeBlock(buffer) + buffer = Array(BLOCK_BYTES) { + when (it) { + in (0 until (chunk.size - (BLOCK_BYTES - bufferCounter))) -> { + chunk[it + (BLOCK_BYTES - bufferCounter)] + } + else -> { + 0U + } + } -// fun addData(data : UByteArray) { -// //Padding -// when { -// bufferCounter + data.size < BLOCK_BYTES -> appendToBuffer(data, bufferCounter) -// bufferCounter + data.size >= BLOCK_BYTES -> { -// val chunked = data.chunked(BLOCK_BYTES) -// chunked.forEach { chunk -> -// if (bufferCounter + chunk.size < BLOCK_BYTES) { -// appendToBuffer(chunk, bufferCounter) -// } else { -// chunk.copyInto( -// destination = buffer, -// destinationOffset = bufferCounter, -// startIndex = 0, -// endIndex = BLOCK_BYTES - bufferCounter -// ) -// counter += BLOCK_BYTES -// consumeBlock(buffer) -// buffer = Array(BLOCK_BYTES) { -// when (it) { -// in (0 until (chunk.size - (BLOCK_BYTES - bufferCounter))) -> { -// chunk[it + (BLOCK_BYTES - bufferCounter)] -// } -// else -> { -// 0U -// } -// } -// -// } -// bufferCounter = chunk.size - (BLOCK_BYTES - bufferCounter) -// } -// } -// -// } -// data.size < 16 -> { -// val paddingSize = 16 - data.size -// val padding = UByteArray(16 - data.size) { paddingSize.toUByte() } -// output += processBlock(data + padding) -// } -// data.size == 16 -> { -// -// } -// data.size > 16 -> { -// -// } -// } -// -// if (data.size < 16) { -// -// } -// -// } + } + bufferCounter = chunk.size - (BLOCK_BYTES - bufferCounter) + } + } + + } + } + + } + + fun encrypt(): Array { + if (bufferCounter > 0) { + val lastBlockPadded = padToBlock(buffer) + if (lastBlockPadded.size > BLOCK_BYTES) { + val chunks = lastBlockPadded.chunked(BLOCK_BYTES) + output += consumeBlock(chunks[0]) + output += consumeBlock(chunks[1]) + } else { + output += consumeBlock(lastBlockPadded) + } + } + return output.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes } + } + + fun decrypt(): Array { + return output.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes } + } private fun appendToBuffer(array: Array, start: Int) { array.copyInto(destination = buffer, destinationOffset = start, startIndex = 0, endIndex = array.size) bufferCounter += array.size } - private fun processBlock(data : Array) : Array { - if (currentOutput.isEmpty()) { - currentOutput = Aes.encrypt(aesKey, data xor iv) - } else { - currentOutput = Aes.encrypt(aesKey, data xor currentOutput) + private fun consumeBlock(data: Array): Array { + return when (mode) { + Mode.ENCRYPT -> { + currentOutput = if (currentOutput.isEmpty()) { + Aes.encrypt(aesKey, data xor iv) + } else { + Aes.encrypt(aesKey, data xor currentOutput) + } + currentOutput + } + Mode.DECRYPT -> { + if (currentOutput.isEmpty()) { + currentOutput = Aes.decrypt(aesKey, data) xor iv + previousEncrypted = data + } else { + currentOutput = Aes.decrypt(aesKey, data) xor previousEncrypted + } + currentOutput + } } - return currentOutput + } } \ No newline at end of file diff --git a/multiplatform-crypto/src/iosCommonMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt similarity index 73% rename from multiplatform-crypto/src/iosCommonMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt rename to multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt index 8e8bbd7..fcede56 100644 --- a/multiplatform-crypto/src/iosCommonMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/SRNGTest.kt @@ -16,13 +16,19 @@ package com.ionspin.kotlin.crypto +import kotlin.test.Test +import kotlin.test.assertTrue + /** * Created by Ugljesa Jovanovic * ugljesa.jovanovic@ionspin.com * on 21-Sep-2019 */ -actual object SRNG { - actual fun getRandomBytes(amount: Int): Array { - TODO("not implemented yet") +class SRNGTest { + @Test + fun testSrng() { + val randomBytes1 = SRNG.getRandomBytes(10) + val randomBytes2 = SRNG.getRandomBytes(10) + assertTrue { !randomBytes1.contentEquals(randomBytes2) } } } \ 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 new file mode 100644 index 0000000..0c676c9 --- /dev/null +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCbcTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Ugljesa Jovanovic + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ionspin.kotlin.crypto.symmetric + +import com.ionspin.kotlin.crypto.hexStringToUByteArray +import com.ionspin.kotlin.crypto.toHexString +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 21-Sep-2019 + */ +@ExperimentalUnsignedTypes +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("Decrypted: ${encrypted.toHexString()}") + assertTrue { + expectedCipherText == encrypted.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("Encrypted: ${decrypted.toHexString()}") + assertTrue { + expectedPlainText == decrypted.toHexString() + } + + } + + + + +} \ No newline at end of file diff --git a/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt b/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt index 8e8bbd7..36cd9e2 100644 --- a/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt +++ b/multiplatform-crypto/src/jsMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt @@ -22,7 +22,8 @@ package com.ionspin.kotlin.crypto * on 21-Sep-2019 */ actual object SRNG { + var counter = 0 actual fun getRandomBytes(amount: Int): Array { - TODO("not implemented yet") + return arrayOf((counter++).toUByte()) // TODO Wow. Such random. Very entropy. } } \ No newline at end of file diff --git a/multiplatform-crypto/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt b/multiplatform-crypto/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt index 8e8bbd7..8bfdaa3 100644 --- a/multiplatform-crypto/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt +++ b/multiplatform-crypto/src/jvmMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt @@ -16,13 +16,19 @@ package com.ionspin.kotlin.crypto +import java.security.SecureRandom + /** * Created by Ugljesa Jovanovic * ugljesa.jovanovic@ionspin.com * on 21-Sep-2019 */ +@ExperimentalUnsignedTypes actual object SRNG { + val secureRandom = SecureRandom() actual fun getRandomBytes(amount: Int): Array { - TODO("not implemented yet") + val byteArray = ByteArray(amount) + secureRandom.nextBytes(byteArray) + return byteArray.toUByteArray().toTypedArray() } } \ No newline at end of file diff --git a/multiplatform-crypto/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt b/multiplatform-crypto/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt index 8e8bbd7..ea8b0fc 100644 --- a/multiplatform-crypto/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt +++ b/multiplatform-crypto/src/nativeMain/kotlin/com/ionspin/kotlin/crypto/SRNG.kt @@ -16,13 +16,26 @@ package com.ionspin.kotlin.crypto +import kotlinx.cinterop.* +import platform.posix.* + /** * Created by Ugljesa Jovanovic * ugljesa.jovanovic@ionspin.com * on 21-Sep-2019 */ actual object SRNG { + @Suppress("EXPERIMENTAL_UNSIGNED_LITERALS") actual fun getRandomBytes(amount: Int): Array { - TODO("not implemented yet") + memScoped { + val array = allocArray(amount) + val urandomFile = fopen("/dev/urandom", "rb") + if (urandomFile != null) { + fread(array, 1, amount.convert(), urandomFile) + } + return Array(amount) { + array[it] + } + } } } \ No newline at end of file