Skip to content

Commit

Permalink
Add SHA256 hash impl (#18)
Browse files Browse the repository at this point in the history
* sha 256 + tests

* test cleanup
  • Loading branch information
Funkatronics authored Jun 12, 2024
1 parent c642876 commit 544e6fb
Show file tree
Hide file tree
Showing 2 changed files with 308 additions and 0 deletions.
126 changes: 126 additions & 0 deletions src/commonMain/kotlin/com/funkatronics/hash/Sha256.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.funkatronics.hash

/**
* Kotlin implementation of SHA-256 hash algorithm.
* Original Java version at https://github.com/meyfa/java-sha256/blob/master/src/main/java/net/meyfa/sha256/Sha256.java
*
* @author Funkatronics
*/
object Sha256 {
private val K = intArrayOf(
0x428a2f98, 0x71374491, -0x4a3f0431, -0x164a245b, 0x3956c25b, 0x59f111f1, -0x6dc07d5c, -0x54e3a12b,
-0x27f85568, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, -0x7f214e02, -0x6423f959, -0x3e640e8c,
-0x1b64963f, -0x1041b87a, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
-0x67c1aeae, -0x57ce3993, -0x4ffcd838, -0x40a68039, -0x391ff40d, -0x2a586eb9, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, -0x7e3d36d2, -0x6d8dd37b,
-0x5d40175f, -0x57e599b5, -0x3db47490, -0x3893ae5d, -0x2e6d17e7, -0x2966f9dc, -0xbf1ca7b, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, -0x7b3787ec, -0x7338fdf8, -0x6f410006, -0x5baf9315, -0x41065c09, -0x398e870e
)

private val H0 = intArrayOf(
0x6a09e667, -0x4498517b, 0x3c6ef372, -0x5ab00ac6, 0x510e527f, -0x64fa9774, 0x1f83d9ab, 0x5be0cd19
)

private const val BLOCK_BITS = 512
private const val BLOCK_BYTES = BLOCK_BITS / 8

/**
* Hashes the given message with SHA-256 and returns the hash.
*
* @param message The bytes to hash.
* @return The hash's bytes.
*/
fun hash(message: ByteArray): ByteArray {
// working arrays
val W = IntArray(64)
val H = H0.copyOf()
val TEMP = IntArray(8)

// initialize all words
val words = pad(message)

// enumerate all blocks (each containing 16 words)
(0 until words.size/16).forEach { i ->
// initialize W from the block's words
words.copyInto(W, 0, i*16, i*16 + 16)
(16 until W.size).forEach { t ->
W[t] = smallSig1(W[t - 2]) + W[t - 7] + smallSig0(W[t - 15]) + W[t - 16]
}

// let TEMP = H
H.copyInto(TEMP)

// operate on TEMP
W.indices.forEach { t ->
val t1 = TEMP[7] + bigSig1(TEMP[4]) + ch(TEMP[4], TEMP[5], TEMP[6]) + K[t] + W[t]
val t2 = bigSig0(TEMP[0]) + maj(TEMP[0], TEMP[1], TEMP[2])
TEMP.copyInto(TEMP, 1, 0, TEMP.size - 1)
TEMP[4] += t1
TEMP[0] = t1 + t2
}

// add values in TEMP to values in H
H.indices.forEach { t ->
H[t] += TEMP[t]
}
}

return H.toByteArray()
}

/**
* **Internal method, no need to call.** Pads the given message to have a length
* that is a multiple of 512 bits (64 bytes), including the addition of a
* 1-bit, k 0-bits, and the message length as a 64-bit integer.
* The result is a 32-bit integer array with big-endian byte representation.
*
* @param message The message to pad.
* @return A new array with the padded message bytes.
*/
internal fun pad(message: ByteArray): IntArray {
// new message length: original + 1-bit and padding + 8-byte length
// --> block count: whole blocks + (padding + length rounded up)
val finalBlockLength = message.size % BLOCK_BYTES
val blockCount = message.size/BLOCK_BYTES + if (finalBlockLength + 1 + 8 > BLOCK_BYTES) 2 else 1

val result = IntArray(blockCount*(BLOCK_BYTES/Int.SIZE_BYTES))

// copy message and append 1 bit
(message + 128.toByte()).forEachIndexed { i, b ->
result[i/Int.SIZE_BYTES] = result[i/Int.SIZE_BYTES] or
((b.toInt() and 0xFF) shl 8 * (Int.SIZE_BYTES - 1 - i%Int.SIZE_BYTES))
}

// place original message length as 64-bit integer at the end
val msgLength = message.size * 8L
result[result.size - 2] = (msgLength ushr 32).toInt()
result[result.size - 1] = msgLength.toInt()

return result
}

private fun IntArray.toByteArray(): ByteArray =
ByteArray(size*Int.SIZE_BYTES) { i ->
(this[i/Int.SIZE_BYTES] shr (8*(Int.SIZE_BYTES - 1 - i%Int.SIZE_BYTES))).toByte()
}

private fun ch(x: Int, y: Int, z: Int): Int = (x and y) or (x.inv() and z)

private fun maj(x: Int, y: Int, z: Int): Int = (x and y) or (x and z) or (y and z)

private fun bigSig0(x: Int): Int =
(x rotateRight 2) xor (x rotateRight 13) xor (x rotateRight 22)

private fun bigSig1(x: Int): Int =
(x rotateRight 6) xor (x rotateRight 11) xor (x rotateRight 25)

private fun smallSig0(x: Int): Int =
(x rotateRight 7) xor (x rotateRight 18) xor (x ushr 3)

private fun smallSig1(x: Int): Int =
(x rotateRight 17) xor (x rotateRight 19) xor (x ushr 10)

private infix fun Int.rotateRight(distance: Int): Int =
this.ushr(distance) or (this shl -distance)
}
182 changes: 182 additions & 0 deletions src/commonTest/kotlin/com/funkatronics/hash/Sha256Tests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package com.funkatronics.hash

import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals

class Sha256Tests {

@Test
fun `empty hash`() {
// given
val message = ByteArray(0)
val expectedHash: ByteArray =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `RFC test vector 24 bit`() {
// given
val message = "abc".encodeToByteArray()
val expectedHash: ByteArray =
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `RFC test vector 448 bit`() {
// given
val message = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq".encodeToByteArray()
val expectedHash: ByteArray =
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `RFC test vector 896 bit`() {
// given
val message = ("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn" +
"hijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu").encodeToByteArray()
val expectedHash: ByteArray =
"cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `RFC test vector 1 million repetitions of character 'a'`() {
// given
val message = ByteArray(1000000) { 'a'.code.toByte() }
val expectedHash: ByteArray =
"cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `hello world hash`() {
// given
val message = "Hello world!".encodeToByteArray()
val expectedHash: ByteArray =
"c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `long hash`() {
// given
val message = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
+ "Proin pulvinar turpis purus, sit amet dapibus magna commodo "
+ "quis metus.").encodeToByteArray()
val expectedHash: ByteArray =
"60497604d2f6b4df42cea5efb8956f587f81a4ad66fa1b65d9e085224d255036".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `hash raw bytes`() {
// given
val message = ByteArray(256) { it.toByte() }
val expectedHash: ByteArray =
"40aff2e9d2d8922e47afd4648e6967497158785fbd1da870e7110266bf944880".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `hash 55`() {
// given
val message = ByteArray(55) { 'a'.code.toByte() }
val expectedHash: ByteArray =
"9f4390f8d30c2dd92ec9f095b65e2b9ae9b0a925a5258e241c9f1e910f734318".decodeHex()

// when
val actualHash = Sha256.hash(message)

// then
assertContentEquals(expectedHash, actualHash)
}

@Test
fun `padded message length divisible by 512`() {
(0..128).forEach { length ->
// given
val b = ByteArray(length)

// when
val padded = Sha256.pad(b)
val paddedLengthBits: Int = padded.size * Int.SIZE_BYTES * 8

// then
assertEquals(0, paddedLengthBits % 512)
}
}

@Test
fun `padded message has 1 bit`() {
// given
val message = ByteArray(64)
val expectedInt = 0b1000_0000_0000_0000_0000_0000_0000_0000.toInt()

// when
val padded = Sha256.pad(message)

// then
assertEquals(expectedInt, padded[message.size / Int.SIZE_BYTES])
}

@Test
fun `pad message length and original message size`() {
// given
val message = "hello".encodeToByteArray()
val originalMessageSizeBits = message.size*8

// when
val padded = Sha256.pad(message)

// then
assertEquals(0, (padded.size * Int.SIZE_BYTES * 8) % 512)
assertEquals(originalMessageSizeBits, padded[padded.size - 2] or padded.last())
}

private fun String.decodeHex(): ByteArray =
chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}

0 comments on commit 544e6fb

Please sign in to comment.