From fe0cb59c51b103428d2c49b70fb6bc5dfcae9bba Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 8 Jun 2024 16:18:07 +0700 Subject: [PATCH] migration to 2.0. New version with reinforced signed box to include timestamp and expiration --- .gitignore | 6 +- .idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml | 8 +++ .../artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml | 8 +++ .idea/misc.xml | 2 +- build.gradle.kts | 63 ++++++++----------- .../kotlin/net/sergeych/crypto2/Seal.kt | 57 ++++++++++++++++- .../kotlin/net/sergeych/crypto2/SignedBox.kt | 12 ++-- .../kotlin/net/sergeych/crypto2/SigningKey.kt | 13 +++- src/commonTest/kotlin/KeysTest.kt | 4 +- 9 files changed, 123 insertions(+), 50 deletions(-) create mode 100644 .idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml create mode 100644 .idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml diff --git a/.gitignore b/.gitignore index b63da45..8f23846 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,8 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +# Other +.kotlin +.idea diff --git a/.idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml b/.idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml new file mode 100644 index 0000000..552b3c5 --- /dev/null +++ b/.idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml b/.idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml new file mode 100644 index 0000000..5ec80b1 --- /dev/null +++ b/.idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 61efd00..76d6398 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 9f3f735..301e7ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,46 +1,35 @@ -import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType - plugins { - kotlin("multiplatform") version "1.9.20" - id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20" + kotlin("multiplatform") version "2.0.0" + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0" `maven-publish` } group = "net.sergeych" -version = "0.1.1-SNAPSHOT" +version = "0.2.1-SNAPSHOT" repositories { mavenCentral() maven("https://maven.universablockchain.com/") maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven") + mavenLocal() } kotlin { - jvm { - jvmToolchain(8) - withJava() - testRuns.named("test") { - executionTask.configure { - useJUnitPlatform() - } - } - } - js(KotlinJsCompilerType.IR) { - browser { - } - } - val hostOs = System.getProperty("os.name") - val isArm64 = System.getProperty("os.arch") == "aarch64" - val isMingwX64 = hostOs.startsWith("Windows") - @Suppress("UNUSED_VARIABLE") - val nativeTarget = when { - hostOs == "Mac OS X" && isArm64 -> macosArm64("native") - hostOs == "Mac OS X" && !isArm64 -> macosX64("native") - hostOs == "Linux" && isArm64 -> linuxArm64("native") - hostOs == "Linux" && !isArm64 -> linuxX64("native") - isMingwX64 -> mingwX64("native") - else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + jvm() +// { +// jvmToolchain(8) +// withJava() +// testRuns.named("test") { +// executionTask.configure { +// useJUnitPlatform() +// } +// } +// } + js(IR) { + browser() + nodejs() } + linuxX64("native") val ktor_version = "2.3.6" @@ -53,13 +42,13 @@ kotlin { val commonMain by getting { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0") api("com.ionspin.kotlin:bignum:0.3.8") - api("net.sergeych:mp_bintools:0.0.6") + api("net.sergeych:mp_bintools:0.1.5-SNAPSHOT") api("net.sergeych:mp_stools:1.4.1") } } @@ -80,11 +69,11 @@ kotlin { } } val jsTest by getting - val nativeMain by getting { - dependencies { - } - } - val nativeTest by getting +// val nativeMain by getting { +// dependencies { +// } +// } +// val nativeTest by getting } } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt index 33e5ff5..88acc7b 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt @@ -1,11 +1,64 @@ package net.sergeych.crypto2 +import kotlinx.datetime.Instant import kotlinx.serialization.Serializable +import net.sergeych.bipack.BipackEncoder +import net.sergeych.utools.now @Serializable class Seal( val publicKey: SigningKey.Public, - val signature: UByteArray + val signature: UByteArray, + val createdAt: Instant, + val expiresAt: Instant? = null, ) { - inline fun verify(message: UByteArray) = publicKey.verify(signature, message) + + @Suppress("unused") + @Serializable + class SealedData( + val message: UByteArray, + val createdAt: Instant?, + val validUntil: Instant?, + ) + + /** + * Return true if the seal is correct, see [verify] + */ + fun isValid(message: UByteArray): Boolean = kotlin.runCatching { verify(message) }.isSuccess + + /** + * Return Result containing success or error reason if the seal is not correct + */ + fun check(message: UByteArray): Result = kotlin.runCatching { verify(message) } + + /** + * Check that message is correct for this seal and throws exception if it is not. + * Note that tampering [createdAt] and [expiresAt] invalidate the seal too. + * + * See [check] and [isValid] for non-throwing checks. + * + * @throws ExpiredSignatureException + * @throws IllegalSignatureException + */ + fun verify(message: UByteArray) { + val n = now() + if (createdAt > n) throw IllegalSignatureException("signature's timestamp in the future") + expiresAt?.let { + if (n >= it) throw ExpiredSignatureException("signature expired at $it") + } + val data = BipackEncoder.encode(SealedData(message, createdAt, expiresAt)) + if (!publicKey.verify(signature, data.toUByteArray())) + throw IllegalSignatureException() + } + + companion object { + operator fun invoke( + key: SigningKey.Secret, message: UByteArray, + createdAt: Instant = now(), + expiresAt: Instant? = null, + ): Seal { + val data = BipackEncoder.encode(SealedData(message, createdAt, expiresAt)).toUByteArray() + return Seal(key.publicKey, key.sign(data), createdAt, expiresAt) + } + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt index 9431d1e..d7fdd86 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SignedBox.kt @@ -8,6 +8,9 @@ import kotlinx.serialization.Transient * instances and [SignedBox.plus] to add more signatures (signing keys), and * [SignedBox.contains] to check for a specific key signature presence. * + * Signatures, [Seal], incorporate creation time and optional expiration which are + * also signed and checked upon deserialization. + * * It is serializable and checks integrity on deserialization. If any of seals does not * match the signed [message], it throws [IllegalSignatureException] _on deserialization_. * E.g., if you have it deserialized, it is ok, check it contains all needed keys among @@ -31,7 +34,7 @@ class SignedBox( */ operator fun plus(key: SigningKey.Secret): SignedBox = if (key.publicKey in this) this - else SignedBox(message, seals + key.seal(message), false) + else SignedBox(message, seals + key.seal(message),false) /** * Check that it is signed with a specified key. @@ -43,7 +46,7 @@ class SignedBox( init { if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal") if (checkOnInit) { - if (!seals.all { it.verify(message) }) throw IllegalSignatureException() + for( s in seals ) s.verify(message) } } @@ -59,7 +62,8 @@ class SignedBox( * @param keys a list of keys to sign with, should be at least one key. * @throws IllegalArgumentException if keys are not specified. */ - operator fun invoke(data: UByteArray, vararg keys: SigningKey.Secret): SignedBox = - SignedBox(data, keys.map { it.seal(data) }, false) + operator fun invoke(data: UByteArray, vararg keys: SigningKey.Secret): SignedBox { + return SignedBox(data, keys.map { it.seal(data) }, false) + } } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt index fee666c..ded9ff8 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SigningKey.kt @@ -2,9 +2,11 @@ package net.sergeych.crypto2 import com.ionspin.kotlin.crypto.signature.InvalidSignatureException import com.ionspin.kotlin.crypto.signature.Signature +import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import net.sergeych.crypto2.SigningKey.Secret +import net.sergeych.crypto2.SigningKey.Companion.pair +import net.sergeych.utools.now /** * Keys in general: public, secret and later symmetric too. @@ -60,7 +62,9 @@ sealed class SigningKey { fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed) - fun seal(message: UByteArray): Seal = Seal(this.publicKey, sign(message)) + fun seal(message: UByteArray, validUntil: Instant? = null): Seal = + Seal(this, message, now(), validUntil) + override fun toString(): String = "Sct:${super.toString()}" } @@ -75,4 +79,7 @@ sealed class SigningKey { } } -class IllegalSignatureException: RuntimeException("signed data is tampered or signature is corrupted") +open class IllegalSignatureException(text: String="signed data is tampered or signature is corrupted") + : IllegalStateException(text) + +class ExpiredSignatureException(text: String): IllegalSignatureException(text) \ No newline at end of file diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index fd90354..a129d69 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -23,10 +23,10 @@ class KeysTest { val data = "8 rays dev!".encodeToUByteArray() val data1 = "8 rays dev!".encodeToUByteArray() val s = stk.seal(data) - assertTrue(s.verify(data)) + s.verify(data) data1[0] = 0x01u - assertFalse(s.verify(data1)) + assertFalse(s.isValid(data1)) val p2 = SigningKey.pair() val p3 = SigningKey.pair()