From 435604379edd4a32ed9823f06d41df12b12d70cc Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 23 Jun 2024 09:55:31 +0700 Subject: [PATCH] Asymmetric.Public now can be converted to Universal and stored into a ring --- .../kotlin/net/sergeych/crypto2/Asymmetric.kt | 26 ++++--- .../net/sergeych/crypto2/BinaryKeyBase.kt | 16 ----- .../kotlin/net/sergeych/crypto2/Container.kt | 4 +- .../net/sergeych/crypto2/DecryptingKey.kt | 4 +- .../net/sergeych/crypto2/EncryptingKey.kt | 4 +- .../net/sergeych/crypto2/KeyInstance.kt | 14 ++++ .../kotlin/net/sergeych/crypto2/SigningKey.kt | 9 +++ .../net/sergeych/crypto2/UniversalKey.kt | 15 +++- .../net/sergeych/crypto2/UniversalRing.kt | 70 ++++++++++++++++--- .../net/sergeych/crypto2/VerifyingKey.kt | 8 +++ src/commonTest/kotlin/KeysTest.kt | 4 +- src/commonTest/kotlin/RingTest.kt | 28 ++++++-- 12 files changed, 149 insertions(+), 53 deletions(-) create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/KeyInstance.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/VerifyingKey.kt diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt index 05a4e68..1d40f60 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt @@ -109,7 +109,7 @@ object Asymmetric { private fun randomNonce(): UByteArray = randomUBytes(crypto_box_NONCEBYTES) - fun new(): SecretKey = generateKeys().secretKey + fun newSecretKey(): SecretKey = generateKeys().secretKey @Suppress("unused") val nonceBytesLength = crypto_box_NONCEBYTES @@ -121,7 +121,7 @@ object Asymmetric { * Anonymous encryption is very slow in comparison. */ @Serializable - class PublicKey(override val keyBytes: UByteArray) : BinaryKeyBase() { + class PublicKey(override val keyBytes: UByteArray) : BinaryKeyBase(), KeyInstance { override val magick: KeysMagickNumber = KeysMagickNumber.defaultAssymmetric @@ -137,7 +137,7 @@ object Asymmetric { * proves that the message was not altered after creation. */ fun encryptAnonymousMessage(plainData: UByteArray, randomFill: IntRange? = null): Message = - encryptMessage(plainData,randomFill=randomFill) + encryptMessage(plainData, randomFill = randomFill) /** * Anonymous encryption, see [encryptAnonymousMessage], to binary data. Sender could not be identified. @@ -158,7 +158,7 @@ object Asymmetric { fun encryptMessage( plainData: UByteArray, nonce: UByteArray = randomNonce(), - senderKey: SecretKey = new(), + senderKey: SecretKey = newSecretKey(), randomFill: IntRange? = null, ) = createMessage(senderKey, this, WithFill.encode(plainData, randomFill), nonce) @@ -167,11 +167,14 @@ object Asymmetric { * [SecretKey] corresponding to this one, will be able to decrypt the message and be sure that [senderKey] * was the author and the message was not altered. */ - fun encryptMessage(plainData: UByteArray, - senderKey: SecretKey, - randomFill: IntRange? = null): Message = + fun encryptMessage( + plainData: UByteArray, + senderKey: SecretKey, + randomFill: IntRange? = null, + ): Message = createMessage(senderKey, this, WithFill.encode(plainData, randomFill)) + fun toUniversalKey(): UniversalKey.Public = UniversalKey.Public(this) } /** @@ -209,8 +212,11 @@ object Asymmetric { * The corresponding public key */ val publicKey: PublicKey by lazy { - PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes)) - .also { cachedPublicKey = it } + if (cachedPublicKey != null) + cachedPublicKey!! + else + PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes)) + .also { cachedPublicKey = it } } /** @@ -240,4 +246,4 @@ object Asymmetric { * Shortcut type: a pair of sender secret key and recipient private key could be used so * simplify such interfaces */ -typealias AsymmetricEncryptionPair = Pair +typealias AsymmetricEncryptionPair = Pair diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryKeyBase.kt b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryKeyBase.kt index 0547fdf..8ee950b 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryKeyBase.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryKeyBase.kt @@ -1,23 +1,7 @@ package net.sergeych.crypto2 -import kotlinx.datetime.Instant import kotlinx.serialization.Serializable -interface VerifyingKey { - val id: KeyId - - /** - * Verify the signature and return true if it is correct. - */ - fun verify(signature: UByteArray, message: UByteArray): Boolean -} - -interface SigningKey { - val verifyingKey: SigningPublicKey - fun sign(message: UByteArray): UByteArray - fun seal(message: UByteArray, expiresAt: Instant? = null): Seal -} - @Serializable abstract class BinaryKeyBase() { diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt index df146de..16ae69b 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt @@ -235,7 +235,7 @@ sealed class Container { recipient.id, recipient.encryptMessage( encodeMainKey, - senderKey = sender ?: Asymmetric.new(), + senderKey = sender ?: Asymmetric.newSecretKey(), ).encoded ) } @@ -391,7 +391,7 @@ sealed class Container { Single( pk.id, pk.encryptMessage( plainData, - senderKey = sk ?: Asymmetric.new(), + senderKey = sk ?: Asymmetric.newSecretKey(), randomFill = fillRange ).encoded, plainData, diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt index 9b47585..1fcc13e 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt @@ -9,7 +9,7 @@ import net.sergeych.crypto2.SymmetricKey.WithNonce * Some key able to perform decrypting. It is not serializable by purpose, as not all such * keys are wise to transfer/save. Concrete implementations are, like [SymmetricKey]. */ -interface DecryptingKey : NonceBased { +interface DecryptingKey : NonceBased, KeyInstance { /** * Authenticated decryption that checks the message is not tampered and therefor * the key is valid. It is not possible in general to distinguish whether the key is invalid @@ -28,8 +28,6 @@ interface DecryptingKey : NonceBased { fun decryptString(cipherData: UByteArray): String = decrypt(cipherData).decodeFromUByteArray() - val id: KeyId - } inline fun DecryptingKey.decryptObject(cipherData: UByteArray): T = diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt index 5145bf0..ee62e4b 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt @@ -11,7 +11,7 @@ import net.sergeych.crypto2.SymmetricKey.WithNonce * It is not serializable by design. * Custom implementations are, see [SymmetricKey] for example. */ -interface EncryptingKey : NonceBased { +interface EncryptingKey : NonceBased, KeyInstance { /** * Authenticated encrypting with optional random fill to protect from message size analysis. * Note that [randomFill] if present should be positive. @@ -27,8 +27,6 @@ interface EncryptingKey : NonceBased { fun encrypt(plainText: String,randomFill: IntRange? = null): UByteArray = encrypt(plainText.encodeToUByteArray(),randomFill) - val id: KeyId - fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange? = null): UByteArray } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/KeyInstance.kt b/src/commonMain/kotlin/net/sergeych/crypto2/KeyInstance.kt new file mode 100644 index 0000000..e393042 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/KeyInstance.kt @@ -0,0 +1,14 @@ +package net.sergeych.crypto2 + +interface KeyInstance { + val id: KeyId +} + +/** + * Create a new instance of the corresponding key. + */ +@Suppress("unused") +fun KeyInstance.toUniversalKey(): UniversalKey { + if (this is UniversalKey) return this + return UniversalKey.from(this) +} diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt new file mode 100644 index 0000000..91d13f9 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt @@ -0,0 +1,9 @@ +package net.sergeych.crypto2 + +import kotlinx.datetime.Instant + +interface SigningKey: KeyInstance { + val verifyingKey: SigningPublicKey + fun sign(message: UByteArray): UByteArray + fun seal(message: UByteArray, expiresAt: Instant? = null): Seal +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt index 1c610da..7531a89 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt @@ -4,8 +4,18 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +/** + * Serializable implementation of the _any key_ conception. Allows serializing collections + * of different keys with arbitrary types, such as [UniversalRing]. + * + * To create an `UniversalKey` instance use [UniversalKey.from] or [KeyInstance] + */ @Serializable sealed class UniversalKey { + /** + * Propagate real ley instance. Base class itself can't be a key instance, but + * has the same id. This allows requiring key instances in [UniversalRing]. + */ abstract val id: KeyId @Serializable @@ -84,17 +94,18 @@ sealed class UniversalKey { companion object { - fun from(key: DecryptingKey): UniversalKey = + fun from(key: KeyInstance): UniversalKey = when (key) { is UniversalKey -> key is Asymmetric.SecretKey -> Secret(key) + is Asymmetric.PublicKey -> Public(key) is SymmetricKey -> Symmetric(key) is SafeKeyExchange.SessionKey -> Session(key) else -> throw UnsupportedOperationException("can't create universal key from ${key::class.simpleName}") } fun newSecretKey(): Secret = - Secret(Asymmetric.new()) + Secret(Asymmetric.newSecretKey()) fun newSigningKey(): Signing = Signing(SigningSecretKey.new()) @Suppress("unused") diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt index bfb4a14..ab0a9bb 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt @@ -2,6 +2,17 @@ package net.sergeych.crypto2 import kotlinx.serialization.Serializable +/** + * Keyring capable of holding [UniversalKey] instances (serializable keys of different types) + * associated with a set of string tags. Rings are used to decrypt [Container], to store + * and find keys, etc. + * + * It is _immutable_ so it is safe to share id with no precautions on possible RC. Every + * function that modifies the ring returns a _new instance_ of it. + * + * __important__ serialized keyring is not protected and can disclose keys. Encrypt it after + * serializing before sending over the network or store on media. + */ @Serializable class UniversalRing( val keyWithTags: Map>, @@ -11,19 +22,44 @@ class UniversalRing( constructor(vararg keyTags: Pair) : this(keyTags.associate { it.first to setOf(it.second) }) - val decryptingKeys: Set by lazy { keys() } + /** + * Only decrypting keys. This is a shortcut for [keysOfType]: + * `keysOfType()`. + */ + val decryptingKeys: Set by lazy { keysOfType() } /** - * Select keys of the specified type + * Select all keys of the specified type. To select all keys, use [allKeys]. */ - inline fun keys(): Set = + inline fun keysOfType(): Set = allKeys.mapNotNull { it as? T }.toSet() val allKeys: Set by lazy { keyWithTags.keys } - - inline fun findKey(id: KeyId): UniversalKey? = - allKeys.find { it is T && it.id == id } + /** + * Find a key of the specified type that matches the id. __Important__ it is not possible to + * require [UniversalKey] as [T], it is not a [KeyInstance], while its descendants are, [UniversalKey.Secret], etc. + * You can freely use [UniversalKey] subtypes or general key interfaces, e.g. + * [EncryptingKey], [DecryptingKey], [SigningKey] and [VerifyingKey]. + * + * Please avoid selecting parameters that make possible to pick more than one key, it will cause an exception. + * + * @return found key by id of the specified type or null + * @throws IllegalArgumentException if there is more than one keys matching the criteria + */ + inline fun findKey(id: KeyId): T? { + val kk = allKeys.filter { it is T && it.id == id } + if( kk.size > 1 ) + throw IllegalArgumentException( + "ambiguous type selector ${T::class::simpleName}: ${kk.size} instances found, at most 1 allowed" + ) + return kk.firstOrNull() as T? + } + + /** + * 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 } /** @@ -40,8 +76,12 @@ class UniversalRing( */ inline fun keyByTag(tag: String) = keysByTags(tag).first { it is T } + /** + * Get keys of the specified type having any of the specified tags associated. + */ @Suppress("unused") - inline fun keyByAnyTag(vararg tags: String) = keysByTags(*tags).first { it is T } + inline fun keysByAnyTag(vararg tags: String): Sequence = + keysByTags(*tags).filter { it is T } /** * Get all keys with a given id. Note that _matching keys_ have the same id, see [KeyId] for more. @@ -72,7 +112,7 @@ class UniversalRing( * both rings. */ operator fun plus(other: UniversalRing): UniversalRing { - var result = keyWithTags.toMutableMap() + val result = keyWithTags.toMutableMap() for (e in other.keyWithTags.entries) { result[e.key]?.let { result[e.key] = it + e.value @@ -158,6 +198,7 @@ class UniversalRing( /** * Create string "report" of the ring contents. Note it has no trailing `\n` */ + @Suppress("unused") fun ls(): String { val result = mutableListOf() for( e in keyWithTags.entries) { @@ -182,8 +223,19 @@ class UniversalRing( return keyRings.reduce { l, r -> l + r } } - fun from(vararg keys: DecryptingKey): UniversalRing = + /** + * Convert any keys to [UniversalKey] and form a ring with it. + * @throws UnsupportedOperationException if [UniversalKey] can't build an instance of the specified class. + */ + fun from(vararg keys: KeyInstance): UniversalRing = UniversalRing(keys.associate { UniversalKey.from(it) to setOf() } ) + + /** + * Convert any keys to [UniversalKey] and form a ring with it. + * @throws UnsupportedOperationException if [UniversalKey] can't build an instance of the specified class. + */ + fun from(vararg keyTags: Pair): UniversalRing = + UniversalRing(keyTags.associate { UniversalKey.from(it.first) to setOf(it.second) } ) } } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingKey.kt new file mode 100644 index 0000000..2fbf483 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingKey.kt @@ -0,0 +1,8 @@ +package net.sergeych.crypto2 + +interface VerifyingKey: KeyInstance { + /** + * Verify the signature and return true if it is correct. + */ + fun verify(signature: UByteArray, message: UByteArray): Boolean +} \ No newline at end of file diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index c1a82c6..dd37b80 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -195,9 +195,9 @@ class KeysTest { assertEquals(usy2, usy1) assertFalse { usy1 == usy3 } - val sk1 = Asymmetric.new() + val sk1 = Asymmetric.newSecretKey() val sk2 = Asymmetric.SecretKey(sk1.keyBytes) - val sk3 = Asymmetric.new() + val sk3 = Asymmetric.newSecretKey() assertEquals(sk1, sk2) assertEquals(sk2, sk1) diff --git a/src/commonTest/kotlin/RingTest.kt b/src/commonTest/kotlin/RingTest.kt index 8cccfa1..5fa7555 100644 --- a/src/commonTest/kotlin/RingTest.kt +++ b/src/commonTest/kotlin/RingTest.kt @@ -16,7 +16,7 @@ class RingTest { val y2 = SymmetricKey("1234567890Hello,dolly.here-we-go".encodeToUByteArray()) assertEquals(y1, y2) - val e1 = Asymmetric.new() + val e1 = Asymmetric.newSecretKey() val e2: Asymmetric.SecretKey = BipackDecoder.decode(BipackEncoder.encode(e1)) assertEquals(e1, e2) @@ -26,8 +26,8 @@ class RingTest { assertEquals(k1, k11) - val k2 = UniversalKey.from(Asymmetric.new()) - val k3 = UniversalKey.from(Asymmetric.new()) + val k2 = UniversalKey.from(Asymmetric.newSecretKey()) + val k3 = UniversalKey.from(Asymmetric.newSecretKey()) // val r = UniversalRing(k1, k2) // val r = UniversalRing(k1) @@ -35,8 +35,8 @@ class RingTest { assertTrue(k1 in r) assertFalse { k3 in r } - println(Asymmetric.new().keyBytes.size) - println(BipackEncoder.encode(Asymmetric.new()).size) + println(Asymmetric.newSecretKey().keyBytes.size) + println(BipackEncoder.encode(Asymmetric.newSecretKey()).size) val encoded = BipackEncoder.encode(r) println(encoded.toDump()) println(encoded.size) @@ -56,12 +56,28 @@ class RingTest { assertEquals(r, r2) } + @Test + fun testAsymmetricPublic() = runTest { + initCrypto() + val sk = Asymmetric.newSecretKey() + assertEquals(sk.id, sk.publicKey.id) + val r = UniversalRing.from(sk.publicKey to "foo") + println(sk.publicKey.id) + println(sk.id) + println(r.findKey(sk.id)) + println(BipackEncoder.encode(r).toDump()) + val r1 = deepCopy(r) + println(r1.findKey(sk.id)) + println(sk) + assertTrue { sk.publicKey.toUniversalKey() == r1.findKey(sk.id) } + } + @Test fun testTags() = runTest { initCrypto() val k1 = UniversalKey.from(SymmetricKey("1234567890Hello,dolly.here-we-go".encodeToUByteArray())) - val k2 = UniversalKey.from(Asymmetric.new()) + val k2 = UniversalKey.from(Asymmetric.newSecretKey()) val r1 = UniversalRing(k1, k2) var r2 = UniversalRing(deepCopy(k1), deepCopy(k2))