diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt index 868b690..969782a 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt @@ -1,5 +1,6 @@ package net.sergeych.crypto2 +import com.ionspin.kotlin.crypto.util.encodeToUByteArray import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import net.sergeych.bintools.CRC @@ -8,7 +9,7 @@ import net.sergeych.mp_tools.decodeBase64Url import kotlin.random.Random @Serializable -class BinaryId( +class BinaryId private constructor ( val id: UByteArray, ) : Comparable { @@ -57,8 +58,11 @@ class BinaryId( companion object { + /** + * Restore a string representation of existing BinaryId. + */ @Suppress("unused") - fun fromString(str: String): BinaryId = + fun restoreFromString(str: String): BinaryId = BinaryId(str.decodeBase64Url().toUByteArray()) @@ -81,5 +85,10 @@ class BinaryId( fun createRandom(magickNumber: Int, size: Int=16) = createFromBytes(magickNumber, Random.Default.nextBytes(size-2)) + /** + * Encode a string as UTF and create a binaryId from its bytes and provided magick. + */ + fun createFromString(magickNumber: Int, text: String): BinaryId = + createFromUBytes(magickNumber, text.encodeToUByteArray()) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/KeyDerivationParams.kt b/src/commonMain/kotlin/net/sergeych/crypto2/KeyDerivationParams.kt deleted file mode 100644 index c6ddd9d..0000000 --- a/src/commonMain/kotlin/net/sergeych/crypto2/KeyDerivationParams.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.sergeych.crypto2 - -import kotlinx.serialization.Serializable - -@Serializable -class KeyDerivationParams() \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt index 9d22198..36aa366 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt @@ -18,7 +18,7 @@ import kotlinx.serialization.Serializable * */ @Serializable -data class KeyId(val id: BinaryId, val kdf: KeyDerivationParams?=null ) { +data class KeyId(val id: BinaryId, val kdf: PBKD.Params?=null ) { /** * Binary array representation of the [id], not including the [kdf]. Used in [SafeKeyExchange] @@ -38,6 +38,6 @@ data class KeyId(val id: BinaryId, val kdf: KeyDerivationParams?=null ) { override fun toString() = id.toString() - constructor(magickNumber: KeysMagickNumber, keyData: UByteArray, kdp: KeyDerivationParams?=null) + constructor(magickNumber: KeysMagickNumber, keyData: UByteArray, kdp: PBKD.Params?=null) : this(BinaryId.createFromUBytes(magickNumber.number, blake2b3l(keyData)), kdp) } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/PBKD.kt b/src/commonMain/kotlin/net/sergeych/crypto2/PBKD.kt new file mode 100644 index 0000000..c544a80 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/PBKD.kt @@ -0,0 +1,109 @@ +package net.sergeych.crypto2 + +import com.ionspin.kotlin.crypto.util.encodeToUByteArray +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.sergeych.bipack.BipackEncoder +import net.sergeych.bipack.Unsigned +import net.sergeych.synctools.ProtectedOp +import net.sergeych.synctools.invoke + +object PBKD { + + @Serializable + class Params( + val kdf: KDF, + @Unsigned + val startOffset: Int, + @Transient + private var precalculatedKey: SymmetricKey?=null + ) { + + /** + * Derive a key from the password in the idempotent manner. + * If the [keyId] is provided, it will be used to determine key type (otherwise default asymmetric) + * and to check the result, and the exception will be thrown. + * + * Note that it is reasonably effective to use with linked keys restoring (keys based on the same password). + * It can also be called repeatedly. + * + * @param password to derive from + * @param keyId optional key id, to get a key type from and check the result + * @throws IncorrectPasswordException if [keyId] is provided and does not match the derivation result + */ + fun derive(password: String, keyId: KeyId? = null): SymmetricKey { + // could be already calculated + precalculatedKey?.let { return it } + + // check key id sanity + keyId?.let { + if (it.id.magick != KeysMagickNumber.defaultSymmetric.number) { + throw NotSupportedException("deriving key of type ${it.id.magick} is not implemented") + } + } + // this will not be ok when we support other key types: + val keySize = SymmetricKey.keyLength + + // derive + val bytes = generate(kdf, password) + val key = SymmetricKey(bytes.sliceArray(startOffset.. { + TODO() + } + + private val op = ProtectedOp() + + private class Entry(val kdf: KDF) { + val op = ProtectedOp() + var data: UByteArray? = null + } + + /** + * Binary id is a binary hash-capable key, we use it. byte array can't be a hash key in kotlin: its value + * does not depend on the byte content. + */ + fun key(kdf: KDF, password: String): BinaryId = + BinaryId.createFromBytes( + 0, + BipackEncoder.encode(kdf) + blake2b3l(password.encodeToUByteArray()).toByteArray() + ) + + private val cache = HashMap() + + /** + * Get a plain bytes by the KDF parameters specified. It implements short-term caching of + * KDF results this could be called repeatedly. + * + * When called from different threads, allows deriving in parallel, but this could deplete memory + * or CPU cores. + */ + fun generate(kdf: KDF, password: String): UByteArray { + val entry = op.invoke { + cache.getOrPut(key(kdf, password)) { Entry(kdf) } + } + return entry.op { + if (entry.data == null) { + entry.data = entry.kdf.derive(password) + } + entry.data!! + } + } +} + + + diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/PBKDParams.kt b/src/commonMain/kotlin/net/sergeych/crypto2/PBKDParams.kt deleted file mode 100644 index 287623f..0000000 --- a/src/commonMain/kotlin/net/sergeych/crypto2/PBKDParams.kt +++ /dev/null @@ -1,19 +0,0 @@ -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 3c80e1f..bf61570 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt @@ -1,8 +1,10 @@ package net.sergeych.crypto2 import com.ionspin.kotlin.crypto.secretbox.SecretBox +import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_KEYBYTES import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient /** * Symmetric key implements authenticated encrypting with random nonce and optional fill. @@ -18,6 +20,8 @@ import kotlinx.serialization.Serializable @Serializable class SymmetricKey( val keyBytes: UByteArray, + @Transient + val pbkdfParams: PBKD.Params?=null ) : EncryptingKey, DecryptingKey { /** @@ -31,13 +35,15 @@ class SymmetricKey( ) override fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange?): UByteArray { - require(nonce.size == nonceByteLength) + require(nonce.size == nonceLength) return SecretBox.easy(WithFill.encode(plainData, randomFill), nonce, keyBytes) } - override val nonceBytesLength: Int = nonceByteLength + override val nonceBytesLength: Int = nonceLength - override val id by lazy { KeyId(KeysMagickNumber.defaultSymmetric,blake2b3l(keyBytes)) } + override val id by lazy { + KeyId(KeysMagickNumber.defaultSymmetric,blake2b3l(keyBytes), pbkdfParams) + } override fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray = protectDecryption { @@ -62,7 +68,8 @@ class SymmetricKey( */ fun new() = SymmetricKey(SecretBox.keygen()) - val nonceByteLength = crypto_secretbox_NONCEBYTES + val nonceLength = crypto_secretbox_NONCEBYTES + val keyLength = crypto_secretbox_KEYBYTES } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/exceptions.kt b/src/commonMain/kotlin/net/sergeych/crypto2/exceptions.kt index f19823b..a114cea 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/exceptions.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/exceptions.kt @@ -12,4 +12,6 @@ class NonceOutOfBoundsException(text: String): Crypto2Exception(text) @Suppress("unused") class NotSupportedException(text: String="operation is not supported for this object") - : Crypto2Exception(text, null) \ No newline at end of file + : Crypto2Exception(text, null) + +class IncorrectPasswordException(): Crypto2Exception("Incorrect password") \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt index 8495df0..a8b8d1d 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt @@ -4,7 +4,6 @@ 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 { @@ -15,41 +14,7 @@ sealed class KDF { 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() - } - - } + abstract fun derive(password: String): UByteArray @Serializable @SerialName("argon") diff --git a/src/commonTest/kotlin/PBKDTest.kt b/src/commonTest/kotlin/PBKDTest.kt new file mode 100644 index 0000000..55d06dc --- /dev/null +++ b/src/commonTest/kotlin/PBKDTest.kt @@ -0,0 +1,9 @@ +import kotlin.test.Test + +class PBKDTest { + + @Test + fun testDerive() { + + } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/RingTest.kt b/src/commonTest/kotlin/RingTest.kt index 5fa7555..237a75c 100644 --- a/src/commonTest/kotlin/RingTest.kt +++ b/src/commonTest/kotlin/RingTest.kt @@ -70,6 +70,7 @@ class RingTest { println(r1.findKey(sk.id)) println(sk) assertTrue { sk.publicKey.toUniversalKey() == r1.findKey(sk.id) } + assertTrue { sk.publicKey.toUniversalKey() == r1.keyByTag("foo") } } @Test diff --git a/src/commonTest/kotlin/ToolsTest.kt b/src/commonTest/kotlin/ToolsTest.kt index 04fdc3b..9763fd9 100644 --- a/src/commonTest/kotlin/ToolsTest.kt +++ b/src/commonTest/kotlin/ToolsTest.kt @@ -1,4 +1,6 @@ import kotlinx.coroutines.test.runTest +import net.sergeych.bintools.encodeToHex +import net.sergeych.crypto2.BinaryId import net.sergeych.crypto2.Contrail import net.sergeych.crypto2.NumericNonce import net.sergeych.crypto2.initCrypto @@ -31,4 +33,26 @@ class ToolsTest { assertEquals(counter, t) assertContentEquals(x1.drop(2), x2.drop(2)) } + + @Test + fun testUBytesAsHashKeys() { + val k1 = BinaryId.createFromString(0,"Just the business") + val k2 = BinaryId.createFromString(0,"Just the business") + val l = mutableListOf() + for( b in k1.id) l += b + val k3 = BinaryId.createFromUBytes(0, k1.id.dropLast(2).toUByteArray() ) + + assertEquals(k1, k2) + + println(k1.id.encodeToHex()) + println(k3.id.encodeToHex()) + + assertEquals(k1, k3) + + val m = mutableMapOf(k1 to "foo") + m[k2] ="bar" + assertEquals("bar", m[k2]) + assertEquals("bar", m[k1]) + assertEquals("bar", m[k3]) + } } \ No newline at end of file