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.
|
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
|
## 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.
|
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.
|
* 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
|
* _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
|
* 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]
|
* To get a cryptographically safe (more or less) key from password use [KDF] classes, or [KDF.deriveKey]
|
||||||
* and [KDF.deriveMultipleKeys].
|
* 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)
|
require(sizeInBytes > 0)
|
||||||
val result = mutableListOf<UByte>()
|
val result = mutableListOf<UByte>()
|
||||||
var round = 0
|
var round = 0
|
||||||
var src = base.encodeToUByteArray()
|
var src = base
|
||||||
do {
|
do {
|
||||||
src = "rnd_${round++}_".encodeToUByteArray() + src
|
src = "rnd_${round++}_".encodeToUByteArray() + src
|
||||||
val hash = digest(src)
|
val hash = digest(src)
|
||||||
if (result.size + hash.size <= sizeInBytes) result += hash
|
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)
|
} while (result.size < sizeInBytes)
|
||||||
return result.toUByteArray()
|
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.
|
* 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 =
|
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)
|
* Create [KDF] for deriving [sizeInBytes] bytes using the provided [salt].
|
||||||
return creteDefault(sizeInBytes, this, 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
|
* Derive multiple keys from the password. Derivation params will be included in the key ids, see
|
||||||
* [SymmetricKey.id] as [KeyId.kdp].
|
* [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
|
* ___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_.
|
* 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.
|
* 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 =
|
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
|
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
|
* 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 =
|
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 {
|
override fun deriveFromBytes(source: UByteArray): UByteArray {
|
||||||
require(derivedSize >= minKeySize) { "The derived size should be at least $minKeySize bytes" }
|
|
||||||
return PasswordHash.pwhash(
|
return PasswordHash.pwhash(
|
||||||
derivedSize,
|
keySize,
|
||||||
source.encodeToBase64Url(),
|
source.encodeToBase64Url(),
|
||||||
salt,
|
salt,
|
||||||
instructionsComplexity,
|
instructionsComplexity,
|
||||||
@ -251,9 +259,22 @@ sealed class KDF {
|
|||||||
|
|
||||||
companion object {
|
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
|
@Test
|
||||||
fun deriveSaltTest() = runTest {
|
fun deriveSaltTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
for( i in 2..257 ) {
|
for (hash in Hash.entries) {
|
||||||
val x = Hash.Sha3AndBlake.deriveSalt("base one", i)
|
for (i in 1..257) {
|
||||||
val y = Hash.Sha3AndBlake.deriveSalt("base one", i)
|
val x = hash.deriveSalt("base one", i)
|
||||||
val z = Hash.Sha3AndBlake.deriveSalt("base two", i)
|
val y = hash.deriveSalt("base one", i)
|
||||||
|
val z = hash.deriveSalt("base two", i)
|
||||||
assertContentEquals(x, y)
|
assertContentEquals(x, y)
|
||||||
assertFalse { x contentEquals z }
|
if (i > 1) assertFalse { x contentEquals z }
|
||||||
assertEquals(x.size, i)
|
assertEquals(i, x.size)
|
||||||
assertEquals(y.size, i)
|
assertEquals(i, y.size)
|
||||||
assertEquals(z.size, i)
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertContentEquals
|
import kotlin.test.assertContentEquals
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
import kotlin.test.assertIs
|
import kotlin.test.assertIs
|
||||||
|
|
||||||
class KDFTest {
|
class KDFTest {
|
||||||
@ -63,18 +62,34 @@ class KDFTest {
|
|||||||
fun complexityKdfForSizeInBytesTest() = runTest {
|
fun complexityKdfForSizeInBytesTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
val size = KDF.Argon.minKeySize + 17
|
val size = KDF.Argon.minKeySize + 17
|
||||||
val domain = "kdf-size-in-bytes-test"
|
val expectedSalt = UByteArray(KDF.Argon.saltSize) { (it + 3).toUByte() }
|
||||||
val expectedSalt = Hash.Blake2b.deriveSalt(domain, KDF.Argon.saltSize)
|
|
||||||
|
|
||||||
val kdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, domain)
|
val kdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, expectedSalt)
|
||||||
assertIs<KDF.Argon>(kdf)
|
assertIs<KDF.Argon>(kdf)
|
||||||
|
|
||||||
assertEquals(KDF.Argon.create(KDF.Complexity.FixedLow, expectedSalt, size), kdf)
|
assertEquals(KDF.Argon.create(KDF.Complexity.FixedLow, expectedSalt, size), kdf)
|
||||||
assertEquals(size, kdf.keySize)
|
assertEquals(size, kdf.keySize)
|
||||||
assertContentEquals(expectedSalt, kdf.salt)
|
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
|
@Test
|
||||||
@ -84,9 +99,9 @@ class KDFTest {
|
|||||||
val kdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 80)
|
val kdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 80)
|
||||||
val source = UByteArray(47) { (it * 3).toUByte() }
|
val source = UByteArray(47) { (it * 3).toUByte() }
|
||||||
|
|
||||||
val b1 = kdf.deriveFromBytes(source, 80)
|
val b1 = kdf.deriveFromBytes(source)
|
||||||
val b2 = kdf.deriveFromBytes(source, 80)
|
val b2 = kdf.deriveFromBytes(source)
|
||||||
val b3 = kdf.deriveFromBytes(source + 1u, 80)
|
val b3 = kdf.deriveFromBytes(source + 1u)
|
||||||
|
|
||||||
assertEquals(80, b1.size)
|
assertEquals(80, b1.size)
|
||||||
assertContentEquals(b1, b2)
|
assertContentEquals(b1, b2)
|
||||||
@ -100,21 +115,22 @@ class KDFTest {
|
|||||||
val source = UByteArray(23) { (it + 7).toUByte() }
|
val source = UByteArray(23) { (it + 7).toUByte() }
|
||||||
|
|
||||||
val b1 = KDF.Complexity.Interactive.derive(source, salt, 64)
|
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)
|
assertContentEquals(b1, b2)
|
||||||
assertEquals(64, b1.size)
|
assertEquals(64, b1.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deriveFromBytesValidationTest() = runTest {
|
fun deriveFromBytesUsesKeySizeTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() }
|
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> {
|
assertEquals(64, shortKdf.deriveFromBytes(source).size)
|
||||||
kdf.deriveFromBytes(ubyteArrayOf(1u, 2u, 3u), KDF.Argon.minKeySize - 1)
|
assertEquals(96, longKdf.deriveFromBytes(source).size)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user