Added "streaming" blake2b support

This commit is contained in:
Ugljesa Jovanovic 2019-07-20 17:55:49 +02:00
parent f17aa19d08
commit 1852db686c
No known key found for this signature in database
GPG Key ID: 46D004C9820EBB98
4 changed files with 247 additions and 14 deletions

View File

@ -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

View File

@ -18,8 +18,12 @@ package com.ionspin.kotlin.crypto.blake2b
import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.BigInteger
import com.ionspin.kotlin.bignum.integer.toBigInteger import com.ionspin.kotlin.bignum.integer.toBigInteger
import com.ionspin.kotlin.crypto.hexColumsPrint import com.ionspin.kotlin.crypto.*
import com.ionspin.kotlin.crypto.rotateRight import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlin.text.chunked
/** /**
* Created by Ugljesa Jovanovic * Created by Ugljesa Jovanovic
@ -28,8 +32,8 @@ import com.ionspin.kotlin.crypto.rotateRight
*/ */
@ExperimentalStdlibApi @ExperimentalStdlibApi
@ExperimentalUnsignedTypes @ExperimentalUnsignedTypes
class Blake2b { class Blake2b(val key: Array<UByte>? = null, val hashLength: Int = 64) : StreamingHash {
companion object { companion object : Hash {
const val BITS_IN_WORD = 64 const val BITS_IN_WORD = 64
const val ROUNDS_IN_COMPRESS = 12 const val ROUNDS_IN_COMPRESS = 12
@ -143,9 +147,9 @@ class Blake2b {
} }
fun digest(inputString: String, key: String? = null): Array<UByte> { fun digest(inputString: String, key: String? = null): Array<UByte> {
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 { val keyBytes = key?.run {
encodeToByteArray().map { it.toUByte() }.toTypedArray() encodeToByteArray().map { it.toUByte() }.toTypedArray()
} ?: emptyArray() } ?: emptyArray()
@ -163,7 +167,6 @@ class Blake2b {
h[0] = h[0] xor 0x01010000UL xor (secretKey.size.toULong() shl 8) xor hashLength.toULong() h[0] = h[0] xor 0x01010000UL xor (secretKey.size.toULong() shl 8) xor hashLength.toULong()
val message = if (secretKey.isEmpty()) { val message = if (secretKey.isEmpty()) {
if (inputMessage.isEmpty()) { if (inputMessage.isEmpty()) {
Array(1) { Array(1) {
@ -181,7 +184,6 @@ class Blake2b {
if (message.size > 1) { if (message.size > 1) {
for (i in 0 until message.size - 1) { for (i in 0 until message.size - 1) {
compress(h, message[i], ((i + 1) * BLOCK_BYTES).toBigInteger(), false).copyInto(h) 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) compress(h, lastBlockPadded, lastSize.toBigInteger(), true).copyInto(h)
return formatResult(h)
}
private fun formatResult(h: Array<ULong>): Array<UByte> {
return h.map { return h.map {
arrayOf( arrayOf(
(it and 0xFFUL).toUByte(), (it and 0xFFUL).toUByte(),
@ -217,7 +223,7 @@ class Blake2b {
}.toTypedArray() }.toTypedArray()
} }
private inline fun padToBlock(unpadded: Array<UByte>): Array<UByte> { private fun padToBlock(unpadded: Array<UByte>): Array<UByte> {
if (unpadded.size == BLOCK_BYTES) { if (unpadded.size == BLOCK_BYTES) {
return unpadded return unpadded
} }
@ -236,17 +242,106 @@ class Blake2b {
} }
} }
fun digest(inputString: String, key: String? = null): Array<UByte> { constructor(
return Blake2b.digest(inputString, key) key: String?,
requestedHashLenght: Int = 64
) : this(
(key?.encodeToByteArray()?.map { it.toUByte() }?.toTypedArray() ?: emptyArray<UByte>()),
requestedHashLenght
)
val job = Job()
val scope = CoroutineScope(Dispatchers.Default + job)
var h = iv.copyOf()
var counter = BigInteger.ZERO
var bufferCounter = 0
var buffer = Array<UByte>(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<UByte>) {
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<UByte>(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<UByte>, start: Int) {
array.copyInto(destination = buffer, destinationOffset = start, startIndex = 0, endIndex = array.size)
bufferCounter += array.size
}
private fun consumeBlock(block: Array<UByte>) {
h = compress(h, block, counter, false)
}
suspend fun update(array: Array<UByte>) {
} }
fun digest(): Array<UByte> {
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 = "")
}
} }

View File

@ -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( val kat = arrayOf(
KnownAnswerTest( KnownAnswerTest(
input = "", input = "",

View File

@ -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<UByte>(
//@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)
}
}
}