From a71a6665689640d30b8639984d275f28f1d5c8bc Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 12 Jun 2024 13:25:45 +0700 Subject: [PATCH] Public-key encryption --- .../net/sergeych/crypto2/AsymmetricKey.kt | 107 +++++++++++++++++- src/commonTest/kotlin/KeysTest.kt | 8 ++ 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt index 64e7833..d445c1e 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt @@ -6,22 +6,55 @@ import com.ionspin.kotlin.crypto.box.crypto_box_NONCEBYTES import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import net.sergeych.crypto2.Asymmetric.Message +import net.sergeych.crypto2.Asymmetric.PublicKey +import net.sergeych.crypto2.Asymmetric.SecretKey - +/** + * Public-key encryption implementation. Generally should be libsodium-compatible. + * + * ## How to + * + * - [Asymmetric.generateKeys] create a key pair. Keys are serializable. + * - [SecretKey] provides authenticated encryption for a [PublicKey] receiver. + * - [PublicKey] provides decryption and anonymous encryption. + * - [Message] is a serializable container with encrypted message and all necessary data to decrypt it. + * + * __Algorithms:__ + * + * - Key exchange: X25519. + * - Encryption: XSalsa20 + * - Authentication: Poly1305 + */ object Asymmetric { + /** + * Encrypted message holder. + * + * Do not instantiate it directly, use [SecretKey.encrypt], [PublicKey.encryptAnonymously] or [createMessage] + * instead. Also [SecretKey.decrypt] can be used to decrypt it same as [decrypt] or [decryptWithSenderKey]. + * + * To successfully decrypt the message, it is necessary to know a sender public key, and non-secret nonce. + * This class carries all this information; serialize and pass it to the recipient. + */ @Serializable class Message( private val nonce: UByteArray, private val encryptedMessage: UByteArray, val senderPublicKey: PublicKey? = null, ) { - + /** + * Decrypt the message, same as [SecretKey.decrypt] + */ fun decrypt(recipientKey: SecretKey): UByteArray { check(senderPublicKey != null) return decryptWithSenderKey(senderPublicKey, recipientKey) } + /** + * Decrypt a message which is not include sender's public key (which should somehow be + * known to the recipient). Use it if [senderPublicKey] is null. + */ fun decryptWithSenderKey(senderKey: PublicKey, recipientKey: SecretKey): UByteArray { return try { Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes) @@ -31,6 +64,21 @@ object Asymmetric { } } + /** + * Encrypt the [plainData] using [from] sender for [recipient] public key. Note that to decrypt it + * the [SecretKey] that corresponds to the [recipient] public key is needed, Sender can't decrypt the message! + * + * The authenticated encryption is used, is the message _is successfully decrypted_, it also means that + * it was signed by the sender, whose public key is known at the decryption time. + * + * When it is important not to provide senders' key, use [PublicKey.encryptAnonymously]. + * + * @param from the senders' secret key. + * @param recipient the recipients' public key. + * @param plainData data to encrypt + * @param excludeSenderKey if set to true, senders' public key will not be included in the message. + * In this case, the recipient must know it from other sources to be able to decrypt the message. + */ fun createMessage( from: SecretKey, recipient: PublicKey, plainData: UByteArray, excludeSenderKey: Boolean = false, @@ -43,8 +91,14 @@ object Asymmetric { ) } - data class KeyPair(val secretKey: SecretKey, public val senderKey: PublicKey) + /** + * The generated key pair. See [generateKeys] + */ + data class KeyPair(val secretKey: SecretKey,val senderKey: PublicKey) + /** + * Generate a new random pair of public and secret keys. + */ fun generateKeys(): KeyPair { val p = Box.keypair() val pk = PublicKey(p.publicKey) @@ -53,6 +107,10 @@ object Asymmetric { private fun randomNonce(): UByteArray = randomBytes(crypto_box_NONCEBYTES) + /** + * The public key used as the recipient for [Message] (see [SecretKey.encrypt], etc.). It also + * could be used to encrypt anonymous messages with [encryptAnonymously] + */ @Serializable class PublicKey(val keyBytes: UByteArray) { override fun equals(other: Any?): Boolean { @@ -62,11 +120,24 @@ object Asymmetric { return keyBytes contentEquals other.keyBytes } + /** + * Create an anonymous message that could be decrypted only with the [SecretKey] that corresponds this. + * Anonymous message uses one-time secret key, the public part of which is included into the + * [Message], so the sender could not be identified. + * The authentication is used despite the anonymity, and the fact of the successful decryption + * proves that the message was not altered after creation. + */ + fun encryptAnonymously(plainData: UByteArray): Message = + createMessage(generateKeys().secretKey, this, plainData) + override fun hashCode(): Int { return keyBytes.hashCode() } } + /** + * The secret key + */ @Serializable class SecretKey( val keyBytes: UByteArray, @@ -74,8 +145,38 @@ object Asymmetric { val _cachedPublicKey: PublicKey? = null, ) { + /** + * Encrypt the message for [recipient]. The [Message] will include the our [publicKey] + * and thus ready to be decrypted. + * + * The message is authenticated by this secret key. + */ + fun encrypt(plainData: UByteArray,recipient: PublicKey): Message = + createMessage(this, recipient, plainData) + + /** + * Decrypt with authentication checks the message which must have [Message.senderPublicKey] set. + * Use [decryptWithSenderKey] otherwise. Note that the authenticated encryption is always use, even if + * the [PublicKey.encryptAnonymously] was used to create a message, if it is successfully decrypted, + * it is guaranteed that the message was not altered after creation. + * + * @throws DecryptionFailedException If the message is tampered (changed after creation) or was not intended for us, + */ + fun decrypt(message: Message): UByteArray = message.decrypt(this) + + + /** + * Decrypt using [senderPublicKey] as a sender key (overriding the [Message.senderPublicKey] if set). + * See [decrypt] for more. + */ + fun decryptWithSenderKey(message: Message,senderPublicKey: PublicKey): UByteArray = + message.decryptWithSenderKey(senderPublicKey,this) + private var cachedPublicKey: PublicKey? = _cachedPublicKey + /** + * The corresponding public key + */ val publicKey: PublicKey by lazy { PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes)) .also { cachedPublicKey = it } diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index 9d77807..acaf34e 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -124,6 +124,14 @@ class KeysTest { assertContentEquals(plain, m.decrypt(sk2)) } + m = pk2.encryptAnonymously(plain) + assertContentEquals(plain, m.decrypt(sk2)) + assertContentEquals(plain, sk2.decrypt(m)) + assertContentEquals(plain, sk2.decrypt(sk1.encrypt(plain, pk2))) + assertThrows { + assertContentEquals(plain, m.decrypt(sk1)) + } + } @Test fun asymmetricKeySerializationTest() = runTest {