Compare commits

..

No commits in common. "d402afe1bd0ba4deaec96b4dc2c410f35328c60b" and "2d55af4e3b0d4506e81e47c1ede8b6aac257ec0c" have entirely different histories.

6 changed files with 50 additions and 122 deletions

View File

@ -110,10 +110,6 @@ 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.
@ -137,4 +133,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. 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 [Sergey Chernov]: https://t.me/real_sergeych

View File

@ -20,7 +20,7 @@ plugins {
} }
group = "net.sergeych" group = "net.sergeych"
version = "0.9.2" version = "0.9.2-SNAPSHOT"
repositories { repositories {
mavenCentral() mavenCentral()

View File

@ -129,41 +129,27 @@ 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 a situation where strength is of concern while extending its length above hash block size does not improve * 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 * it_. The only reason to use this function is when the desired _salt size_ is not equal to block size, or
* not known beforehand. * not known beforehand.1
* *
* 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 var src = base.encodeToUByteArray()
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.copyOf(sizeInBytes - result.size) else result += hash.slice(0..<(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)
} }

View File

@ -42,28 +42,22 @@ 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 by default. If a custom [salt] size is not [Argon.saltSize], * Random salt of proper size is used
* 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 =
createDefault(SymmetricKey.keyLength * numberOfKeys, this, salt) creteDefault(SymmetricKey.keyLength * numberOfKeys, this, salt)
/** fun kdfForSizeInBytes(sizeInBytes: Int, domain: String): KDF {
* Create [KDF] for deriving [sizeInBytes] bytes using the provided [salt]. val salt = Hash.Blake2b.deriveSalt(domain, Argon.saltSize)
* 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].
* If the [salt] size is not [Argon.saltSize], it is deterministically normalized with * Random salt of proper size is used
* `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_.
@ -71,7 +65,7 @@ sealed class KDF {
* to change with time. * to change with time.
*/ */
@Suppress("unused") @Suppress("unused")
fun deriveMultiple(password: String, count: Int, salt: UByteArray): List<SymmetricKey> = fun deriveMultiple(password: String, count: Int,salt: UByteArray): List<SymmetricKey> =
kdfForSize(count, salt).deriveMultipleKeys(password, count) kdfForSize(count, salt).deriveMultipleKeys(password, count)
/** /**
@ -81,12 +75,9 @@ 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 =
createDefault(derivedSize, this, salt).deriveFromBytes(source) creteDefault(derivedSize, this, salt).deriveFromBytes(source, derivedSize)
} }
/** /**
@ -95,9 +86,9 @@ sealed class KDF {
abstract fun deriveKey(password: String): UByteArray abstract fun deriveKey(password: String): UByteArray
/** /**
* Derive bytes from bytes using this KDF parameters. The derived byte count is this KDF's configured key size. * Derive bytes from bytes using this KDF parameters.
*/ */
abstract fun deriveFromBytes(source: UByteArray): UByteArray 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 * Derive keys from lower part of bytes derived from the password. E.g., if generated size is longer than
@ -157,9 +148,10 @@ 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): UByteArray { override fun deriveFromBytes(source: UByteArray, derivedSize: Int): UByteArray {
require(derivedSize >= minKeySize) { "The derived size should be at least $minKeySize bytes" }
return PasswordHash.pwhash( return PasswordHash.pwhash(
keySize, derivedSize,
source.encodeToBase64Url(), source.encodeToBase64Url(),
salt, salt,
instructionsComplexity, instructionsComplexity,
@ -259,23 +251,10 @@ sealed class KDF {
companion object { companion object {
/** fun creteDefault(keySize: Int, complexity: Complexity, salt: UByteArray = Argon.randomSalt()): KDF {
* Create default [Argon] KDF parameters. If [salt] is not exactly [Argon.saltSize], it is return Argon.create(complexity, salt, keySize)
* 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) data class Instance(val kdf: KDF, val password: String)

View File

@ -55,33 +55,15 @@ class HashTest {
@Test @Test
fun deriveSaltTest() = runTest { fun deriveSaltTest() = runTest {
initCrypto() initCrypto()
for (hash in Hash.entries) { for( i in 2..257 ) {
for (i in 1..257) { val x = Hash.Sha3AndBlake.deriveSalt("base one", i)
val x = hash.deriveSalt("base one", i) val y = Hash.Sha3AndBlake.deriveSalt("base one", i)
val y = hash.deriveSalt("base one", i) val z = Hash.Sha3AndBlake.deriveSalt("base two", 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)
}
} }
} }
@ -96,3 +78,4 @@ suspend fun <T> sw(label: String, f: suspend () -> T): T {
return result return result
} }

View File

@ -16,6 +16,7 @@ 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 {
@ -62,34 +63,18 @@ class KDFTest {
fun complexityKdfForSizeInBytesTest() = runTest { fun complexityKdfForSizeInBytesTest() = runTest {
initCrypto() initCrypto()
val size = KDF.Argon.minKeySize + 17 val size = KDF.Argon.minKeySize + 17
val expectedSalt = UByteArray(KDF.Argon.saltSize) { (it + 3).toUByte() } val domain = "kdf-size-in-bytes-test"
val expectedSalt = Hash.Blake2b.deriveSalt(domain, KDF.Argon.saltSize)
val kdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, expectedSalt) val kdf = KDF.Complexity.FixedLow.kdfForSizeInBytes(size, domain)
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
@ -99,9 +84,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) val b1 = kdf.deriveFromBytes(source, 80)
val b2 = kdf.deriveFromBytes(source) val b2 = kdf.deriveFromBytes(source, 80)
val b3 = kdf.deriveFromBytes(source + 1u) val b3 = kdf.deriveFromBytes(source + 1u, 80)
assertEquals(80, b1.size) assertEquals(80, b1.size)
assertContentEquals(b1, b2) assertContentEquals(b1, b2)
@ -115,22 +100,21 @@ 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) val b2 = KDF.Argon.create(KDF.Complexity.Interactive, salt, 64).deriveFromBytes(source, 64)
assertContentEquals(b1, b2) assertContentEquals(b1, b2)
assertEquals(64, b1.size) assertEquals(64, b1.size)
} }
@Test @Test
fun deriveFromBytesUsesKeySizeTest() = runTest { fun deriveFromBytesValidationTest() = runTest {
initCrypto() initCrypto()
val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() } val salt = UByteArray(KDF.Argon.saltSize) { it.toUByte() }
val source = ubyteArrayOf(1u, 2u, 3u) val kdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, KDF.Argon.minKeySize)
val shortKdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 64)
val longKdf = KDF.Argon.create(KDF.Complexity.Interactive, salt, 96)
assertEquals(64, shortKdf.deriveFromBytes(source).size) assertFailsWith<IllegalArgumentException> {
assertEquals(96, longKdf.deriveFromBytes(source).size) kdf.deriveFromBytes(ubyteArrayOf(1u, 2u, 3u), KDF.Argon.minKeySize - 1)
}
} }
} }