forked from sergeych/crypto2
migration to 2.0. New version with reinforced signed box to include timestamp and expiration
This commit is contained in:
parent
b86c7a853e
commit
fe0cb59c51
4
.gitignore
vendored
4
.gitignore
vendored
@ -40,3 +40,7 @@ bin/
|
|||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.kotlin
|
||||||
|
.idea
|
||||||
|
8
.idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml
generated
Normal file
8
.idea/artifacts/crypto2_js_0_1_1_SNAPSHOT.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<component name="ArtifactManager">
|
||||||
|
<artifact type="jar" name="crypto2-js-0.1.1-SNAPSHOT">
|
||||||
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
|
<root id="archive" name="crypto2-js-0.1.1-SNAPSHOT.jar">
|
||||||
|
<element id="module-output" name="crypto2.jsMain" />
|
||||||
|
</root>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
8
.idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml
generated
Normal file
8
.idea/artifacts/crypto2_jvm_0_1_1_SNAPSHOT.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<component name="ArtifactManager">
|
||||||
|
<artifact type="jar" name="crypto2-jvm-0.1.1-SNAPSHOT">
|
||||||
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
|
<root id="archive" name="crypto2-jvm-0.1.1-SNAPSHOT.jar">
|
||||||
|
<element id="module-output" name="crypto2.jvmMain" />
|
||||||
|
</root>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -4,7 +4,7 @@
|
|||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -1,46 +1,35 @@
|
|||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "1.9.20"
|
kotlin("multiplatform") version "2.0.0"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.1.1-SNAPSHOT"
|
version = "0.2.1-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://maven.universablockchain.com/")
|
maven("https://maven.universablockchain.com/")
|
||||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||||
|
mavenLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm {
|
jvm()
|
||||||
jvmToolchain(8)
|
// {
|
||||||
withJava()
|
// jvmToolchain(8)
|
||||||
testRuns.named("test") {
|
// withJava()
|
||||||
executionTask.configure {
|
// testRuns.named("test") {
|
||||||
useJUnitPlatform()
|
// executionTask.configure {
|
||||||
}
|
// useJUnitPlatform()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
js(KotlinJsCompilerType.IR) {
|
// }
|
||||||
browser {
|
js(IR) {
|
||||||
}
|
browser()
|
||||||
}
|
nodejs()
|
||||||
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.")
|
|
||||||
}
|
}
|
||||||
|
linuxX64("native")
|
||||||
|
|
||||||
val ktor_version = "2.3.6"
|
val ktor_version = "2.3.6"
|
||||||
|
|
||||||
@ -53,13 +42,13 @@ kotlin {
|
|||||||
|
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
|
||||||
|
|
||||||
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
|
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
|
||||||
api("com.ionspin.kotlin:bignum:0.3.8")
|
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")
|
api("net.sergeych:mp_stools:1.4.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,11 +69,11 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsTest by getting
|
val jsTest by getting
|
||||||
val nativeMain by getting {
|
// val nativeMain by getting {
|
||||||
dependencies {
|
// dependencies {
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
val nativeTest by getting
|
// val nativeTest by getting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,64 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import net.sergeych.bipack.BipackEncoder
|
||||||
|
import net.sergeych.utools.now
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Seal(
|
class Seal(
|
||||||
val publicKey: SigningKey.Public,
|
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<Unit> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -8,6 +8,9 @@ import kotlinx.serialization.Transient
|
|||||||
* instances and [SignedBox.plus] to add more signatures (signing keys), and
|
* instances and [SignedBox.plus] to add more signatures (signing keys), and
|
||||||
* [SignedBox.contains] to check for a specific key signature presence.
|
* [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
|
* It is serializable and checks integrity on deserialization. If any of seals does not
|
||||||
* match the signed [message], it throws [IllegalSignatureException] _on deserialization_.
|
* 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
|
* 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 =
|
operator fun plus(key: SigningKey.Secret): SignedBox =
|
||||||
if (key.publicKey in this) this
|
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.
|
* Check that it is signed with a specified key.
|
||||||
@ -43,7 +46,7 @@ class SignedBox(
|
|||||||
init {
|
init {
|
||||||
if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal")
|
if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal")
|
||||||
if (checkOnInit) {
|
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.
|
* @param keys a list of keys to sign with, should be at least one key.
|
||||||
* @throws IllegalArgumentException if keys are not specified.
|
* @throws IllegalArgumentException if keys are not specified.
|
||||||
*/
|
*/
|
||||||
operator fun invoke(data: UByteArray, vararg keys: SigningKey.Secret): SignedBox =
|
operator fun invoke(data: UByteArray, vararg keys: SigningKey.Secret): SignedBox {
|
||||||
SignedBox(data, keys.map { it.seal(data) }, false)
|
return SignedBox(data, keys.map { it.seal(data) }, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,9 +2,11 @@ package net.sergeych.crypto2
|
|||||||
|
|
||||||
import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
|
import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
|
||||||
import com.ionspin.kotlin.crypto.signature.Signature
|
import com.ionspin.kotlin.crypto.signature.Signature
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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.
|
* 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 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()}"
|
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)
|
@ -23,10 +23,10 @@ class KeysTest {
|
|||||||
val data = "8 rays dev!".encodeToUByteArray()
|
val data = "8 rays dev!".encodeToUByteArray()
|
||||||
val data1 = "8 rays dev!".encodeToUByteArray()
|
val data1 = "8 rays dev!".encodeToUByteArray()
|
||||||
val s = stk.seal(data)
|
val s = stk.seal(data)
|
||||||
assertTrue(s.verify(data))
|
s.verify(data)
|
||||||
|
|
||||||
data1[0] = 0x01u
|
data1[0] = 0x01u
|
||||||
assertFalse(s.verify(data1))
|
assertFalse(s.isValid(data1))
|
||||||
val p2 = SigningKey.pair()
|
val p2 = SigningKey.pair()
|
||||||
val p3 = SigningKey.pair()
|
val p3 = SigningKey.pair()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user