Added "streaming" blake2b support
This commit is contained in:
parent
f17aa19d08
commit
1852db686c
@ -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
|
||||
|
@ -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<UByte>? = 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<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 {
|
||||
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<ULong>): Array<UByte> {
|
||||
return h.map {
|
||||
arrayOf(
|
||||
(it and 0xFFUL).toUByte(),
|
||||
@ -217,7 +223,7 @@ class Blake2b {
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
private inline fun padToBlock(unpadded: Array<UByte>): Array<UByte> {
|
||||
private fun padToBlock(unpadded: Array<UByte>): Array<UByte> {
|
||||
if (unpadded.size == BLOCK_BYTES) {
|
||||
return unpadded
|
||||
}
|
||||
@ -236,17 +242,106 @@ class Blake2b {
|
||||
}
|
||||
}
|
||||
|
||||
fun digest(inputString: String, key: String? = null): Array<UByte> {
|
||||
return Blake2b.digest(inputString, key)
|
||||
constructor(
|
||||
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 = "")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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 = "",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user