From 14a63a05c2f213d94acc1e2fb77ae0619fc28904 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 28 Aug 2024 09:24:20 +0200 Subject: [PATCH] - BinaryId is now open - Container returns data on the keys used to decrypt it --- build.gradle.kts | 2 +- .../kotlin/net/sergeych/crypto2/Asymmetric.kt | 12 +++++ .../kotlin/net/sergeych/crypto2/BinaryId.kt | 21 +++++++- .../kotlin/net/sergeych/crypto2/Container.kt | 48 +++++++++++++++---- .../net/sergeych/crypto2/DecryptingKey.kt | 3 +- .../kotlin/net/sergeych/crypto2/SecretKey.kt | 4 +- src/commonTest/kotlin/ContainerTest.kt | 31 +++++++++++- src/commonTest/kotlin/KeysTest.kt | 5 +- 8 files changed, 109 insertions(+), 17 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 84f1a12..faee797 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "net.sergeych" -version = "0.5.5" +version = "0.5.6-SNAPSHOT" repositories { mavenCentral() diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt index e11b34e..5c85910 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Asymmetric.kt @@ -4,6 +4,7 @@ import com.ionspin.kotlin.crypto.box.Box import com.ionspin.kotlin.crypto.box.BoxCorruptedOrTamperedDataException import com.ionspin.kotlin.crypto.box.crypto_box_NONCEBYTES import kotlinx.serialization.Serializable +import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackEncoder import net.sergeych.crypto2.Asymmetric.Message import net.sergeych.crypto2.Asymmetric.generateKeys @@ -64,6 +65,16 @@ object Asymmetric { } val encoded: UByteArray by lazy { BipackEncoder.encode(this).toUByteArray() } + + companion object { + fun decode(packed: UByteArray): Message = try { + BipackDecoder.decode(packed) + } + catch(x: Exception) { + throw DecryptionFailedException("can't decode message structure",x) + } + } + } /** @@ -97,6 +108,7 @@ object Asymmetric { fun newSecretKey() = SecretKey.new() val nonceBytesLength = crypto_box_NONCEBYTES + } /** diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt index e39df2a..21fa649 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt @@ -9,7 +9,7 @@ import net.sergeych.mp_tools.decodeBase64Url import kotlin.random.Random @Serializable -class BinaryId private constructor ( +open class BinaryId protected constructor ( val id: UByteArray, ) : Comparable { @@ -29,6 +29,25 @@ class BinaryId private constructor ( private val innerData: UByteArray by lazy { id.sliceArray( 1..< id.size-1 ) } + /** + * The id body: all the bytes except check and magic. These could carry useful information. + */ + val body: UByteArray by lazy { id.sliceArray( 0 until id.size-2 ) } + + val asVerifyingKey: VerifyingKey by lazy { + if( magic != KeysmagicNumber.defaultVerifying.ordinal) + throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultVerifying.ordinal}") + check(body.size == 32) + VerifyingPublicKey(body) + } + + val asPublicKey: PublicKey by lazy { + if( magic != KeysmagicNumber.defaultAssymmetric.ordinal) + throw InvalidException("It is not a veryfing key: magic=$magic, required ${KeysmagicNumber.defaultAssymmetric.ordinal}") + check(body.size == 32) + PublicKey(body) + } + override fun toString(): String = id.encodeToBase64Url() override fun compareTo(other: BinaryId): Int { diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt index 30ccd9a..42f4fa2 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt @@ -14,7 +14,7 @@ import net.sergeych.crypto2.Container.Companion.createWith * decrypt it. This is sometimes very important to be able to add recipients to the message * keeping existing recipients you know no keys of, or update the message when only one of the * keys is known. - * + * * - [createWith] for more on create a new container * - [decryptWith] to decrypt * - [addRecipients] and various [plus] operators to add recipients @@ -123,7 +123,7 @@ sealed class Container { * Update the data in the decrypted container. It keeps the same set of keys and update * the data only. */ - abstract fun updateData(newPlainData: UByteArray,randomFill: IntRange?=null): Container + abstract fun updateData(newPlainData: UByteArray, randomFill: IntRange? = null): Container /** * Binary encoded version. It is desirable to include [Container] as an object, though, @@ -135,6 +135,23 @@ sealed class Container { BipackEncoder.encode(this).toUByteArray() } + /** + * When the container is decrypted, the [KeyId] of the key that unlocked it, + * otherwise null. + */ + abstract val decryptedWithKeyId: KeyId? + + /** + * If the container _is decrypted by the [PublicKey]_, e.g., using secret key encryption, + * contains the [PublicKey] that corresponds the [SecretKey] used while encrypting, this + * authenticating the sender party cryptographically. This key could be used to encrypt + * the response to be visible to the sender only; the sender, providing it kept his secret key, + * could decrypt it. + */ + @Transient + var authorisedByKey: PublicKey? = null + protected set + /** * @suppress @@ -152,6 +169,9 @@ sealed class Container { @Transient private var decryptedWithKey: DecryptingKey? = null + override val decryptedWithKeyId: KeyId? + get() = decryptedWithKey?.id + init { decryptedData = creationData } @@ -163,6 +183,9 @@ sealed class Container { kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let { decryptedData = it decryptedWithKey = k + if( k is SecretKey) { + authorisedByKey = Asymmetric.Message.decode(encryptedMessage).senderPublicKey + } return it } } @@ -170,7 +193,7 @@ sealed class Container { return null } - private fun reEncrypt(data: UByteArray? = null,f: Builder.()->Unit): Container { + private fun reEncrypt(data: UByteArray? = null, f: Builder.() -> Unit): Container { check(isDecrypted) { "container should be decrypted" } return create(data ?: decryptedData ?: throw IllegalArgumentException("no data is provided")) { f() @@ -204,7 +227,7 @@ sealed class Container { reEncrypt { alwaysMulti() } } - override fun updateData(newPlainData: UByteArray,randomFill: IntRange?): Container = + override fun updateData(newPlainData: UByteArray, randomFill: IntRange?): Container = reEncrypt(newPlainData) { randomFill?.let { fill(it) } } @@ -220,7 +243,7 @@ sealed class Container { val encryptedKeys: List, val encryptedMessage: UByteArray, @Transient internal var mainKey: SymmetricKey? = null, @Transient internal var knownPlainData: UByteArray? = null, - ) : Container() { + ) : Container() { @Serializable class EncryptedKey(val tag: KeyId, val cipherData: UByteArray) { constructor(key: EncryptingKey, encodeMainKey: UByteArray) : @@ -240,6 +263,10 @@ sealed class Container { knownPlainData?.let { decryptedData = it } } + @Transient + override var decryptedWithKeyId: KeyId? = null + private set + override fun decryptWith(keyRing: UniversalRing): UByteArray? { decryptedData?.let { return it } for (key in keyRing.decryptingKeys) { @@ -251,10 +278,13 @@ sealed class Container { key.decrypt(encryptedKey.cipherData).toByteArray() ) }.getOrNull()?.let { k -> - println(k) if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure) throw InvalidContainerException() + decryptedWithKeyId = key.id mainKey = k + if( key is SecretKey) { + authorisedByKey = Asymmetric.Message.decode(encryptedKey.cipherData).senderPublicKey + } } if (decryptedData != null) return decryptedData } @@ -438,11 +468,11 @@ sealed class Container { @Suppress("unused") - inline fun encrypt(plainData: T, vararg keys: EncryptingKey): UByteArray = + inline fun encrypt(plainData: T, vararg keys: EncryptingKey): UByteArray = createWith(BipackEncoder.encode(plainData).toUByteArray(), *keys).encoded - inline fun decrypt(cipherData: UByteArray, vararg keys: DecryptingKey): T? = - decryptAsUBytes(cipherData,*keys)?.let { BipackDecoder.decode(it.asByteArray())} + inline fun decrypt(cipherData: UByteArray, vararg keys: DecryptingKey): T? = + decryptAsUBytes(cipherData, *keys)?.let { BipackDecoder.decode(it.asByteArray()) } fun decryptAsUBytes(cipherData: UByteArray, vararg keys: DecryptingKey): UByteArray? = decode(cipherData).decryptWith(*keys) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt index 13d1a17..460ff45 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt @@ -7,7 +7,8 @@ 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]. + * keys are wise to transfer/save. Concrete implementations are, like [SymmetricKey] or + * [SecretKey]. */ interface DecryptingKey : NonceBased, KeyInstance { /** diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SecretKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SecretKey.kt index 131c89e..96be114 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SecretKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SecretKey.kt @@ -5,7 +5,6 @@ import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import net.sergeych.bipack.BipackDecoder /** * The secret key used in public-key encryption; it is used to _decrypt_ data encrypted with its @@ -62,8 +61,7 @@ class SecretKey( * Decrypt without a nonce as edwards curve decryption does not need it */ override fun decrypt(cipherData: UByteArray): UByteArray { - val message: Asymmetric.Message = BipackDecoder.decode(cipherData.asByteArray()) - return message.decrypt(this) + return Asymmetric.Message.decode(cipherData).decrypt(this) } override val magic: KeysmagicNumber = KeysmagicNumber.defaultAssymmetric diff --git a/src/commonTest/kotlin/ContainerTest.kt b/src/commonTest/kotlin/ContainerTest.kt index 162e33a..572e1f4 100644 --- a/src/commonTest/kotlin/ContainerTest.kt +++ b/src/commonTest/kotlin/ContainerTest.kt @@ -50,11 +50,40 @@ class ContainerTest { assertNotNull(d) assertContentEquals(data, d) assertTrue { c1.isDecrypted } + assertEquals(p2.publicKey.id, c1.decryptedWithKeyId) + assertEquals(p1.publicKey, c1.authorisedByKey) val data2 = "To push unpushinable".encodeToUByteArray() val c2 = Container.decode(c.updateData(data2).encoded) assertFalse { c2.isDecrypted } - assertContentEquals(data2, c2.decryptWith(p2.secretKey)) + + } + @Test + fun testMultiplePair() = runTest { + initCrypto() + val p1 = Asymmetric.generateKeys() + val p2 = Asymmetric.generateKeys() + val p3 = Asymmetric.generateKeys() + val p4 = Asymmetric.generateKeys() + val data = "sergeych, ohm many.".encodeToUByteArray() + + val c = Container.createWith(data, p1.secretKey to p2.publicKey, p1.secretKey to p4.publicKey) + assertTrue { c.isDecrypted } + val c1 = Container.decode(c.encoded) + assertFalse { c1.isDecrypted } + + assertNull(c1.decryptWith(p3.secretKey, p1.secretKey)) + val d = c1.decryptWith(p3.secretKey, p4.secretKey) + assertNotNull(d) + assertContentEquals(data, d) + assertTrue { c1.isDecrypted } + assertEquals(p4.publicKey.id, c1.decryptedWithKeyId) + assertEquals(p1.publicKey, c1.authorisedByKey) + + val data2 = "To push unpushinable".encodeToUByteArray() + val c2 = Container.decode(c.updateData(data2).encoded) + assertFalse { c2.isDecrypted } + } @Test diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index 1b9ee2a..0e03723 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -164,7 +164,9 @@ class KeysTest { assertContentEquals(pk1.keyBytes, pk1.id.binaryTag.take(32).toUByteArray()) assertContentEquals(pk1.keyBytes, sk1.id.binaryTag.take(32).toUByteArray()) + assertEquals(pk1, pk1.id.id.asPublicKey) } + @Test fun asymmetricKeySerializationTest() = runTest { initCrypto() @@ -271,6 +273,7 @@ class KeysTest { // not hashed! assertContentEquals(k.verifyingKey.keyBytes, dk2.id.binaryTag.take(32).toUByteArray()) assertContentEquals(k.verifyingKey.keyBytes, dk1.id.binaryTag.take(32).toUByteArray()) - + // and restored from id should be the same: + assertEquals( k.verifyingKey, dk2.id.id.asVerifyingKey) } } \ No newline at end of file