Compare commits

..

3 Commits

Author SHA1 Message Date
26cff5d331 implement tests 2025-12-12 16:11:08 +01:00
8f21a346db implement common interface 2025-12-12 16:10:57 +01:00
6dec2fea74 upgrade dependencies 2025-12-12 16:06:26 +01:00
5 changed files with 179 additions and 30 deletions

View File

@@ -24,6 +24,13 @@ kotlin {
implementation(libs.hash) implementation(libs.hash)
implementation(libs.bytearray) implementation(libs.bytearray)
} }
commonTest.dependencies {
implementation(libs.test)
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.ExperimentalUnsignedTypes")
} }
} }

View File

@@ -1,8 +1,8 @@
[versions] [versions]
kotlin = "2.2.10" kotlin = "2.2.21"
mac = "1.1.2" mac = "1.2.0"
hash = "1.2.2" hash = "1.3.2"
bytearray = "1.1.1" bytearray = "1.2.4"
[plugins] [plugins]
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
@@ -11,3 +11,4 @@ multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotl
mac = { module = "com.infendro:mac", version.ref = "mac" } mac = { module = "com.infendro:mac", version.ref = "mac" }
hash = { module = "com.infendro:hash", version.ref = "hash" } hash = { module = "com.infendro:hash", version.ref = "hash" }
bytearray = { module = "com.infendro:bytearray", version.ref = "bytearray" } bytearray = { module = "com.infendro:bytearray", version.ref = "bytearray" }
test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }

View File

@@ -0,0 +1,12 @@
package com.infendro.kdf
interface KDF {
val bytes: Int
val bits: Int
get() = bytes * 8
fun hash(
value: UByteArray,
salt: UByteArray,
): UByteArray
}

View File

@@ -1,39 +1,49 @@
package com.infendro.kdf package com.infendro.kdf
import com.infendro.bytearray.addAll import com.infendro.bytearray.toUByteArray
import com.infendro.bytearray.toByteArray
import com.infendro.bytearray.xor import com.infendro.bytearray.xor
import com.infendro.hash.HashFunction import com.infendro.hash.HashFunction
import com.infendro.mac.HMAC import com.infendro.mac.HMAC
import kotlin.math.min
class PBKDF2( class PBKDF2(
private val function: HashFunction, override val bytes: Int,
) { private val iterations: Int,
private val hmac = HMAC(function) function: HashFunction,
) : KDF {
init {
if (bytes < 1) throw IllegalArgumentException()
if (iterations < 1) throw IllegalArgumentException()
}
fun hash( private val mac = HMAC(function)
value: ByteArray,
salt: ByteArray,
iterations: Int,
length: Int,
): ByteArray {
val key = buildList {
for (i in 1..(length + function.bytes - 1) / function.bytes) {
val input = salt + i.toUInt().toByteArray()
var u = hmac.hash(value, input) override fun hash(
var result = u value: UByteArray,
salt: UByteArray,
): UByteArray {
return buildList {
var i = 1
while (size < bytes) {
addAll(
f(value, salt, i)
.sliceArray(0..<min(bytes - size, mac.bytes))
)
i++
}
}.toUByteArray()
}
private fun f(
value: UByteArray,
salt: UByteArray,
iteration: Int,
): UByteArray {
return buildList {
add(mac.hash(value, salt + iteration.toUByteArray()))
repeat(iterations - 1) { repeat(iterations - 1) {
u = hmac.hash(value, u) add(mac.hash(value, last()))
result = result xor u
} }
}.reduce { acc, bytes -> acc xor bytes }
addAll(result)
}
}
return key
.take(length)
.toByteArray()
} }
} }

View File

