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()