diff --git a/build.gradle.kts b/build.gradle.kts index 5b21e08..b6532d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt index 55a62bb..7b2f21b 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt @@ -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() + 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() diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/PBKD.kt b/src/commonMain/kotlin/net/sergeych/crypto2/PBKD.kt index a0ab153..6b3a435 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/PBKD.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/PBKD.kt @@ -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!! } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt index 07697cc..a3c686d 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt @@ -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 = allKeys.filter { it.id == id } + fun findById(id: KeyId): List = 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 keyByTag(tag: String) = keysByTags(tag).first { it is T } + inline fun 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 keysByAnyTag(vararg tags: String): Sequence = - 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): UniversalRing = UniversalRing(keyWithTags + (key to tags.toSet())) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt index 95fae61..1e05e23 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt @@ -34,7 +34,7 @@ sealed class KDF { * * Random salt of proper size is used */ - fun kdfForSize(numberOfKeys: Int): KDF = creteDefault(SymmetricKey.keyLength*numberOfKeys, this) + fun kdfForSize(numberOfKeys: Int): KDF = creteDefault(SymmetricKey.keyLength * numberOfKeys, this) /** * Derive multiple keys from the password. Derivation params will be included in the key ids, see @@ -49,20 +49,20 @@ sealed class KDF { */ @Suppress("unused") fun deriveMultiple(password: String, count: Int): List = - 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 { - val bytes = derive(password) + fun deriveMultipleKeys(password: String, count: Int): List { + 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.. { + /** * 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) diff --git a/src/commonTest/kotlin/HashTest.kt b/src/commonTest/kotlin/HashTest.kt index 9bc8693..28a0528 100644 --- a/src/commonTest/kotlin/HashTest.kt +++ b/src/commonTest/kotlin/HashTest.kt @@ -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 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) + } + } + } diff --git a/src/commonTest/kotlin/KDFTest.kt b/src/commonTest/kotlin/KDFTest.kt index 371fd18..4620930 100644 --- a/src/commonTest/kotlin/KDFTest.kt +++ b/src/commonTest/kotlin/KDFTest.kt @@ -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) } diff --git a/src/commonTest/kotlin/PBKDTest.kt b/src/commonTest/kotlin/PBKDTest.kt index 85a2495..9f10b39 100644 --- a/src/commonTest/kotlin/PBKDTest.kt +++ b/src/commonTest/kotlin/PBKDTest.kt @@ -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) diff --git a/src/commonTest/kotlin/RingTest.kt b/src/commonTest/kotlin/RingTest.kt index 81dc57b..ac010ed 100644 --- a/src/commonTest/kotlin/RingTest.kt +++ b/src/commonTest/kotlin/RingTest.kt @@ -157,14 +157,14 @@ class RingTest { assertEquals(a, r1.findKey(a.id)) assertEquals(a, r1.keyByTag("foo_a")) assertEquals(b, r1.findKey(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(a.id)) assertEquals(a, r1.keyByTag("foo_a")) assertEquals(b, r1.findKey(b.id)) - assertEquals(c, r1.keysById(c.id).first()) + assertEquals(c, r1.findById(c.id).first()) } } \ No newline at end of file