@@ -0,0 +1,119 @@
package com.infendro.kdf
import com.infendro.bytearray.encodeToUByteArray
import com.infendro.hash.sha2.`SHA-256`
import kotlin.test.Test
import kotlin.test.assertContentEquals
class `PBKDF2 Test` {
@Test
fun `32 bytes, 1 iteration`() {
val function = PBKDF2(32, 1, `SHA-256`)
val expected = ubyteArrayOf(
0x12U, 0x0fU, 0xb6U, 0xcfU, 0xfcU, 0xf8U, 0xb3U, 0x2cU,
0x43U, 0xe7U, 0x22U, 0x52U, 0x56U, 0xc4U, 0xf8U, 0x37U,
0xa8U, 0x65U, 0x48U, 0xc9U, 0x2cU, 0xccU, 0x35U, 0x48U,
0x08U, 0x05U, 0x98U, 0x7cU, 0xb7U, 0x0bU, 0xe1U, 0x7bU,
)
val actual = function.hash(
"password".encodeToUByteArray(),
"salt".encodeToUByteArray(),
)
assertContentEquals(expected, actual)
}
@Test
fun `32 bytes, 2 iterations`() {
val function = PBKDF2(32, 2, `SHA-256`)
val expected = ubyteArrayOf(
0xaeU, 0x4dU, 0x0cU, 0x95U, 0xafU, 0x6bU, 0x46U, 0xd3U,
0x2dU, 0x0aU, 0xdfU, 0xf9U, 0x28U, 0xf0U, 0x6dU, 0xd0U,
0x2aU, 0x30U, 0x3fU, 0x8eU, 0xf3U, 0xc2U, 0x51U, 0xdfU,
0xd6U, 0xe2U, 0xd8U, 0x5aU, 0x95U, 0x47U, 0x4cU, 0x43U,
)
val actual = function.hash(
"password".encodeToUByteArray(),
"salt".encodeToUByteArray(),
)
assertContentEquals(expected, actual)
}
@Test
fun `32 bytes, 4096 iterations`() {
val function = PBKDF2(32, 4096, `SHA-256`)
val expected = ubyteArrayOf(
0xc5U, 0xe4U, 0x78U, 0xd5U, 0x92U, 0x88U, 0xc8U, 0x41U,
0xaaU, 0x53U, 0x0dU, 0xb6U, 0x84U, 0x5cU, 0x4cU, 0x8dU,
0x96U, 0x28U, 0x93U, 0xa0U, 0x01U, 0xceU, 0x4eU, 0x11U,
0xa4U, 0x96U, 0x38U, 0x73U, 0xaaU, 0x98U, 0x13U, 0x4aU,
)
val actual = function.hash(
"password".encodeToUByteArray(),
"salt".encodeToUByteArray(),
)
assertContentEquals(expected, actual)
}
@Test
fun `40 bytes, 4096 iterations`() {
val function = PBKDF2(40, 4096, `SHA-256`)
val expected = ubyteArrayOf(
0xc5U, 0xe4U, 0x78U, 0xd5U, 0x92U, 0x88U, 0xc8U, 0x41U,
0xaaU, 0x53U, 0x0dU, 0xb6U, 0x84U, 0x5cU, 0x4cU, 0x8dU,
0x96U, 0x28U, 0x93U, 0xa0U, 0x01U, 0xceU, 0x4eU, 0x11U,
0xa4U, 0x96U, 0x38U, 0x73U, 0xaaU, 0x98U, 0x13U, 0x4aU,
0xf7U, 0xadU, 0x98U, 0xc1U, 0xb4U, 0x58U, 0xceU, 0x3fU,
)
val actual = function.hash(
"password".encodeToUByteArray(),
"salt".encodeToUByteArray(),
)
assertContentEquals(expected, actual)
}
@Test
fun `64 bytes, 1 iteration`() {
val function = PBKDF2(64, 1, `SHA-256`)
val expected = ubyteArrayOf(
0x55U, 0xacU, 0x04U, 0x6eU, 0x56U, 0xe3U, 0x08U, 0x9fU,
0xecU, 0x16U, 0x91U, 0xc2U, 0x25U, 0x44U, 0xb6U, 0x05U,
0xf9U, 0x41U, 0x85U, 0x21U, 0x6dU, 0xdeU, 0x04U, 0x65U,
0xe6U, 0x8bU, 0x9dU, 0x57U, 0xc2U, 0x0dU, 0xacU, 0xbcU,
0x49U, 0xcaU, 0x9cU, 0xccU, 0xf1U, 0x79U, 0xb6U, 0x45U,
0x99U, 0x16U, 0x64U, 0xb3U, 0x9dU, 0x77U, 0xefU, 0x31U,
0x7cU, 0x71U, 0xb8U, 0x45U, 0xb1U, 0xe3U, 0x0bU, 0xd5U,
0x09U, 0x11U, 0x20U, 0x41U, 0xd3U, 0xa1U, 0x97U, 0x83U,
)
val actual = function.hash(
"passwd".encodeToUByteArray(),
"salt".encodeToUByteArray(),
)
assertContentEquals(expected, actual)
}
@Test
fun `64 bytes, 80,000 iterations`() {
val function = PBKDF2(64, 80_000, `SHA-256`)
val expected = ubyteArrayOf(
0x4dU, 0xdcU, 0xd8U, 0xf6U, 0x0bU, 0x98U, 0xbeU, 0x21U,
0x83U, 0x0cU, 0xeeU, 0x5eU, 0xf2U, 0x27U, 0x01U, 0xf9U,
0x64U, 0x1aU, 0x44U, 0x18U, 0xd0U, 0x4cU, 0x04U, 0x14U,
0xaeU, 0xffU, 0x08U, 0x87U, 0x6bU, 0x34U, 0xabU, 0x56U,
0xa1U, 0xd4U, 0x25U, 0xa1U, 0x22U, 0x58U, 0x33U, 0x54U,
0x9aU, 0xdbU, 0x84U, 0x1bU, 0x51U, 0xc9U, 0xb3U, 0x17U,
0x6aU, 0x27U, 0x2bU, 0xdeU, 0xbbU, 0xa1U, 0xd0U, 0x78U,
0x47U, 0x8fU, 0x62U, 0xb3U, 0x97U, 0xf3U, 0x3cU, 0x8dU,
)
val actual = function.hash(
"Password".encodeToByteArray().toUByteArray(),
"NaCl".encodeToByteArray().toUByteArray()
)
assertContentEquals(expected, actual)
}
}