diff --git a/README.md b/README.md index 43ad863..824fba9 100644 --- a/README.md +++ b/README.md @@ -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. @@ -133,4 +137,4 @@ you need to obtain a license from https://8-rays.dev or [Sergey Chernov]. For op It will be moved to open source; we also guarantee that it will be moved to open source immediately if the software export restrictions will be lifted. We do not support such practices here at 8-rays.dev and assume open source must be open. -[Sergey Chernov]: https://t.me/real_sergeych \ No newline at end of file +[Sergey Chernov]: https://t.me/real_sergeych diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt index 4fedabf..27d4eb6 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt @@ -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() 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) + } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt index 76c3fc4..f2eadad 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt @@ -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) + fun kdfForSize(numberOfKeys: Int, salt: UByteArray = Argon.randomSalt()): KDF = + 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_. @@ -65,7 +71,7 @@ sealed class KDF { * to change with time. */ @Suppress("unused") - fun deriveMultiple(password: String, count: Int,salt: UByteArray): List = + fun deriveMultiple(password: String, count: Int, salt: UByteArray): List = kdfForSize(count, salt).deriveMultipleKeys(password, count) /** @@ -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,10 +259,23 @@ 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) + } data class Instance(val kdf: KDF, val password: String) diff --git a/src/commonTest/kotlin/HashTest.kt b/src/commonTest/kotlin/HashTest.kt index 2da3237..beff90d 100644 --- a/src/commonTest/kotlin/HashTest.kt +++ b/src/commonTest/kotlin/HashTest.kt @@ -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) - assertContentEquals(x, y) - assertFalse { x contentEquals z } - assertEquals(x.size, i) - assertEquals(y.size, i) - assertEquals(z.size, 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) + 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 sw(label: String, f: suspend () -> T): T { return result } - diff --git a/src/commonTest/kotlin/KDFTest.kt b/src/commonTest/kotlin/KDFTest.kt index 4033ae7..16105dd 100644 --- a/src/commonTest/kotlin/KDFTest.kt +++ b/src/commonTest/kotlin/KDFTest.kt @@ -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) 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(shortKdf) + assertIs(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 { - kdf.deriveFromBytes(ubyteArrayOf(1u, 2u, 3u), KDF.Argon.minKeySize - 1) - } + assertEquals(64, shortKdf.deriveFromBytes(source).size) + assertEquals(96, longKdf.deriveFromBytes(source).size) } }