diff --git a/crypto-core/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Hash.kt b/crypto-core/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Hash.kt new file mode 100644 index 0000000..7a5bf09 --- /dev/null +++ b/crypto-core/src/commonMain/kotlin/com/ionspin/kotlin/crypto/Hash.kt @@ -0,0 +1,27 @@ +/* + * 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 + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 20-Jul-2019 + */ +interface Hash + +interface StreamingHash : Hash + diff --git a/crypto-core/src/commonMain/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2b.kt b/crypto-core/src/commonMain/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2b.kt index 69fe4cd..2ef96af 100644 --- a/crypto-core/src/commonMain/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2b.kt +++ b/crypto-core/src/commonMain/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2b.kt @@ -18,8 +18,12 @@ package com.ionspin.kotlin.crypto.blake2b import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.toBigInteger -import com.ionspin.kotlin.crypto.hexColumsPrint -import com.ionspin.kotlin.crypto.rotateRight +import com.ionspin.kotlin.crypto.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlin.text.chunked /** * Created by Ugljesa Jovanovic @@ -28,8 +32,8 @@ import com.ionspin.kotlin.crypto.rotateRight */ @ExperimentalStdlibApi @ExperimentalUnsignedTypes -class Blake2b { - companion object { +class Blake2b(val key: Array? = null, val hashLength: Int = 64) : StreamingHash { + companion object : Hash { const val BITS_IN_WORD = 64 const val ROUNDS_IN_COMPRESS = 12 @@ -143,9 +147,9 @@ class Blake2b { } - fun digest(inputString: String, key: String? = null): Array { - val chunked = inputString.encodeToByteArray().map {it.toUByte() }.toList().chunked(BLOCK_BYTES).map { it.toTypedArray() }.toTypedArray() + val chunked = inputString.encodeToByteArray().map { it.toUByte() }.toList().chunked(BLOCK_BYTES) + .map { it.toTypedArray() }.toTypedArray() val keyBytes = key?.run { encodeToByteArray().map { it.toUByte() }.toTypedArray() } ?: emptyArray() @@ -163,7 +167,6 @@ class Blake2b { h[0] = h[0] xor 0x01010000UL xor (secretKey.size.toULong() shl 8) xor hashLength.toULong() - val message = if (secretKey.isEmpty()) { if (inputMessage.isEmpty()) { Array(1) { @@ -181,7 +184,6 @@ class Blake2b { if (message.size > 1) { for (i in 0 until message.size - 1) { compress(h, message[i], ((i + 1) * BLOCK_BYTES).toBigInteger(), false).copyInto(h) - h.hexColumsPrint() } } @@ -201,6 +203,10 @@ class Blake2b { compress(h, lastBlockPadded, lastSize.toBigInteger(), true).copyInto(h) + return formatResult(h) + } + + private fun formatResult(h: Array): Array { return h.map { arrayOf( (it and 0xFFUL).toUByte(), @@ -217,7 +223,7 @@ class Blake2b { }.toTypedArray() } - private inline fun padToBlock(unpadded: Array): Array { + private fun padToBlock(unpadded: Array): Array { if (unpadded.size == BLOCK_BYTES) { return unpadded } @@ -236,17 +242,106 @@ class Blake2b { } } - fun digest(inputString: String, key: String? = null): Array { - return Blake2b.digest(inputString, key) + constructor( + key: String?, + requestedHashLenght: Int = 64 + ) : this( + (key?.encodeToByteArray()?.map { it.toUByte() }?.toTypedArray() ?: emptyArray()), + requestedHashLenght + ) + + val job = Job() + val scope = CoroutineScope(Dispatchers.Default + job) + + + var h = iv.copyOf() + var counter = BigInteger.ZERO + var bufferCounter = 0 + var buffer = Array(BLOCK_BYTES) { 0U } + + + init { + h[0] = h[0] xor 0x01010000UL xor (key?.run { size.toULong() shl 8 } ?: 0UL) xor hashLength.toULong() + + if (!key.isNullOrEmpty()) { + appendToBuffer(padToBlock(key), bufferCounter) + } + } + + fun updateBlocking(array: Array) { + if (array.isEmpty()) { + throw RuntimeException("Updating with empty array is not allowed. If you need empty hash, just call digest without updating") + } + + when { + bufferCounter + array.size < BLOCK_BYTES -> appendToBuffer(array, bufferCounter) + bufferCounter + array.size >= BLOCK_BYTES -> { + val chunked = array.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) + } + } + + } + } + + } + + fun updateBlocking(input: String) { + updateBlocking(input.encodeToByteArray().map { it.toUByte() }.toTypedArray()) + } + + private fun appendToBuffer(array: Array, start: Int) { + array.copyInto(destination = buffer, destinationOffset = start, startIndex = 0, endIndex = array.size) + bufferCounter += array.size + } + + private fun consumeBlock(block: Array) { + h = compress(h, block, counter, false) + } + + suspend fun update(array: Array) { + + } + fun digest(): Array { + val lastBlockPadded = padToBlock(buffer) + counter += bufferCounter + compress(h, lastBlockPadded, counter, true) + val result = formatResult(h) + println(result.map { it.toString(16) }.joinToString(separator = "")) + return result + } - - - + fun digestString(): String { + return digest().map { it.toString(16) }.joinToString(separator = "") + } } diff --git a/crypto-core/src/commonTest/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2bKnowAnswerTests.kt b/crypto-core/src/commonTest/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2bKnowAnswerTests.kt index 761f2a7..58f2c4c 100644 --- a/crypto-core/src/commonTest/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2bKnowAnswerTests.kt +++ b/crypto-core/src/commonTest/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2bKnowAnswerTests.kt @@ -48,6 +48,21 @@ class Blake2bKnowAnswerTests { } } + @Test + fun knownAnswerTestStreaming() { + + kat.forEach { kat -> + val parsedInput = kat.input.chunked(2).map { it.toUByte(16) }.toTypedArray() + val chunkedInput = parsedInput.toList().chunked(128).map { it.toTypedArray() }.toTypedArray() + val blake2b = Blake2b(key = kat.key.chunked(2).map { it.toUByte(16) }.toTypedArray()) + chunkedInput.forEach { blake2b.updateBlocking(it) } + val result = blake2b.digest() + assertTrue("KAT ${kat.input} \nkey: ${kat.key} \nexpected: {${kat.hash}") { + result.contentEquals(kat.hash.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } + } + val kat = arrayOf( KnownAnswerTest( input = "", diff --git a/crypto-core/src/commonTest/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2bStreaming.kt b/crypto-core/src/commonTest/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2bStreaming.kt new file mode 100644 index 0000000..b9874b6 --- /dev/null +++ b/crypto-core/src/commonTest/kotlin/com/ionspin/kotlin/crypto/blake2b/Blake2bStreaming.kt @@ -0,0 +1,96 @@ +/* + * 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.blake2b + +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 20-Jul-2019 + */ +@ExperimentalUnsignedTypes +@ExperimentalStdlibApi +class Blake2bStreaming { + + @Test + fun testStreamingBlake2b() { + val updates = 14 + val input = "1234567890" + val expectedResult = arrayOf( + //@formatter:off + 0x2fU, 0x49U, 0xaeU, 0xb6U, 0x13U, 0xe3U, 0x4eU, 0x92U, 0x4eU, 0x17U, 0x5aU, 0x6aU, 0xf2U, 0xfaU, 0xadU, + 0x7bU, 0xc7U, 0x82U, 0x35U, 0xf9U, 0xc5U, 0xe4U, 0x61U, 0xc6U, 0x8fU, 0xd5U, 0xb4U, 0x07U, 0xeeU, 0x8eU, + 0x2fU, 0x0dU, 0x2fU, 0xb4U, 0xc0U, 0x7dU, 0x7eU, 0x4aU, 0x72U, 0x40U, 0x46U, 0x12U, 0xd9U, 0x28U, 0x99U, + 0xafU, 0x8aU, 0x32U, 0x8fU, 0x3bU, 0x61U, 0x4eU, 0xd7U, 0x72U, 0x44U, 0xb4U, 0x81U, 0x15U, 0x1dU, 0x40U, + 0xb1U, 0x1eU, 0x32U, 0xa4U + //@formatter:on + ) + + val blake2b = Blake2b() + for (i in 0 until updates) { + blake2b.updateBlocking(input) + } + val result = blake2b.digest() + + assertTrue { + result.contentEquals(expectedResult) + } + } + + @Test + fun testDigestToString() { + val updates = 14 + val input = "1234567890" + val expectedResult = "2F49AEB613E34E924E175A6AF2FAAD7BC78235F9C5E461C68FD5B47E".toLowerCase() + + "E8E2FD2FB4C07D7E4A72404612D92899AF8A328F3B614ED77244B481151D40B11E32A4".toLowerCase() + + val blake2b = Blake2b() + for (i in 0 until updates) { + blake2b.updateBlocking(input) + } + val result = blake2b.digestString() + assertTrue { + result == expectedResult + } + } + + @Test + fun testDigestWithKey() { + val test = "abc" + val key = "key" + val blake2b = Blake2b(key) + blake2b.updateBlocking(test) + val result = blake2b.digest() + val printout = result.map { it.toString(16) }.chunked(16) + printout.forEach { println(it.joinToString(separator = " ") { it.toUpperCase() }) } + + + + assertTrue { + result.isNotEmpty() + } + val expectedResult = ("5c6a9a4ae911c02fb7e71a991eb9aea371ae993d4842d206e" + + "6020d46f5e41358c6d5c277c110ef86c959ed63e6ecaaaceaaff38019a43264ae06acf73b9550b1") + .chunked(2).map { it.toUByte(16) }.toTypedArray() + + assertTrue { + result.contentEquals(expectedResult) + } + } +} \ No newline at end of file