From 382c3277afcf6df08f242bcc75191e205cf6e80d Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 11 Jun 2024 15:55:37 +0700 Subject: [PATCH] x25519 key exchange --- .../net/sergeych/crypto2/SafeKeyExchange.kt | 80 +++++++++++++++++++ .../net/sergeych/crypto2/SymmetricKey.kt | 2 +- src/commonTest/kotlin/KeysTest.kt | 22 +++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt new file mode 100644 index 0000000..913d37f --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt @@ -0,0 +1,80 @@ +package net.sergeych.crypto2 + +import com.ionspin.kotlin.crypto.keyexchange.KeyExchange +import kotlinx.serialization.Serializable +import net.sergeych.crypto2.SafeKeyExchange.SessionKey + +/** + * Bidirectional key, not necessarily symmetric. It could be asymmetric too. Examples: + * + * - [SafeKeyExchange] provides _asymmetric_ [SafeKeyExchange.SessionKey] + * - [SymmetricKey] is the symmetric cipher key + */ +interface CipherKey : EncryptingKey, DecryptingKey + +/** + * Safe (and typesafe) key exchange based on the x25519 protocol. + * + * Usage: + * + * 1. Create [SafeKeyExchange] on both server and client sides + * 2. Exchange [publicKey] instances + * 3. Create [serverSessionKey] and [clientSessionKey] respectively + * 4. Use [SessionKey.sendingKey] and [SessionKey.receivingKey] to send and receive encrypted data. + * + * Some important rules to keep security level high: + * + * - do not reuse [SafeKeyExchange] after session keys are generated, at least on the client. Create new + * instances as often as performance considerations allow. + * - while it is possible to generate several keys "ahead", the care should be taken when storing them, + * encrypt it with some other key to maintain safety. + * - do not use [publicKey] for anything but creating session keys. + */ +class SafeKeyExchange { + private val pair = KeyExchange.keypair() + + /** + * The session key. It uses a pair of keys to encrypt and decrypt messages to maintain high + * security level and allow using counters as nonce with no extra precautions. + */ + @Serializable + class SessionKey internal constructor( + val sendingKey: EncryptingKey, + val receivingKey: DecryptingKey, + ): CipherKey, EncryptingKey by sendingKey, DecryptingKey by receivingKey + + /** + * The public key; it should be transmitted to the other party, this is serializable. + * Do not use it except to get [SessionKey] with [clientSessionKey] or [serverSessionKey]. Storing and reusing + * it is a great danger. + */ + @Serializable + class PublicKey(val keyBytes: UByteArray) + + /** + * The public key to be sent to the other party. When received, get the session keys with [clientSessionKey] + * and [serverSessionKey] respectively. Do not reuse public key, do not use it for any other reason than + * to create a single session keys instance. For a new session, create new [SafeKeyExchange] at least on the + * client side, and get new keys. It is recommended to change server-side [SafeKeyExchange] + * instance every time or often enough. + */ + val publicKey = PublicKey(pair.publicKey) + + /** + * Create asymmetric [SessionKey] to encrypt the session. It can't decrypt what is encrypted by it + * but can decrypt what was encrypted with the [serverSessionKey] on the another side. + */ + fun clientSessionKey(serverPublicKey: PublicKey): SessionKey = + KeyExchange.clientSessionKeys(pair.publicKey, pair.secretKey, serverPublicKey.keyBytes) + .let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey))} + + /** + * Create an asymmetric [SessionKey] instance to work with [clientSessionKey] on the other side. + * Note that asymmetric means it can't decrypt what it encrypted. + */ + fun serverSessionKey(clientPublicKey: PublicKey): SessionKey = + KeyExchange.serverSessionKeys(pair.publicKey, pair.secretKey, clientPublicKey.keyBytes) + .let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey))} + + +} \ 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 045f127..34ad337 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt @@ -24,7 +24,7 @@ import kotlin.random.nextUBytes @Serializable class SymmetricKey( val keyBytes: UByteArray -): EncryptingKey, DecryptingKey { +): CipherKey { /** * @suppress diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index ec6e689..9c444a5 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -83,4 +83,26 @@ class KeysTest { } } + @Test + fun keyExchangeTest() = runTest { + initCrypto() + val ske = SafeKeyExchange() + val cke = SafeKeyExchange() + + val clientSessionKey = cke.clientSessionKey(ske.publicKey) + val serverSessionKey = ske.serverSessionKey(cke.publicKey) + + val src = "Hello, Dolly!" + assertEquals(src, serverSessionKey.decryptString(clientSessionKey.encrypt(src))) + assertEquals(src, serverSessionKey.decryptString(clientSessionKey.encrypt(src))) + assertEquals(src, serverSessionKey.decryptString(clientSessionKey.encrypt(src))) + assertEquals(src, serverSessionKey.decryptString(clientSessionKey.encrypt(src))) + + assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src))) + assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src))) + assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src))) + assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src))) + + } + } \ No newline at end of file