+hash salt derivation for any size. better naming

This commit is contained in:
Sergey Chernov 2024-11-25 17:11:08 +07:00
parent 9f7babdf58
commit 10ec58ec08
9 changed files with 88 additions and 19 deletions

View File

@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@ -8,7 +9,7 @@ plugins {
}
group = "net.sergeych"
version = "0.6.2-SNAPSHOT"
version = "0.6.3-SNAPSHOT"
repositories {
mavenCentral()
@ -19,6 +20,7 @@ repositories {
kotlin {
jvm {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}

View File

@ -111,6 +111,34 @@ enum class Hash(
for (block in source) sp.update(block)
return sp.final()
}
/**
* Derive a salt of any size from a text. ___Salt could not be used as a password key___ source as it is not
* strong to brute force, but it is good when you just need to get a deterministic salt of arbitrary size.
*
* _Note that deriving salt of sizes less than hash block will reduce hash strength and should not be allowed
* in situation where strength is of concern while extending its length above hash block size does not improve
* it_. The only reason to use this function is when the desired _salt size_ is not equal to block size, or
* not known beforehand.1
*
* To get a cryptographically safe (more or less) key from password use [KDF] classes, or [KDF.deriveKey]
* and [KDF.deriveMultipleKeys].
*/
fun deriveSalt(base: String, sizeInBytes: Int): UByteArray {
require(sizeInBytes > 0)
val result = mutableListOf<UByte>()
var round = 0
var src = base.encodeToUByteArray()
do {
src = "rnd_${round++}_".encodeToUByteArray() + src
val hash = digest(src)
if (result.size + hash.size <= sizeInBytes) result += hash
else result += hash.slice(0..<(sizeInBytes - result.size))
} while (result.size < sizeInBytes)
return result.toUByteArray()
}
}
private val defaultSuffix1 = "All lay loads on a willing horse".encodeToUByteArray()

View File

@ -114,7 +114,7 @@ object PBKD {
}
return entry.op {
if (entry.data == null) {
entry.data = entry.kdf.derive(password)
entry.data = entry.kdf.deriveKey(password)
}
entry.data!!
}

View File

@ -62,28 +62,34 @@ class UniversalRing(
* Get all keys for the specified id (normally it could be 0, 1 or 2). See [KeyId] about
* matching id keys.
*/
fun keysById(id: KeyId): List<UniversalKey> = allKeys.filter { it.id == id }
fun findById(id: KeyId): List<UniversalKey> = allKeys.filter { it.id == id }
@Deprecated("please replace", replaceWith = ReplaceWith("findById"))
fun keysById(id: KeyId) = findById(id)
/**
* Return sequence of keys that have at least one of the [tags]
*/
fun keysByTags(vararg tags: String) = sequence {
fun findByTags(vararg tags: String) = sequence {
for (e in keyWithTags.entries) {
if (tags.any { it in e.value }) yield(e.key)
}
}
@Deprecated("please replace", replaceWith = ReplaceWith("findByTag"))
fun keysByTag(vararg tags: String) = findByTags(*tags)
/**
* Get the first key of the specified type having the [tag]
*/
inline fun <reified T> keyByTag(tag: String) = keysByTags(tag).first { it is T }
inline fun <reified T> keyByTag(tag: String) = findByTags(tag).first { it is T }
/**
* Get keys of the specified type having any of the specified tags associated.
*/
@Suppress("unused")
inline fun <reified T> keysByAnyTag(vararg tags: String): Sequence<UniversalKey> =
keysByTags(*tags).filter { it is T }
findByTags(*tags).filter { it is T }
/**
* Get all keys with a given id. Note that _matching keys_ have the same id, see [KeyId] for more.
@ -134,6 +140,7 @@ class UniversalRing(
/**
* Add key and tags to the ring. If the key already exists, tags are merged.
*/
@Suppress("unused")
fun add(key: UniversalKey, tags: Collection<String>): UniversalRing =
UniversalRing(keyWithTags + (key to tags.toSet()))

View File

@ -49,20 +49,20 @@ sealed class KDF {
*/
@Suppress("unused")
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> =
kdfForSize(count).deriveMultiple(password, count)
kdfForSize(count).deriveMultipleKeys(password, count)
}
/**
* Derive a single key from the password, same as [deriveMultiple] with `count==1`
* Derive a single key from the password, same as [deriveMultipleKeys] with `count==1`
*/
abstract fun derive(password: String): UByteArray
abstract fun deriveKey(password: String): 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.
*/
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> {
val bytes = derive(password)
fun deriveMultipleKeys(password: String, count: Int): List<SymmetricKey> {
val bytes = deriveKey(password)
val ks = SymmetricKey.keyLength
check(ks * count <= bytes.size) { "KDF is too short for $count keys: ${bytes.size} we need ${ks * count}" }
return (0..<count).map {
@ -84,6 +84,7 @@ sealed class KDF {
val keySize: Int,
) : KDF(), Comparable<Argon> {
/**
* Very abstract strength comparison. If a1 > a2 it generally means that its complexity, and, hopefully,
* strength is higher.
@ -111,7 +112,7 @@ sealed class KDF {
}
}
override fun derive(password: String): UByteArray =
override fun deriveKey(password: String): UByteArray =
PasswordHash.pwhash(keySize, password, salt, instructionsComplexity, memComplexity, algorithm.code)
override fun equals(other: Any?): Boolean {
@ -141,6 +142,17 @@ sealed class KDF {
fun randomSalt() = randomUBytes(saltSize)
/**
* Create a deterministic salt suitable fot this KDF from a given text.
*
* We recommend to use random salts stored, and [KeyId] of password-generated keys already
* do it for you. Use this method only if you can't store [KeyId] or salt; it is generally less secure:
* knowing the base text it is possible to understand, for example, that the same derived password was used
* more than once (random salt makes it impossible).
*/
@Suppress("unused")
fun deriveSaltFromString(text: String) = Hash.Blake2b.deriveSalt(text, saltSize)
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" }
@ -165,6 +177,7 @@ sealed class KDF {
268435456,
salt, keySize
)
Moderate -> Argon(
Alg.default,
crypto_pwhash_OPSLIMIT_MODERATE,
@ -192,10 +205,10 @@ sealed class KDF {
}
companion object {
@Suppress("unused")
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
return Argon.create(complexity, salt, keySize)
}
}
data class Instance(val kdf: KDF, val password: String)

View File

@ -5,7 +5,10 @@ import net.sergeych.crypto2.Hash
import net.sergeych.crypto2.initCrypto
import kotlin.random.Random
import kotlin.random.nextUBytes
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
suspend fun <T> sw(label: String, f: suspend () -> T): T {
@ -46,5 +49,21 @@ class HashTest {
assertContentEquals(Hash.Blake2b.digest(a), p1)
assertContentEquals(Hash.Sha3_384.digest(a), p2)
}
@Test
fun deriveSaltTest() = runTest {
initCrypto()
for( i in 2..257 ) {
val x = Hash.Sha3AndBlake.deriveSalt("base one", i)
val y = Hash.Sha3AndBlake.deriveSalt("base one", i)
val z = Hash.Sha3AndBlake.deriveSalt("base two", i)
assertContentEquals(x, y)
assertFalse { x contentEquals z }
assertEquals(x.size, i)
assertEquals(y.size, i)
assertEquals(z.size, i)
}
}
}

View File

@ -41,7 +41,7 @@ class KDFTest {
@Test
fun complexityTest() = runTest{
initCrypto()
val kk = KDF.Complexity.Interactive.kdfForSize(3).deriveMultiple("lala", 3)
val kk = KDF.Complexity.Interactive.kdfForSize(3).deriveMultipleKeys("lala", 3)
assertEquals(3, kk.size)
}

View File

@ -33,7 +33,7 @@ class PBKDTest {
assertEquals(i.kdp, kx.id.kdp)
}
val (y1,y2,y3) = k1.id.kdp!!.kdf.deriveMultiple("foobar", 3)
val (y1,y2,y3) = k1.id.kdp!!.kdf.deriveMultipleKeys("foobar", 3)
for( (a,b) in listOf(y1,y2,y3).zip(listOf(k1,k2,k3))) {
assertEquals(a,b)
assertNotNull(a.id.kdp)

View File

@ -157,14 +157,14 @@ class RingTest {
assertEquals(a, r1.findKey<SecretKey>(a.id))
assertEquals(a, r1.keyByTag<UniversalKey>("foo_a"))
assertEquals(b, r1.findKey<SigningKey>(b.id))
assertEquals(c, r1.keysById(c.id).first())
assertEquals(c, r1.findById(c.id).first())
r1 = UniversalRing.join(listOf(ra, rb, rc, rd))
assertEquals(a, r1.findKey<SecretKey>(a.id))
assertEquals(a, r1.keyByTag<UniversalKey>("foo_a"))
assertEquals(b, r1.findKey<SigningKey>(b.id))
assertEquals(c, r1.keysById(c.id).first())
assertEquals(c, r1.findById(c.id).first())
}
}