From 995cb23176a748a0308464b9ef89c32c303fcbe9 Mon Sep 17 00:00:00 2001 From: Ugljesa Jovanovic Date: Sun, 21 Jul 2019 00:55:11 +0200 Subject: [PATCH] Tests cleanup --- .../com/ionspin/kotlin/crypto/sha/Sha512.kt | 265 ++++++++++++------ .../ionspin/kotlin/crypto/sha/Sha256Test.kt | 27 ++ .../kotlin/crypto/sha/Sha256UpdateableTest.kt | 47 ++++ .../ionspin/kotlin/crypto/sha/Sha512Test.kt | 10 +- .../kotlin/crypto/sha/Sha512UpdateableTest.kt | 89 ++++++ 5 files changed, 345 insertions(+), 93 deletions(-) create mode 100644 multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512UpdateableTest.kt diff --git a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/sha/Sha512.kt b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/sha/Sha512.kt index 80dfcb7..9b5fffc 100644 --- a/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/sha/Sha512.kt +++ b/multiplatform-crypto/src/commonMain/kotlin/com/ionspin/kotlin/crypto/sha/Sha512.kt @@ -30,6 +30,8 @@ import com.ionspin.kotlin.crypto.rotateRight class Sha512 : Hash { companion object { const val BLOCK_SIZE = 1024 + const val BLOCK_SIZE_IN_BYTES = 128 + const val CHUNK_SIZE = 80 const val ULONG_MASK = 0xFFFFFFFFFFFFFFFFUL val k = arrayOf( @@ -133,102 +135,29 @@ class Sha512 : Hash { fun digest(message: Array): Array { - var h0 = 0x6a09e667f3bcc908UL - var h1 = 0xbb67ae8584caa73bUL - var h2 = 0x3c6ef372fe94f82bUL - var h3 = 0xa54ff53a5f1d36f1UL - var h4 = 0x510e527fade682d1UL - var h5 = 0x9b05688c2b3e6c1fUL - var h6 = 0x1f83d9abfb41bd6bUL - var h7 = 0x5be0cd19137e2179UL + var h = iv.copyOf() - val originalMessageSizeInBits = message.size * 8 - - - //K such that L + 1 + K + 64 is a multiple of 512 - val expandedRemainderOf1024 = (originalMessageSizeInBits + 130) % BLOCK_SIZE - val zeroAddAmount = when (expandedRemainderOf1024) { - 0 -> 0 - else -> (BLOCK_SIZE - expandedRemainderOf1024) / 8 - } - val expansionArray = Array(zeroAddAmount + 1) { - when (it) { - 0 -> 0b10000000U - else -> 0U - } - } + val expansionArray = createExpansionArray(message.size) val chunks = - (message + expansionArray + originalMessageSizeInBits.toULong().toPadded128BitByteArray()).chunked(128) + (message + expansionArray + (message.size * 8).toULong().toPadded128BitByteArray()).chunked( + BLOCK_SIZE_IN_BYTES) chunks.forEach { chunk -> - val w = Array(80) { - when (it) { - in 0 until 16 -> { - var collected = (chunk[(it * 8)].toULong() shl 56) + - (chunk[(it * 8) + 1].toULong() shl 48) + - (chunk[(it * 8) + 2].toULong() shl 40) + - (chunk[(it * 8) + 3].toULong() shl 32) + - (chunk[(it * 8) + 4].toULong() shl 24) + - (chunk[(it * 8) + 5].toULong() shl 16) + - (chunk[(it * 8) + 6].toULong() shl 8) + - (chunk[(it * 8) + 7].toULong()) - collected - } - else -> 0UL - } - } - for (i in 16 until 80) { - val s0 = scheduleSigma0(w[i - 15]) - val s1 = scheduleSigma1(w[i - 2]) - w[i] = w[i - 16] + s0 + w[i - 7] + s1 - } - - var a = h0 - var b = h1 - var c = h2 - var d = h3 - var e = h4 - var f = h5 - var g = h6 - var h = h7 - - for (i in 0 until 80) { - val s1 = compressionSigma1(e) - val ch = ch(e, f, g) - val temp1 = h + s1 + ch + k[i] + w[i] - val s0 = compressionSigma0(a) - val maj = maj(a, b, c) - val temp2 = s0 + maj - h = g - g = f - f = e - e = d + temp1 - d = c - c = b - b = a - a = temp1 + temp2 - } - - h0 += a - h1 += b - h2 += c - h3 += d - h4 += e - h5 += f - h6 += g - h7 += h + val w = expandChunk(chunk) + mix(h, w) } - val digest = h0.toPaddedByteArray() + - h1.toPaddedByteArray() + - h2.toPaddedByteArray() + - h3.toPaddedByteArray() + - h4.toPaddedByteArray() + - h5.toPaddedByteArray() + - h6.toPaddedByteArray() + - h7.toPaddedByteArray() + val digest = + h[0].toPaddedByteArray() + + h[1].toPaddedByteArray() + + h[2].toPaddedByteArray() + + h[3].toPaddedByteArray() + + h[4].toPaddedByteArray() + + h[5].toPaddedByteArray() + + h[6].toPaddedByteArray() + + h[7].toPaddedByteArray() return digest } @@ -256,6 +185,86 @@ class Sha512 : Hash { return ((x and y) xor (x and z) xor (y and z)) } + private fun expandChunk(chunk: Array): Array { + val w = Array(CHUNK_SIZE) { + when (it) { + in 0 until 16 -> { + var collected = (chunk[(it * 8)].toULong() shl 56) + + (chunk[(it * 8) + 1].toULong() shl 48) + + (chunk[(it * 8) + 2].toULong() shl 40) + + (chunk[(it * 8) + 3].toULong() shl 32) + + (chunk[(it * 8) + 4].toULong() shl 24) + + (chunk[(it * 8) + 5].toULong() shl 16) + + (chunk[(it * 8) + 6].toULong() shl 8) + + (chunk[(it * 8) + 7].toULong()) + collected + } + else -> 0UL + } + } + for (i in 16 until CHUNK_SIZE) { + val s0 = scheduleSigma0(w[i - 15]) + val s1 = scheduleSigma1(w[i - 2]) + w[i] = w[i - 16] + s0 + w[i - 7] + s1 + } + return w + } + + private fun mix(h: Array, w: Array): Array { + var paramA = h[0] + var paramB = h[1] + var paramC = h[2] + var paramD = h[3] + var paramE = h[4] + var paramF = h[5] + var paramG = h[6] + var paramH = h[7] + + for (i in 0 until CHUNK_SIZE) { + val s1 = compressionSigma1(paramE) + val ch = ch(paramE, paramF, paramG) + val temp1 = paramH + s1 + ch + k[i] + w[i] + val s0 = compressionSigma0(paramA) + val maj = maj(paramA, paramB, paramC) + val temp2 = s0 + maj + paramH = paramG + paramG = paramF + paramF = paramE + paramE = paramD + temp1 + paramD = paramC + paramC = paramB + paramB = paramA + paramA = temp1 + temp2 + } + + h[0] += paramA + h[1] += paramB + h[2] += paramC + h[3] += paramD + h[4] += paramE + h[5] += paramF + h[6] += paramG + h[7] += paramH + return h + } + + fun createExpansionArray(originalSizeInBytes : Int) : Array { + val originalMessageSizeInBits = originalSizeInBytes * 8 + + val expandedRemainderOf1024 = (originalMessageSizeInBits + 129) % BLOCK_SIZE + val zeroAddAmount = when (expandedRemainderOf1024) { + 0 -> 0 + else -> (BLOCK_SIZE - expandedRemainderOf1024) / 8 + } + val expansionArray = Array(zeroAddAmount + 1) { + when (it) { + 0 -> 0b10000000U + else -> 0U + } + } + return expansionArray + } + private fun ULong.toPaddedByteArray(): Array { val byteMask = 0xFFUL @@ -294,5 +303,85 @@ class Sha512 : Hash { } } + var h = iv.copyOf() + var counter = 0 + var bufferCounter = 0 + var buffer = Array(BLOCK_SIZE_IN_BYTES) { 0U } + + @ExperimentalStdlibApi + fun update(message: String) { + return update(message.encodeToByteArray().map { it.toUByte() }.toTypedArray()) + } + + fun update(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_SIZE_IN_BYTES -> appendToBuffer(array, bufferCounter) + bufferCounter + array.size >= BLOCK_SIZE_IN_BYTES -> { + val chunked = array.chunked(BLOCK_SIZE_IN_BYTES) + chunked.forEach { chunk -> + if (bufferCounter + chunk.size < BLOCK_SIZE_IN_BYTES) { + appendToBuffer(chunk, bufferCounter) + } else { + chunk.copyInto( + destination = buffer, + destinationOffset = bufferCounter, + startIndex = 0, + endIndex = BLOCK_SIZE_IN_BYTES - bufferCounter + ) + counter += BLOCK_SIZE_IN_BYTES + consumeBlock(buffer) + buffer = Array(BLOCK_SIZE_IN_BYTES) { + when (it) { + in (0 until (chunk.size - (BLOCK_SIZE_IN_BYTES - bufferCounter))) -> { + chunk[it + (BLOCK_SIZE_IN_BYTES - bufferCounter)] + } + else -> { + 0U + } + } + + } + bufferCounter = chunk.size - (BLOCK_SIZE_IN_BYTES - bufferCounter) + } + } + + } + } + } + + private fun consumeBlock(block: Array) { + val w = expandChunk(block) + mix(h, w).copyInto(h) + } + + fun digest() : Array { + val length = counter + bufferCounter + val expansionArray = createExpansionArray(length) + val finalBlock = buffer.copyOfRange(0, bufferCounter) + expansionArray + (length * 8).toULong().toPadded128BitByteArray() + finalBlock.chunked(BLOCK_SIZE_IN_BYTES).forEach { + consumeBlock(it) + } + + + val digest = h[0].toPaddedByteArray() + + h[1].toPaddedByteArray() + + h[2].toPaddedByteArray() + + h[3].toPaddedByteArray() + + h[4].toPaddedByteArray() + + h[5].toPaddedByteArray() + + h[6].toPaddedByteArray() + + h[7].toPaddedByteArray() + return digest + } + + private fun appendToBuffer(array: Array, start: Int) { + array.copyInto(destination = buffer, destinationOffset = start, startIndex = 0, endIndex = array.size) + bufferCounter += array.size + } + } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256Test.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256Test.kt index 57507bd..505d05f 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256Test.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256Test.kt @@ -50,4 +50,31 @@ class Sha256Test { resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) } } + + @ExperimentalStdlibApi + @Test + fun testWellKnown3() { //It's good that I'm consistent with names. + + + val resultDoubleBlock = Sha256.digest(message = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") + println(resultDoubleBlock.map{ it.toString(16)}.joinToString(separator = "")) + val expectedResultForDoubleBlock = "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } + + @ExperimentalStdlibApi + @Test + fun testWellKnownLong() { + val inputBuilder = StringBuilder() + for (i in 0 until 1000000) { + inputBuilder.append("a") + } + val resultDoubleBlock = Sha256.digest(message = (inputBuilder.toString()).encodeToByteArray().map { it.toUByte() }.toTypedArray()) + val expectedResultForDoubleBlock = "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256UpdateableTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256UpdateableTest.kt index 01a0dae..9ba98cb 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256UpdateableTest.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha256UpdateableTest.kt @@ -17,6 +17,7 @@ package com.ionspin.kotlin.crypto.sha import com.ionspin.kotlin.crypto.chunked +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertTrue @@ -54,4 +55,50 @@ class Sha256UpdateableTest { resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) } } + + @ExperimentalStdlibApi + @Test + fun testWellKnown3() { //It's good that I'm consistent with names. + val sha256 = Sha256() + sha256.update(message = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") + val resultDoubleBlock = sha256.digest() + println(resultDoubleBlock.map{ it.toString(16)}.joinToString(separator = "")) + val expectedResultForDoubleBlock = "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } + + @ExperimentalStdlibApi + @Test + fun testWellKnownLong() { + val sha256 = Sha256() + for (i in 0 until 10000) { + sha256.update("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + } + val resultDoubleBlock = sha256.digest() + val expectedResultForDoubleBlock = "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } + + @Ignore() + @ExperimentalStdlibApi + @Test + fun testWellKnownLonger() { + val sha256 = Sha256() + for (i in 0 until 16_777_216) { + if (i % 10000 == 0) { + println("$i/16777216") + } + sha256.update("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmno") + } + val resultDoubleBlock = sha256.digest() + val expectedResultForDoubleBlock = "50e72a0e26442fe2552dc3938ac58658228c0cbfb1d2ca872ae435266fcd055e" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } + //50e72a0e 26442fe2 552dc393 8ac58658 228c0cbf b1d2ca87 2ae43526 6fcd055e } \ No newline at end of file diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512Test.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512Test.kt index 5a375e3..37d5c07 100644 --- a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512Test.kt +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512Test.kt @@ -44,12 +44,12 @@ class Sha512Test { @ExperimentalUnsignedTypes @ExperimentalStdlibApi @Test - fun testWellKnownDoubleBlock() { - - val resultDoubleBlock = Sha512.digest(message = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn" + - "hijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") + fun testWellKnown3() { + val sha512 = Sha512() + sha512.update(message = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") + val resultDoubleBlock = sha512.digest() val expectedResultForDoubleBlock = "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018" + - "501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909" + "501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909" assertTrue { resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) } diff --git a/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512UpdateableTest.kt b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512UpdateableTest.kt new file mode 100644 index 0000000..bdcf195 --- /dev/null +++ b/multiplatform-crypto/src/commonTest/kotlin/com/ionspin/kotlin/crypto/sha/Sha512UpdateableTest.kt @@ -0,0 +1,89 @@ +/* + * 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.sha + +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Created by Ugljesa Jovanovic + * ugljesa.jovanovic@ionspin.com + * on 21-Jul-2019 + */ +class Sha512UpdateableTest { + @ExperimentalStdlibApi + @Test + fun testWellKnownValue() { + val sha512 = Sha512() + sha512.update("abc") + val result = sha512.digest() + val expectedResult = "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a" + + "2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f" + assertTrue { + result.contentEquals(expectedResult.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + + + } + + @ExperimentalStdlibApi + @Test + fun testWellKnownDoubleBlock() { + val sha512 = Sha512() + sha512.update(message = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") + val resultDoubleBlock = sha512.digest() + println(resultDoubleBlock.map{ it.toString(16)}.joinToString(separator = "")) + val expectedResultForDoubleBlock = "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018" + + "501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } + + @ExperimentalStdlibApi + @Test + fun testWellKnownLong() { + val sha512 = Sha512() + for (i in 0 until 10000) { + sha512.update("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + } + val resultDoubleBlock = sha512.digest() + val expectedResultForDoubleBlock = "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } + + @Ignore() //Interestingly enough I'm not the only one having trouble with this test. + @ExperimentalStdlibApi + @Test + fun testWellKnownLonger() { + val sha512 = Sha512() + for (i in 0 until 16_777_216) { + if (i % 10000 == 0) { + println("$i/16777216") + } + sha512.update("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmno") + } + val resultDoubleBlock = sha512.digest() + val expectedResultForDoubleBlock = "b47c933421ea2db149ad6e10fce6c7f93d0752380180ffd7f4629a712134831d77be6091b819ed352c2967a2e2d4fa5050723c9630691f1a05a7281dbe6c1086" + assertTrue { + resultDoubleBlock.contentEquals(expectedResultForDoubleBlock.chunked(2).map { it.toUByte(16) }.toTypedArray()) + } + } +} \ No newline at end of file