Fix salt derivation and KDF byte derivation
This commit is contained in:
parent
2d55af4e3b
commit
ff494ab4ef
@ -110,6 +110,10 @@ Keys could be associated with tags. Keyrings are used primarily to store keys in
|
||||
|
||||
Using very strong Argon_v2id, and adjustable complexity. Allows storing password key derivation parameters (included in the generated symmetric keys) to re-derive keys later, allows multiple keys derivation. All structures meant to be stored are serializable.
|
||||
|
||||
KDF convenience constructors accept salt bytes directly. When the provided salt is not exactly the Argon salt size, it is deterministically normalized with `Hash.Blake2b.deriveSaltFormBytes(...)` before the KDF is created; exact-size salts are used as is.
|
||||
|
||||
`KDF.deriveFromBytes(source)` derives exactly the configured KDF key size. Create the KDF with the required key size first, for example through `KDF.createDefault(...)` or `Complexity.kdfForSizeInBytes(...)`.
|
||||
|
||||
## Unified keys hierarchy
|
||||
|
||||
Allows the application code to use proper key abstraction and work with more key types in the future, e.g. `SigningKey`, `VerifyingKey`, `EncryptingKey` and `DecryptingKey`. Effective key generation and random byte sequence producers.
|
||||
|
||||
@ -129,27 +129,41 @@ enum class Hash(
|
||||
* 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
|
||||
* in a 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
|
||||
* not known beforehand.
|
||||
*
|
||||
* 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 {
|
||||
fun deriveSalt(base: String, sizeInBytes: Int): UByteArray =
|
||||
deriveSaltFromBytes(base.encodeToUByteArray(), sizeInBytes)
|
||||
|
||||
/**
|
||||
* Derive a salt of any size from binary data.
|
||||
*
|
||||
* @see deriveSalt
|
||||
*/
|
||||
fun deriveSaltFromBytes(base: UByteArray, sizeInBytes: Int): UByteArray {
|
||||
require(sizeInBytes > 0)
|
||||
val result = mutableListOf<UByte>()
|
||||
var round = 0
|
||||
var src = base.encodeToUByteArray()
|
||||
var src = base
|
||||
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))
|
||||
else result += hash.copyOf(sizeInBytes - result.size)
|
||||
} while (result.size < sizeInBytes)
|
||||
return result.toUByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Typo-compatible alias for [deriveSaltFromBytes].
|
||||
*/
|
||||
fun deriveSaltFormBytes(base: UByteArray, sizeInBytes: Int): UByteArray =
|
||||
deriveSaltFromBytes(base, sizeInBytes)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -42,22 +42,28 @@ sealed class KDF {
|
||||
/**
|
||||
* Create [KDF] of the corresponding strength suitable to derive [numberOfKeys] symmetric keys.
|
||||
*
|
||||
* Random salt of proper size is used
|
||||
* Random salt of proper size is used by default. If a custom [salt] size is not [Argon.saltSize],
|
||||
* it is deterministically normalized with `Hash.Blake2b.deriveSaltFormBytes(...)`.
|
||||
*/
|
||||
fun kdfForSize(numberOfKeys: Int, salt: UByteArray = Argon.randomSalt()): KDF =
|
||||
creteDefault(SymmetricKey.keyLength * numberOfKeys, this, salt)
|
||||
createDefault(SymmetricKey.keyLength * numberOfKeys, this, salt)
|
||||
|
||||
fun kdfForSizeInBytes(sizeInBytes: Int, domain: String): KDF {
|
||||
val salt = Hash.Blake2b.deriveSalt(domain, Argon.saltSize)
|
||||
return creteDefault(sizeInBytes, this, salt)
|
||||
}
|
||||
/**
|
||||
* Create [KDF] for deriving [sizeInBytes] bytes using the provided [salt].
|
||||
*
|
||||
* The [salt] is used as is when its size is [Argon.saltSize]. Otherwise, it is deterministically
|
||||
* normalized with `Hash.Blake2b.deriveSaltFormBytes(...)` to the required size.
|
||||
*/
|
||||
fun kdfForSizeInBytes(sizeInBytes: Int, salt: UByteArray): KDF =
|
||||
createDefault(sizeInBytes, this, salt)
|
||||
|
||||
|
||||
/**
|
||||
* Derive multiple keys from the password. Derivation params will be included in the key ids, see
|
||||
* [SymmetricKey.id] as [KeyId.kdp].
|
||||
|
||||
* Random salt of proper size is used
|
||||
* If the [salt] size is not [Argon.saltSize], it is deterministically normalized with
|
||||
* `Hash.Blake2b.deriveSaltFormBytes(...)`.
|
||||
*
|
||||
* ___Important: symmetric keys do not save key ids___. _Container do it, so it is possible to re-derive
|
||||
* key to open the container, but in many cases you might need to save [KeyId.kdp] separately_.
|
||||
@ -75,9 +81,12 @@ sealed class KDF {
|
||||
|
||||
/**
|
||||
* Derive bytes from bytes using the default [Argon] KDF with this complexity.
|
||||
*
|
||||
* If the [salt] size is not [Argon.saltSize], it is deterministically normalized with
|
||||
* `Hash.Blake2b.deriveSaltFormBytes(...)`.
|
||||
*/
|
||||
fun derive(source: UByteArray, salt: UByteArray, derivedSize: Int): UByteArray =
|
||||
creteDefault(derivedSize, this, salt).deriveFromBytes(source, derivedSize)
|
||||
createDefault(derivedSize, this, salt).deriveFromBytes(source)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,9 +95,9 @@ sealed class KDF {
|
||||
abstract fun deriveKey(password: String): UByteArray
|
||||
|
||||
/**
|
||||
* Derive bytes from bytes using this KDF parameters.
|
||||
* Derive bytes from bytes using this KDF parameters. The derived byte count is this KDF's configured key size.
|
||||
*/
|
||||
abstract fun deriveFromBytes(source: UByteArray, derivedSize: Int): UByteArray
|
||||
abstract fun deriveFromBytes(source: UByteArray): UByteArray
|
||||
|
||||
/**
|
||||
* Derive keys from lower part of bytes derived from the password. E.g., if generated size is longer than
|
||||
@ -148,10 +157,9 @@ 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" }
|
||||
override fun deriveFromBytes(source: UByteArray): UByteArray {
|
||||
return PasswordHash.pwhash(
|
||||
derivedSize,
|
||||
keySize,
|
||||
source.encodeToBase64Url(),
|
||||
salt,
|
||||
instructionsComplexity,
|
||||
@ -251,9 +259,22 @@ sealed class KDF {
|
||||
|
||||
companion object {
|
||||
|
||||
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
|
||||
return Argon.create(complexity, salt, keySize)
|
||||
/**
|
||||
* Create default [Argon] KDF parameters. If [salt] is not exactly [Argon.saltSize], it is
|
||||
* deterministically normalized with [Hash.Blake2b.deriveSaltFormBytes] before creating the KDF.
|
||||
*/
|
||||
fun createDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
|
||||
val normalizedSalt = if (salt.size == Argon.saltSize) {
|
||||
salt
|
||||
} else {
|
||||
Hash.Blake2b.deriveSaltFormBytes(salt, Argon.saltSize)
|
||||
}
|
||||
return Argon.create(complexity, normalizedSalt, keySize)
|
||||
}
|
||||
|
||||
@Deprecated("Use createDefault", ReplaceWith("createDefault(keySize, complexity, salt)"))
|
||||
fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF =
|
||||
createDefault(keySize, complexity, salt)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -55,15 +55,33 @@ class HashTest {
|
||||
@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)
|
||||
for (hash in Hash.entries) {
|
||||
for (i in 1..257) {
|
||||
val x = hash.deriveSalt("base one", i)
|
||||
val y = hash.deriveSalt("base one", i)
|
||||
val z = hash.deriveSalt("base two", i)
|
||||
assertContentEquals(x, y)
|
||||
assertFalse { x contentEquals z }
|
||||
assertEquals(x.size, i)
|
||||
assertEquals(y.size, i)
|
||||
assertEquals(z.size, i)
|
||||
if (i > 1) assertFalse { x contentEquals z }
|
||||
assertEquals(i, x.size)
|
||||
assertEquals(i, y.size)
|
||||
assertEquals(i, z.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deriveSaltFromBytesTest() = runTest {
|
||||
initCrypto()
|
||||
val base = "base one".encodeToByteArray().asUByteArray()
|
||||
for (hash in Hash.entries) {
|
||||
for (i in listOf(1, 5, 31, 32, 33, 96, 97)) {
|
||||
val fromString = hash.deriveSalt("base one", i)
|
||||
val fromBytes = hash.deriveSaltFromBytes(base, i)
|
||||
val fromTypoAlias = hash.deriveSaltFormBytes(base, i)
|
||||
assertEquals(i, fromBytes.size)
|
||||
assertContentEquals(fromString, fromBytes)
|
||||
assertContentEquals(fromBytes, fromTypoAlias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,4 +96,3 @@ suspend fun <T> sw(label: String, f: suspend () -> T): T {
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class KDFTest {
|
||||
@ -63,18 +62,34 @@ class KDFTest {
|
||||
fun complexityKdfForSizeInBytesTest() = runTest {
|
||||
initCrypto()
|
||||
val size = KDF.Argon.minKeySize + 17
|
||||
val domain = "kdf-size-in-bytes-test"
|
||||
val expectedSalt = Hash.Blake2b.deriveSalt(domain, KDF.Argon.saltSize)
|
||||
val expectedSalt = UByteArray(KDF.Argon.saltSize) { (it + 3).toUByte() }
|
||||
|
||||
val kdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, domain)
|
||||
val kdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, expectedSalt)
|
||||
assertIs<KDF.Argon>(kdf)
|
||||
|
||||
assertEquals(KDF.Argon.create(KDF.Complexity.FixedLow, expectedSalt, size), kdf)
|
||||
assertEquals(size, kdf.keySize)
|
||||
assertContentEquals(expectedSalt, kdf.salt)
|
||||
assertFalse {
|
||||
kdf.salt contentEquals Hash.Blake2b.deriveSalt("$domain-other", KDF.Argon.saltSize)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createDefaultNormalizesSaltTest() = runTest {
|
||||
initCrypto()
|
||||
val size = KDF.Argon.minKeySize + 19
|
||||
val shortSalt = ubyteArrayOf(1u, 2u, 3u, 4u, 5u)
|
||||
val longSalt = UByteArray(KDF.Argon.saltSize + 7) { (it * 5).toUByte() }
|
||||
val expectedShortSalt = Hash.Blake2b.deriveSaltFormBytes(shortSalt, KDF.Argon.saltSize)
|
||||
val expectedLongSalt = Hash.Blake2b.deriveSaltFormBytes(longSalt, KDF.Argon.saltSize)
|
||||
|
||||
val shortKdf = KDF.createDefault(size, KDF.Complexity.FixedLow, shortSalt)
|
||||
val longKdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, longSalt)
|
||||
|
||||
assertIs<KDF.Argon>(shortKdf)
|
||||
assertIs<KDF.Argon>(longKdf)
|
||||
assertEquals(KDF.Argon.saltSize, shortKdf.salt.size)
|
||||
assertEquals(KDF.Argon.saltSize, longKdf.salt.size)
|
||||
assertContentEquals(expectedShortSalt, shortKdf.salt)
|
||||
assertContentEquals(expectedLongSalt, longKdf.salt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -84,9 +99,9 @@ class KDFTest {
|
||||
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)
|
||||
val b1 = kdf.deriveFromBytes(source)
|
||||
val b2 = kdf.deriveFromBytes(source)
|
||||
val b3 = kdf.deriveFromBytes(source + 1u)
|
||||
|
||||
assertEquals(80, b1.size)
|
||||
assertContentEquals(b1, b2)
|
||||
@ -100,21 +115,22 @@ class KDFTest {
|
||||
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)
|
||||
val b2 = KDF.Argon.create(KDF.Complexity.Interactive, salt, 64).deriveFromBytes(source)
|
||||
|
||||
assertContentEquals(b1, b2)
|
||||
assertEquals(64, b1.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deriveFromBytesValidationTest() = runTest {
|
||||
fun deriveFromBytesUsesKeySizeTest() = runTest {
|
||||
initCrypto()
|
||||
val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() }
|
||||
val kdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, KDF.Argon.minKeySize)
|
||||
val source = ubyteArrayOf(1u, 2u, 3u)
|
||||
val shortKdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 64)
|
||||
val longKdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 96)
|
||||
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
kdf.deriveFromBytes(ubyteArrayOf(1u, 2u, 3u), KDF.Argon.minKeySize - 1)
|
||||
}
|
||||
assertEquals(64, shortKdf.deriveFromBytes(source).size)
|
||||
assertEquals(96, longKdf.deriveFromBytes(source).size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user