diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/Aes.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/Aes.kt index 427cc74..b775a68 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/Aes.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/Aes.kt @@ -84,7 +84,8 @@ internal class Aes internal constructor(val aesKey: AesKey, val input: Array @@ -247,11 +248,16 @@ internal class Aes internal constructor(val aesKey: AesKey, val input: Array it xor temp[index] }.toTypedArray() + clearArray(temp) } return expandedKey } fun encrypt(): Array { + if (completed) { + throw RuntimeException("Encrypt can only be called once per Aes instance, since the state is cleared at the " + + "end of the operation") + } printState() addRoundKey() printState() @@ -280,11 +286,16 @@ internal class Aes internal constructor(val aesKey: AesKey, val input: Array { + if (completed) { + throw RuntimeException("Decrypt can only be called once per Aes instance, since the state is cleared at the " + + "end of the operation") + } round = numberOfRounds printState() inverseAddRoundKey() @@ -316,9 +327,15 @@ internal class Aes internal constructor(val aesKey: AesKey, val input: Array) { + array.indices.forEach { array[it] = 0U } + } + private fun printState() { 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 33663a5..7369b94 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 @@ -125,10 +125,20 @@ class AesCbc internal constructor(val aesKey: AesKey, val mode: Mode, initializa fun decrypt(): Array { val removePaddingCount = output.last().last() - val removedPadding = output.last().dropLast(removePaddingCount.toInt() and 0x7F) - val preparedOutput = output.dropLast(1).toTypedArray() + removedPadding.toTypedArray() - return preparedOutput.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes } + + val removedPadding = if (removePaddingCount > 0U && removePaddingCount < 16U) { + output.last().dropLast(removePaddingCount.toInt() and 0x7F) + } else { + output.last().toList() + } + 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 } + return folded + } private fun appendToBuffer(array: Array, start: Int) { 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 new file mode 100644 index 0000000..30806c1 --- /dev/null +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtr.kt @@ -0,0 +1,162 @@ +/* + * 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.bignum.Endianness +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.xor + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 22-Sep-2019 + */ +@ExperimentalUnsignedTypes +class AesCtr internal constructor(val aesKey: AesKey, val mode: Mode, initialCounter: Array? = null) { + + 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() + } + + 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() + var previousEncrypted: Array = arrayOf() + val counterStart = initialCounter ?: SRNG.getRandomBytes(16) + var blockCounter = modularCreator.fromBigInteger(BigInteger.fromUByteArray(counterStart, Endianness.BIG)) + + val output = MutableList>(0) { arrayOf() } + + 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, blockCounter) + blockCounter += 1 + 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) + } + } + + } + } + + } + + 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], blockCounter) + blockCounter += 1 + output += consumeBlock(chunks[1], blockCounter) + } else { + output += consumeBlock(lastBlockPadded, blockCounter) + } + } + return output.reversed().foldRight(Array(0) { 0U }) { arrayOfUBytes, acc -> acc + arrayOfUBytes } + } + + fun decrypt(): Array { + val removePaddingCount = output.last().last() + val removedPadding = output.last().dropLast(removePaddingCount.toInt() and 0x7F) + 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 } + return folded + } + + private fun appendToBuffer(array: Array, start: Int) { + array.copyInto(destination = buffer, destinationOffset = start, startIndex = 0, endIndex = array.size) + bufferCounter += array.size + } + + private fun consumeBlock(data: Array, blockCount: ModularBigInteger): Array { + return when (mode) { + Mode.ENCRYPT -> { + Aes.encrypt(aesKey, blockCount.toUByteArray(Endianness.BIG)) xor data + } + Mode.DECRYPT -> { + Aes.decrypt(aesKey, blockCount.toUByteArray(Endianness.BIG)) xor data + } + } + + } + +} \ 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 new file mode 100644 index 0000000..6f80b71 --- /dev/null +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/symmetric/AesCtrTest.kt @@ -0,0 +1,75 @@ +/* + * 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 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() + } + + } + + @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 { + 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 36cd9e2..c1db5e7 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 @@ -23,7 +23,14 @@ package com.ionspin.kotlin.crypto */ actual object SRNG { var counter = 0 + @ExperimentalUnsignedTypes actual fun getRandomBytes(amount: Int): Array { +// val runningOnNode = js("(typeof window === 'undefined')").unsafeCast() +// if (runningOnNode) { +// js("var crypto = require('crypto')").asDynamic().randomBytes(amount) +// } else { +// throw RuntimeException("Secure random not supported yet for non-nodejs environment") +// } return arrayOf((counter++).toUByte()) // TODO Wow. Such random. Very entropy. } } \ No newline at end of file