fixed SafeKeyEXchange.SessionKey serialization issue

This commit is contained in:
Sergey Chernov 2026-06-20 23:55:01 +03:00
parent 84a3e82216
commit 66254f8fa6
6 changed files with 73 additions and 25 deletions

1
.idea/misc.xml generated
View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">

View File

@ -13,14 +13,14 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("multiplatform") version "2.2.21"
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.21"
kotlin("multiplatform") version "2.4.0"
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.0"
id("org.jetbrains.dokka") version "1.9.20"
`maven-publish`
}
group = "net.sergeych"
version = "0.9.3"
version = "0.9.4-SNAPSHOT"
repositories {
mavenCentral()

View File

@ -155,15 +155,9 @@ sealed class Container {
abstract val decryptedWithKeyId: KeyId?
/**
* If the container _is decrypted by the [EncryptingPublicKey]_, e.g., using secret key encryption,
* contains the [EncryptingPublicKey] that corresponds the [DecryptingSecretKey] 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.
* When the container is decrypted, the key that unlocked it, otherwise null.
*/
@Transient
var authorisedByKey: EncryptingPublicKey? = null
protected set
abstract val decryptedWithKey: UniversalKey?
/**
* List of [KeyId] of the keys that unlocked the container, in the same order used for encryption..
@ -185,7 +179,8 @@ sealed class Container {
) : Container() {
@Transient
private var decryptedWithKey: DecryptingKey? = null
override var decryptedWithKey: UniversalKey? = null
private set
override val decryptedWithKeyId: KeyId?
get() = decryptedWithKey?.id
@ -202,10 +197,7 @@ sealed class Container {
if (k.id == keyId) {
kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let {
decryptedData = it
decryptedWithKey = k
if(k is DecryptingSecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedMessage).senderPublicKey
}
decryptedWithKey = k as UniversalKey
return it
}
}
@ -284,9 +276,12 @@ sealed class Container {
}
@Transient
override var decryptedWithKeyId: KeyId? = null
override var decryptedWithKey: UniversalKey? = null
private set
override val decryptedWithKeyId: KeyId?
get() = decryptedWithKey?.id
override val keyIds: List<KeyId> = encryptedKeys.map { it.tag }
override fun decryptWith(keyRing: UniversalRing): UByteArray? {
@ -302,11 +297,8 @@ sealed class Container {
}.getOrNull()?.let { k ->
if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure)
throw InvalidContainerException()
decryptedWithKeyId = key.id
decryptedWithKey = key as UniversalKey
mainKey = k
if(key is DecryptingSecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedKey.cipherData).senderPublicKey
}
}
if (decryptedData != null) return decryptedData
}

View File

@ -15,6 +15,7 @@ import com.ionspin.kotlin.crypto.keyexchange.KeyExchangeKeyPair
import com.ionspin.kotlin.crypto.keyexchange.crypto_kx_PUBLICKEYBYTES
import com.ionspin.kotlin.crypto.keyexchange.crypto_kx_SECRETKEYBYTES
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
@ -81,7 +82,7 @@ class SafeKeyExchange private constructor(
* 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
@Serializable(with = SafeKeyExchange.SessionKeySerializer::class)
class SessionKey(
val sendingKey: EncryptingKey,
val receivingKey: DecryptingKey,
@ -128,6 +129,36 @@ class SafeKeyExchange private constructor(
}
@Serializable
private class PackedSessionKey(
val sendingKey: SymmetricKey,
val receivingKey: SymmetricKey,
val isClient: Boolean,
)
object SessionKeySerializer : KSerializer<SessionKey> {
private val packedSerializer = PackedSessionKey.serializer()
override val descriptor: SerialDescriptor = packedSerializer.descriptor
override fun serialize(encoder: Encoder, value: SessionKey) {
val sendingKey = value.sendingKey as? SymmetricKey
?: throw SerializationException("SessionKey.sendingKey must be SymmetricKey")
val receivingKey = value.receivingKey as? SymmetricKey
?: throw SerializationException("SessionKey.receivingKey must be SymmetricKey")
encoder.encodeSerializableValue(
packedSerializer,
PackedSessionKey(sendingKey, receivingKey, value.isClient)
)
}
override fun deserialize(decoder: Decoder): SessionKey {
val packed = decoder.decodeSerializableValue(packedSerializer)
return SessionKey(packed.sendingKey, packed.receivingKey, packed.isClient)
}
}
/**
* 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

View File

@ -34,6 +34,8 @@ class ContainerTest {
assertNotNull(d)
assertContentEquals(data, d)
assertTrue { c1.isDecrypted }
assertEquals(syk1.id, c1.decryptedWithKeyId)
assertEquals(syk1, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded)
@ -61,7 +63,7 @@ class ContainerTest {
assertContentEquals(data, d)
assertTrue { c1.isDecrypted }
assertEquals(p2.publicKey.id, c1.decryptedWithKeyId)
assertEquals(p1.publicKey, c1.authorisedByKey)
assertEquals(p2.secretKey, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded)
@ -88,7 +90,7 @@ class ContainerTest {
assertContentEquals(data, d)
assertTrue { c1.isDecrypted }
assertEquals(p4.publicKey.id, c1.decryptedWithKeyId)
assertEquals(p1.publicKey, c1.authorisedByKey)
assertEquals(p4.secretKey, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded)
@ -338,4 +340,4 @@ class ContainerTest {
expectOpen(data2, syk3, p3.secretKey, p4.secretKey)
}
}
}

View File

@ -241,6 +241,28 @@ class KeysTest {
assertContentEquals(clientSessionKey.sessionTag, serverSessionKey.sessionTag)
}
@Test
fun sessionKeySerializationTest() = runTest {
initCrypto()
val ske = SafeKeyExchange()
val cke = SafeKeyExchange()
val clientSessionKey = cke.clientSessionKey(ske.publicKey)
val serverSessionKey = ske.serverSessionKey(cke.publicKey)
val restoredClientSessionKey = unpack<SafeKeyExchange.SessionKey>(pack(clientSessionKey))
val restoredServerSessionKey = unpack<SafeKeyExchange.SessionKey>(pack(serverSessionKey))
assertEquals(clientSessionKey, restoredClientSessionKey)
assertEquals(serverSessionKey, restoredServerSessionKey)
assertContentEquals(clientSessionKey.sessionTag, restoredClientSessionKey.sessionTag)
assertContentEquals(serverSessionKey.sessionTag, restoredServerSessionKey.sessionTag)
val src = "Hello from restored session keys!"
assertEquals(src, restoredServerSessionKey.decryptString(restoredClientSessionKey.encrypt(src)))
assertEquals(src, restoredClientSessionKey.decryptString(restoredServerSessionKey.encrypt(src)))
}
@Test
fun asymmetricKeyTest() = runTest {
initCrypto()