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"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">

View File

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

View File

@ -155,15 +155,9 @@ sealed class Container {
abstract val decryptedWithKeyId: KeyId? abstract val decryptedWithKeyId: KeyId?
/** /**
* If the container _is decrypted by the [EncryptingPublicKey]_, e.g., using secret key encryption, * When the container is decrypted, the key that unlocked it, otherwise null.
* 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.
*/ */
@Transient abstract val decryptedWithKey: UniversalKey?
var authorisedByKey: EncryptingPublicKey? = null
protected set
/** /**
* List of [KeyId] of the keys that unlocked the container, in the same order used for encryption.. * 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() { ) : Container() {
@Transient @Transient
private var decryptedWithKey: DecryptingKey? = null override var decryptedWithKey: UniversalKey? = null
private set
override val decryptedWithKeyId: KeyId? override val decryptedWithKeyId: KeyId?
get() = decryptedWithKey?.id get() = decryptedWithKey?.id
@ -202,10 +197,7 @@ sealed class Container {
if (k.id == keyId) { if (k.id == keyId) {
kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let { kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let {
decryptedData = it decryptedData = it
decryptedWithKey = k decryptedWithKey = k as UniversalKey
if(k is DecryptingSecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedMessage).senderPublicKey
}
return it return it
} }
} }
@ -284,9 +276,12 @@ sealed class Container {
} }
@Transient @Transient
override var decryptedWithKeyId: KeyId? = null override var decryptedWithKey: UniversalKey? = null
private set private set
override val decryptedWithKeyId: KeyId?
get() = decryptedWithKey?.id
override val keyIds: List<KeyId> = encryptedKeys.map { it.tag } override val keyIds: List<KeyId> = encryptedKeys.map { it.tag }
override fun decryptWith(keyRing: UniversalRing): UByteArray? { override fun decryptWith(keyRing: UniversalRing): UByteArray? {
@ -302,11 +297,8 @@ sealed class Container {
}.getOrNull()?.let { k -> }.getOrNull()?.let { k ->
if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure) if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure)
throw InvalidContainerException() throw InvalidContainerException()
decryptedWithKeyId = key.id decryptedWithKey = key as UniversalKey
mainKey = k mainKey = k
if(key is DecryptingSecretKey) {
authorisedByKey = Asymmetric.Message.decode(encryptedKey.cipherData).senderPublicKey
}
} }
if (decryptedData != null) return decryptedData 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_PUBLICKEYBYTES
import com.ionspin.kotlin.crypto.keyexchange.crypto_kx_SECRETKEYBYTES import com.ionspin.kotlin.crypto.keyexchange.crypto_kx_SECRETKEYBYTES
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder 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 * 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. * security level and allow using counters as nonce with no extra precautions.
*/ */
@Serializable @Serializable(with = SafeKeyExchange.SessionKeySerializer::class)
class SessionKey( class SessionKey(
val sendingKey: EncryptingKey, val sendingKey: EncryptingKey,
val receivingKey: DecryptingKey, 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. * 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 * 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) assertNotNull(d)
assertContentEquals(data, d) assertContentEquals(data, d)
assertTrue { c1.isDecrypted } assertTrue { c1.isDecrypted }
assertEquals(syk1.id, c1.decryptedWithKeyId)
assertEquals(syk1, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray() val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded) val c2 = Container.decode(c.updateData(data2).encoded)
@ -61,7 +63,7 @@ class ContainerTest {
assertContentEquals(data, d) assertContentEquals(data, d)
assertTrue { c1.isDecrypted } assertTrue { c1.isDecrypted }
assertEquals(p2.publicKey.id, c1.decryptedWithKeyId) assertEquals(p2.publicKey.id, c1.decryptedWithKeyId)
assertEquals(p1.publicKey, c1.authorisedByKey) assertEquals(p2.secretKey, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray() val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded) val c2 = Container.decode(c.updateData(data2).encoded)
@ -88,7 +90,7 @@ class ContainerTest {
assertContentEquals(data, d) assertContentEquals(data, d)
assertTrue { c1.isDecrypted } assertTrue { c1.isDecrypted }
assertEquals(p4.publicKey.id, c1.decryptedWithKeyId) assertEquals(p4.publicKey.id, c1.decryptedWithKeyId)
assertEquals(p1.publicKey, c1.authorisedByKey) assertEquals(p4.secretKey, c1.decryptedWithKey)
val data2 = "To push unpushinable".encodeToUByteArray() val data2 = "To push unpushinable".encodeToUByteArray()
val c2 = Container.decode(c.updateData(data2).encoded) val c2 = Container.decode(c.updateData(data2).encoded)

View File

@ -241,6 +241,28 @@ class KeysTest {
assertContentEquals(clientSessionKey.sessionTag, serverSessionKey.sessionTag) 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 @Test
fun asymmetricKeyTest() = runTest { fun asymmetricKeyTest() = runTest {
initCrypto() initCrypto()