From 5c3ff72123302833c945cf8124484ec4b2361091 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 29 May 2026 13:22:52 +0300 Subject: [PATCH] +KDF derive from bytes --- .../kotlin/net/sergeych/crypto2/kdf.kt | 27 +++++++++++- src/commonTest/kotlin/KDFTest.kt | 44 ++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt index 6af9322..2a3b490 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt @@ -66,6 +66,12 @@ sealed class KDF { * Derive single key from password, same as [deriveMultiple] with count=1. */ 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 + /** + * 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 * 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 = 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 { if (this === other) return true if (other !is Argon) return false @@ -171,7 +194,7 @@ sealed class KDF { fun create(complexity: Complexity, salt: UByteArray, keySize: Int): Argon { 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) { FixedLow -> Argon( V2id_13, @@ -221,6 +244,7 @@ sealed class KDF { } companion object { + fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF { return Argon.create(complexity, salt, keySize) } @@ -230,4 +254,3 @@ sealed class KDF { data class Instance(val kdf: KDF, val password: String) } - diff --git a/src/commonTest/kotlin/KDFTest.kt b/src/commonTest/kotlin/KDFTest.kt index 5c55b5f..ece84fe 100644 --- a/src/commonTest/kotlin/KDFTest.kt +++ b/src/commonTest/kotlin/KDFTest.kt @@ -12,8 +12,10 @@ import kotlinx.coroutines.test.runTest import net.sergeych.crypto2.KDF import net.sergeych.crypto2.initCrypto import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertFailsWith class KDFTest { @Test @@ -55,4 +57,44 @@ class KDFTest { assertEquals(3, kk.size) } -} \ No newline at end of file + @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 { + kdf.deriveFromBytes(ubyteArrayOf(1u, 2u, 3u), KDF.Argon.minKeySize - 1) + } + } + +}