diff --git a/build.gradle.kts b/build.gradle.kts index 03fbc61..9db0ea5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ kotlin { dependencies { implementation(kotlin("test")) implementation("org.slf4j:slf4j-simple:2.0.9") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") } } val native by creating { diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt index c449cbb..7b82a33 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt @@ -135,7 +135,7 @@ object Asymmetric { } override fun hashCode(): Int { - return keyBytes.hashCode() + return keyBytes.contentHashCode() } /** @@ -234,7 +234,7 @@ object Asymmetric { } override fun hashCode(): Int { - return keyBytes.hashCode() + return keyBytes.contentHashCode() } /** diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/PBKDParams.kt b/src/commonMain/kotlin/net/sergeych/crypto2/PBKDParams.kt new file mode 100644 index 0000000..287623f --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/PBKDParams.kt @@ -0,0 +1,19 @@ +package net.sergeych.crypto2 + +import kotlinx.serialization.Serializable +import net.sergeych.bipack.Unsigned + +@Serializable +class PBKDParams( + val kdf: KDF, + @Unsigned + val offset: UInt, + @Unsigned + val length: UInt +) { +// val key by lazy { +// SymmetricKey(derivedBytes) +// } + +// val derivedBytes by lazy {kdf.derivedBytes} +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt index 512a636..f4d23f6 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt @@ -52,7 +52,7 @@ class SymmetricKey( } override fun hashCode(): Int { - return keyBytes.hashCode() + return keyBytes.contentHashCode() } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt index 838ceff..73892ad 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt @@ -28,10 +28,13 @@ class UniversalRing( if( keyTag in byTag ) UniversalRing(keySet.filter { it.tag != keyTag }) else this override fun equals(other: Any?): Boolean { + println("compare1\r\n") if (this === other) return true + println("compare2\r\n") if (other !is UniversalRing) return false - return size == other.size && keySet == other.keySet + println("compare2 ${size == other.size}: $size : ${other.size} | ${keySet == other.keySet}\r\n") + return size == other.size && keySet.containsAll(other.keySet) } override fun toString(): String { diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt new file mode 100644 index 0000000..8495df0 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt @@ -0,0 +1,157 @@ +package net.sergeych.crypto2 + +import com.ionspin.kotlin.crypto.pwhash.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.sergeych.bipack.Unsigned +import net.sergeych.synctools.ProtectedOp + +@Serializable +sealed class KDF { + + enum class Complexity { + Interactive, + Moderate, + Sensitive, + } + + fun derivedCached(password: String): UByteArray = + deriveCached(this, password) + + + abstract protected fun derive(password: String): UByteArray + + companion object { + private val op = ProtectedOp() + private val cache = HashMap() + + private class Entry(val kdf: KDF) { + val op = ProtectedOp() + var data: UByteArray? = null + +// fun generateCached(): UByteArray = op { +// if (data == null) +// data = kdf.derive() +// data!! +// } + + } + + +// fun key(kdf: KDF,password: String): KDF { +// return BipackEncoder.encode() +// } + fun deriveCached(kdf: KDF,password: String): UByteArray { + TODO() +// val entry = op { +// cache.getOrPut(kdf) { Entry(kdf) } +// } +// return entry.generateCached() + } + + } + + @Serializable + @SerialName("argon") + data class Argon( + val algorithm: Alg, + @Unsigned + val instructionsComplexity: ULong, + @Unsigned + val memComplexity: Int, + val salt: UByteArray, + @Unsigned + val keySize: Int, + ) : KDF(), Comparable { + + /** + * Very abstract strength comparison. If a1 > a2 it generally means that its complexity, and, hopefully, + * strength is higher. + */ + override fun compareTo(other: Argon): Int { + var d = algorithm.ordinal.compareTo(other.algorithm.ordinal) + if (d != 0) return d + d = instructionsComplexity.compareTo(other.instructionsComplexity) + if (d != 0) return d + d = memComplexity.compareTo(other.memComplexity) + if (d != 0) return d + d = keySize + if (d != 0) return d + return 0 + } + + enum class Alg(val code: Int) { + V2i_13(crypto_pwhash_argon2i_ALG_ARGON2I13), + V2id_13(crypto_pwhash_argon2id_ALG_ARGON2ID13), + ; + + companion object { + val default = V2id_13 + } + } + + override fun derive(password: String): UByteArray { + TODO() + //PasswordHash.pwhash(keySize, ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Argon) return false + + if (algorithm != other.algorithm) return false + if (instructionsComplexity != other.instructionsComplexity) return false + if (memComplexity != other.memComplexity) return false + if (keySize != other.keySize) return false + return salt contentEquals other.salt + } + + override fun hashCode(): Int { + var result = algorithm.hashCode() + result = 31 * result + instructionsComplexity.hashCode() + result = 31 * result + memComplexity + result = 31 * result + salt.contentHashCode() + result = 31 * result + keySize + return result + } + + + companion object { + val saltSize: Int = crypto_pwhash_SALTBYTES + val minKeySize: Int = crypto_pwhash_BYTES_MIN.toInt() + + fun randomSalt() = randomUBytes(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" } + return when (complexity) { + Complexity.Interactive -> Argon( + Alg.default, + crypto_pwhash_OPSLIMIT_INTERACTIVE.toULong(), + crypto_pwhash_MEMLIMIT_INTERACTIVE, + salt, keySize + ) + + Complexity.Moderate -> Argon( + Alg.default, + crypto_pwhash_OPSLIMIT_MODERATE, + crypto_pwhash_MEMLIMIT_MODERATE, + salt, keySize + ) + + Complexity.Sensitive -> Argon( + Alg.default, + crypto_pwhash_OPSLIMIT_SENSITIVE, + crypto_pwhash_MEMLIMIT_SENSITIVE, + salt, keySize + ) + } + } + } + } + + data class Instance(val kdf: KDF,val password: String) + +} + diff --git a/src/commonTest/kotlin/KDFTest.kt b/src/commonTest/kotlin/KDFTest.kt new file mode 100644 index 0000000..84b7189 --- /dev/null +++ b/src/commonTest/kotlin/KDFTest.kt @@ -0,0 +1,41 @@ +import kotlinx.coroutines.test.runTest +import net.sergeych.crypto2.KDF +import net.sergeych.crypto2.initCrypto +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class KDFTest { + @Test + fun testCacheKey() = runTest{ + initCrypto() + val k1 = KDF.Argon.create(KDF.Complexity.Interactive, KDF.Argon.randomSalt(), 128) + val k2 = KDF.Argon.create(KDF.Complexity.Interactive, k1.salt, 128) + + assertEquals(k1, k2) + assertEquals(k1.hashCode(), k2.hashCode()) + + val key1 = KDF.Instance(k1, "foo") + val key2 = KDF.Instance(k2, "foo") + val key3 = KDF.Instance(k2, "bar") + + assertEquals(key1, key2) + assertEquals(key1.hashCode(), key2.hashCode()) + assertFalse { key3 == key2 } + assertFalse { key3.hashCode() == key2.hashCode() } + + val m = mutableMapOf() + m[key1] = "foo" + m[key2] = "bar" + m[key3] = "foobar" + + assertEquals("bar", m[key1]) + assertEquals("bar", m[key2]) + assertEquals("foobar", m[key3]) + + val set1 = setOf(key1, key2, key3) + val set2 = setOf(key3, key2, key1) + assertEquals(set2, set1) + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index a1722ec..5fe5739 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -172,4 +172,76 @@ class KeysTest { // println(j.encodeToString(sk1)) } + + @Test + fun testUniKeys() = runTest { + initCrypto() + val sy1 = SymmetricKey.random() + val sy2 = SymmetricKey(sy1.keyBytes) + val sy3 = SymmetricKey.random() + + assertEquals(sy1, sy2) + assertEquals(sy2, sy1) + assertFalse { sy1 == sy3 } + + assertEquals(sy1, deepCopy(sy1), "symmetric key should be equal to the restored copy") + assertEquals(sy1.hashCode(), deepCopy(sy1).hashCode(), "hashcode of the restored symmetric key is wrong") + + val usy1 = UniversalKey.from(sy1) + val usy2 = UniversalKey.from(sy2) + val usy3 = UniversalKey.from(sy3) + + assertEquals(usy1, usy2) + assertEquals(usy2, usy1) + assertFalse { usy1 == usy3 } + + val sk1 = Asymmetric.randomSecretKey() + val sk2 = Asymmetric.SecretKey(sk1.keyBytes) + val sk3 = Asymmetric.randomSecretKey() + + assertEquals(sk1, sk2) + assertEquals(sk2, sk1) + assertFalse { sk1 == sk3 } + + var usk1 = UniversalKey.from(sk1) + var usk2 = UniversalKey.from(sk2) + var usk3 = UniversalKey.from(sk3) + val usk4 = UniversalKey.from(sy3) + + assertEquals(usk1, usk2) + assertEquals(usk2, usk1) + assertFalse { usk1 == usk3 } + + var a = setOf(sy1, sy2, sk1, sk2) + var b = setOf(sk1, sk2, sy2, sy1) + + assertEquals(a,b) + + a = setOf(usk1, usk2, usk3, usk4) + b = setOf(usk1, usk2, usk3, usk4) + + assertEquals(a, b) + // usk1 and usk2 are equal so set with only one of should be the same + assertEquals(a, setOf(usk1, usk3, usk4)) + + usk1 = deepCopy(usk1) + usk2 = deepCopy(usk2) + usk3 = deepCopy(usk3) + + assertEquals(usk1.hashCode(),usk2.hashCode()) + + assertEquals(usk1, usk2) + assertEquals(usk2, usk1) + assertFalse { usk1 == usk3 } + + a = setOf(usk1, usk2, usk3, usk4) + b = setOf(usk4, usk3, usk2, usk1) + + assertEquals(3, a.size) + assertEquals(3, b.size) + + assertEquals(a, b) + // usk1 and usk2 are equal so set with only one of should be the same + assertEquals(a, setOf(usk1, usk3, usk4)) + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/RingTest.kt b/src/commonTest/kotlin/RingTest.kt index d519dcd..ca22c68 100644 --- a/src/commonTest/kotlin/RingTest.kt +++ b/src/commonTest/kotlin/RingTest.kt @@ -46,9 +46,13 @@ class RingTest { assertContains(r2, k1) assertFalse { k3 in r2 } + println("\r\n") println(r) println(r2) +// Kr[U.Sym:XYjneNaPFbg-PZJIYjgIz7F-DsH1dEY8Mg6LCirko2QBFA,U.Sec:FzuzDbrS0xR5nTkdd-mYvrqsfQn9HbQgtFnIw9CEirIAlw] +// Kr[U.Sym:XYjneNaPFbg-PZJIYjgIz7F-DsH1dEY8Mg6LCirko2QBFA,U.Sec:FzuzDbrS0xR5nTkdd-mYvrqsfQn9HbQgtFnIw9CEirIAlw] + assertEquals(r, r2) } diff --git a/src/commonTest/kotlin/test_tools.kt b/src/commonTest/kotlin/test_tools.kt new file mode 100644 index 0000000..e8716ce --- /dev/null +++ b/src/commonTest/kotlin/test_tools.kt @@ -0,0 +1,5 @@ +import net.sergeych.bipack.BipackDecoder +import net.sergeych.bipack.BipackEncoder + +inline fun deepCopy(x: T):T = + BipackDecoder.decode(BipackEncoder.encode(x)) \ No newline at end of file