+KDF derive from bytes

This commit is contained in:
Sergey Chernov 2026-05-29 13:22:52 +03:00
parent d0f1fffe6d
commit 5c3ff72123
2 changed files with 68 additions and 3 deletions

View File

@ -66,6 +66,12 @@ sealed class KDF {
* Derive single key from password, same as [deriveMultiple] with count=1. * Derive single key from password, same as [deriveMultiple] with count=1.
*/ */
fun derive(password: String, salt: UByteArray): SymmetricKey = deriveMultiple(password, 1, salt).first() fun derive(password: String, salt: UByteArray): SymmetricKey = deriveMultiple(password, 1, salt).first()
/**
* Derive bytes from bytes using the default [Argon] KDF with this complexity.
*/
fun derive(source: UByteArray, salt: UByteArray, derivedSize: Int): UByteArray =
creteDefault(derivedSize, this, salt).deriveFromBytes(source, derivedSize)
} }
/** /**
@ -73,6 +79,11 @@ sealed class KDF {
*/ */
abstract fun deriveKey(password: String): UByteArray abstract fun deriveKey(password: String): UByteArray
/**
* Derive bytes from bytes using this KDF parameters.
*/
abstract fun deriveFromBytes(source: UByteArray, derivedSize: Int): UByteArray
/** /**
* Derive keys from lower part of bytes derived from the password. E.g., if generated size is longer than * Derive keys from lower part of bytes derived from the password. E.g., if generated size is longer than
* required to create [count] keys, the first bytes will be used. It let use rest bytes for other purposes. * required to create [count] keys, the first bytes will be used. It let use rest bytes for other purposes.
@ -131,6 +142,18 @@ sealed class KDF {
override fun deriveKey(password: String): UByteArray = override fun deriveKey(password: String): UByteArray =
PasswordHash.pwhash(keySize, password, salt, instructionsComplexity, memComplexity, algorithm.code) PasswordHash.pwhash(keySize, password, salt, instructionsComplexity, memComplexity, algorithm.code)
override fun deriveFromBytes(source: UByteArray, derivedSize: Int): UByteArray {
require(derivedSize >= minKeySize) { "The derived size should be at least $minKeySize bytes" }
return PasswordHash.pwhash(
derivedSize,
source.encodeToBase64Url(),
salt,
instructionsComplexity,
memComplexity,
algorithm.code
)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Argon) return false if (other !is Argon) return false
@ -171,7 +194,7 @@ sealed class KDF {
fun create(complexity: Complexity, salt: UByteArray, keySize: Int): Argon { fun create(complexity: Complexity, salt: UByteArray, keySize: Int): Argon {
require(salt.size == saltSize) { "The salt size should be $saltSize" } require(salt.size == saltSize) { "The salt size should be $saltSize" }
require(keySize > minKeySize) { "The key size should be at least $keySize bytes" } require(keySize >= minKeySize) { "The key size should be at least $minKeySize bytes" }
return when (complexity) { return when (complexity) {
FixedLow -> Argon( FixedLow -> Argon(
V2id_13, V2id_13,
@ -221,6 +244,7 @@ sealed class KDF {
} }
companion object { companion object {
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF { fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
return Argon.create(complexity, salt, keySize) return Argon.create(complexity, salt, keySize)
} }
@ -230,4 +254,3 @@ sealed class KDF {
data class Instance(val kdf: KDF, val password: String) data class Instance(val kdf: KDF, val password: String)
} }

View File

@ -12,8 +12,10 @@ import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.KDF import net.sergeych.crypto2.KDF
import net.sergeych.crypto2.initCrypto import net.sergeych.crypto2.initCrypto
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertFailsWith
class KDFTest { class KDFTest {
@Test @Test
@ -55,4 +57,44 @@ class KDFTest {
assertEquals(3, kk.size) assertEquals(3, kk.size)
} }
} @Test
fun deriveFromBytesTest() = runTest {
initCrypto()
val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() }
val kdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 80)
val source = UByteArray(47) { (it * 3).toUByte() }
val b1 = kdf.deriveFromBytes(source, 80)
val b2 = kdf.deriveFromBytes(source, 80)
val b3 = kdf.deriveFromBytes(source + 1u, 80)
assertEquals(80, b1.size)
assertContentEquals(b1, b2)
assertFalse { b1 contentEquals b3 }
}
@Test
fun complexityDeriveFromBytesTest() = runTest {
initCrypto()
val salt = UByteArray(KDF.Argon.saltSize) { (it + 1).toUByte() }
val source = UByteArray(23) { (it + 7).toUByte() }
val b1 = KDF.Complexity.Interactive.derive(source, salt, 64)
val b2 = KDF.Argon.create(KDF.Complexity.Interactive, salt, 64).deriveFromBytes(source, 64)
assertContentEquals(b1, b2)
assertEquals(64, b1.size)
}
@Test
fun deriveFromBytesValidationTest() = runTest {
initCrypto()
val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() }
val kdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, KDF.Argon.minKeySize)
assertFailsWith<IllegalArgumentException> {
kdf.deriveFromBytes(ubyteArrayOf(1u, 2u, 3u), KDF.Argon.minKeySize - 1)
}
}
}