diff --git a/build.gradle.kts b/build.gradle.kts index 3a9be63..15c61a3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "net.sergeych" -version = "0.5.7" +version = "0.5.8-SNAPSHOT" repositories { mavenCentral() diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/ByteChunk.kt b/src/commonMain/kotlin/net/sergeych/crypto2/ByteChunk.kt new file mode 100644 index 0000000..457a622 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/ByteChunk.kt @@ -0,0 +1,76 @@ +package net.sergeych.crypto2 + +import kotlinx.serialization.Serializable +import net.sergeych.bintools.decodeHex +import net.sergeych.bintools.encodeToHex +import kotlin.math.min + +/** + * Bytes sequence with comparison, concatenation, and string representation, + * could be used as hash keys for pure binary values, etc. + */ +@Suppress("unused") +@Serializable +class ByteChunk(val data: UByteArray): Comparable { + + val size: Int get() = data.size + + /** + * Per-byte comparison also of different length. From two chunks + * of different size but equal beginning, the shorter is considered + * the smaller. + */ + override fun compareTo(other: ByteChunk): Int { + val limit = min(size, other.size) + for( i in 0 ..< limit) { + val own = data[i] + val their = other.data[i] + if( own < their) return -1 + else if( own > their) return 1 + } + if( size < other.size ) return -1 + if( size > other.size ) return 1 + return 0 + } + + /** + * Equal chunks means content equality. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ByteChunk) return false + + return data contentEquals other.data + } + + /** + * Content-based hash code + */ + override fun hashCode(): Int { + return data.contentHashCode() + } + + /** + * hex representation of data + */ + override fun toString(): String = hex + + /** + * Hex encoded data + */ + val hex by lazy { data.encodeToHex() } + + /** + * human-readable dump + */ + val dump by lazy { data.toDump() } + + /** + * Concatenate two chunks and return new one + */ + operator fun plus(other: ByteChunk): ByteChunk = ByteChunk(data + other.data) + + companion object { + fun fromHex(hex: String): ByteChunk = ByteChunk(hex.decodeHex().asUByteArray()) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt index 25d2982..8f0af25 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt @@ -28,6 +28,8 @@ data class KeyId(val id: BinaryId, val kdp: PBKD.Params? = null) { /** * Binary array representation of the [id], not including the [kdp]. Used in [SafeKeyExchange] * and other key exchanges to generate session tokens, etc. + * + * In shortcut for packed [BinaryId], from [id]. If you need only key bytes, use [UniversalKey.keyBytes]. */ val binaryTag: UByteArray by lazy { id.id } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Multikey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Multikey.kt new file mode 100644 index 0000000..56f7b52 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Multikey.kt @@ -0,0 +1,178 @@ +package net.sergeych.crypto2 + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.sergeych.crypto2.Multikey.Companion.allOf +import net.sergeych.crypto2.Multikey.Companion.allOfMultikeys +import net.sergeych.crypto2.Multikey.Companion.anyOf +import net.sergeych.crypto2.Multikey.Companion.anyOfMultikeys +import net.sergeych.crypto2.Multikey.Companion.invoke +import net.sergeych.crypto2.Multikey.Companion.someOf +import net.sergeych.crypto2.Multikey.Companion.someOfMultikeys + +/** + * Multi-signed key. + * An arbitrary combination of [VerifyingPublicKey] to implement any multiple keys scenario, like N of M, + * and logical expression. Sample usage: + * + * ```kotlin + * val k1 = SigningSecretKey.new().verifyingKey + * val k2 = SigningSecretKey.new().verifyingKey + * val k3 = SigningSecretKey.new().verifyingKey + * val k4 = SigningSecretKey.new().verifyingKey + * + * val multikey = (k1 or k2) and (k3 or k4) + * + * val b: SealedBox = SealedBox.decode(someData) + * + * if( b.isSealedBy(multikey) ) { + * println("sealed box is properly sealed by a multikey") + * } + * ``` + * To build multikeys, use `and` and `or` infix operators against [VerifyingPublicKey], [Multikey], or even + * [SigningSecretKey] instances, and shortcut methods: + * + * - [someOfMultikeys], [someOf] family for `n of M` logic + * - [anyOfMultikeys], [anyOf], [allOf], and [allOfMultikeys] + * - [invoke] for a single-key multikey + * + * __Important__. When serializing, always serialize as root [Multikey] instance to keep + * it compatible with any combination. + */ +@Serializable +sealed class Multikey { + + /** + * Check that the [keys] satisfy the condition of this instance + */ + abstract fun check(keys: Iterable): Boolean + + /** + * Check that [verifyingKeys] satisfy the multikey condition + */ + fun check(vararg verifyingKeys: VerifyingPublicKey): Boolean = check(verifyingKeys.asIterable()) + + infix fun or(mk: Multikey): Multikey = SomeOf(1, listOf(this,mk)) + + infix fun or(k: VerifyingPublicKey) = SomeOf( 1, listOf(this, Multikey(k))) + + infix fun or(k: SigningSecretKey) = SomeOf( 1, listOf(this, Multikey(k.verifyingKey))) + + infix fun and(mk: Multikey): Multikey = SomeOf(2, listOf(this,mk)) + + infix fun and(k: VerifyingPublicKey) = SomeOf( 2, listOf(this, Multikey(k))) + + infix fun and(k: SigningSecretKey) = SomeOf( 2, listOf(this, Multikey(k.verifyingKey))) + + /** + * Multikey instance implementing `m of N` logic against [VerifyingPublicKey] set. Do not use + * it directly, use any [Multikey.someOfMultikeys] functions instead. + */ + @Serializable + @SerialName("k") + class Keys internal constructor(val requiredMinimum: Int, val validKeys: Set) : Multikey() { + override fun check(keys: Iterable): Boolean { + var matches = 0 + for( signer in keys ) { + if( signer in validKeys) { + if( ++matches >= requiredMinimum ) return true + } + } + return false + } + } + + /** + * Multikey instance implementing `m of N` logic against other [Multikey] instances. Do not use + * it directly, use any [Multikey.someOfMultikeys] functions instead. + */ + @Serializable + @SerialName("n") + class SomeOf internal constructor(val requiredMinimum: Int,val validKeys: List) : Multikey() { + override fun check(keys: Iterable): Boolean { + var matches = 0 + for( k in validKeys ) { + if( k.check(keys) ) { + if( ++matches >= requiredMinimum ) return true + } + } + return false + } + } + + + companion object { + + operator fun invoke(k: SigningSecretKey): Multikey = Keys(1, setOf( k.verifyingKey)) + operator fun invoke(k: VerifyingPublicKey): Multikey = Keys(1, setOf( k)) + + /** + * Create a multikey instance that requires some keys from a list + */ + fun someOf(requiredMinimum: Int, vararg keys: VerifyingPublicKey): Multikey = + Keys(requiredMinimum, keys.toSet()) + + /** + * Create a multikey instance that requires some keys from a list + */ + fun someOfMultikeys(requiredMinimum: Int, vararg keys: Multikey): Multikey = + SomeOf(requiredMinimum, keys.toList()) + + /** + * Create a multikey instance that requires some keys from a list + */ + fun someOfMultikeys(requiredMinimum: Int, keys: List): Multikey = + SomeOf(requiredMinimum, keys) + + /** + * Create a multikey instance that requires some keys from a list + */ + fun someOf(requiredMinimum: Int, keys: List): Multikey = + Keys(requiredMinimum, keys.toSet()) + + /** + * Create a multikey instance that requires any key from a list + */ + fun anyOf(vararg keys: VerifyingPublicKey): Multikey = someOf(1, *keys) + + /** + * Create a multikey instance that requires any key from a list + */ + fun anyOfMultikeys(vararg keys: Multikey): Multikey = someOfMultikeys(1, *keys) + + /** + * Create a multikey instance that requires any key from a list + */ + fun anyOfMultikeys(keys: List): Multikey = someOfMultikeys(1, keys) + + /** + * Create a multikey instance that requires any key from a list + */ + fun anyOf(keys: List): Multikey = someOf(1, keys) + + + /** + * Create a multikey instance that requires all keys from a list + */ + fun allOf(vararg keys: VerifyingPublicKey): Multikey = someOf(keys.size, *keys) + + /** + * Create a multikey instance that requires all keys from a list + */ + fun allOfMultikeys(vararg keys: Multikey): Multikey = someOfMultikeys(keys.size, *keys) + + /** + * Create a multikey instance that requires all keys from a list + */ + fun allOfMultikeys(keys: List): Multikey = someOfMultikeys(keys.size, keys) + + /** + * Create a multikey instance that requires all keys from a list + */ + fun allOf(keys: List): Multikey = someOf(keys.size, keys) + + + + + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt index 131da3f..4a8f659 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt @@ -27,11 +27,22 @@ import net.sergeych.bipack.decodeFromBipack @Serializable class SealedBox( val message: UByteArray, - private val seals: List, + /** + * [Seal] instance representing _correct signatures_ of this box. Note that if the box + * is constructed (deserialized, etc) successfully, all seals are ok. Initial check + * of signatures could be bypassed by setting [checkOnInit] to false, which should + * be avoided. + */ + val seals: List, @Transient private val checkOnInit: Boolean = true ) { + /** + * Extract [VerifyingPublicKey] from [seals]. + */ + val signedByKeys: List by lazy { seals.map { it.publicKey } } + @Suppress("unused") constructor(message: UByteArray, vararg keys: SigningKey) : this(message, keys.map { it.seal(message) } ) @@ -61,6 +72,12 @@ class SealedBox( return seals.any { it.publicKey == publicKey } } + /** + * Checks that the box is signed by enough keys to satisfy the given [Multikey]. + */ + @Suppress("unused") + fun isSealedBy(multikey: Multikey) = multikey.check(signedByKeys) + init { if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal") if (checkOnInit) { diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt index 498482e..d3f6df3 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt @@ -33,6 +33,32 @@ class SigningSecretKey( override val label: String get() = "sig" + /** + * Create a [Multikey] that requires presence of this or [other] key + */ + infix fun or(other: VerifyingPublicKey) = Multikey(this) or other + /** + * Create a [Multikey] that requires presence of this or [other] key + */ + infix fun or(other: SigningSecretKey) = Multikey(this) or other + /** + * Create a [Multikey] that requires presence of this or [other] key + */ + infix fun or(other: Multikey) = Multikey(this) or other + + /** + * Create a [Multikey] that requires presence of this and [other] key + */ + infix fun and(other: VerifyingPublicKey) = Multikey(this) and other + /** + * Create a [Multikey] that requires presence of this and [other] key + */ + infix fun and(other: SigningSecretKey) = Multikey(this) and other + /** + * Create a [Multikey] that requires presence of this and [other] key + */ + infix fun and(other: Multikey) = Multikey(this) and other + companion object { data class SigningKeyPair(val secretKey: SigningSecretKey, val publicKey: VerifyingPublicKey) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt index 27adc15..82d3d4f 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt @@ -26,4 +26,32 @@ class VerifyingPublicKey(override val keyBytes: UByteArray) : UniversalKey(), Ve override val magic: KeysmagicNumber = KeysmagicNumber.defaultVerifying override val id by lazy { KeyId(magic, keyBytes, null, true) } + + /** + * Create a [Multikey] that requires presence of this or [other] key + */ + infix fun or(other: VerifyingPublicKey) = Multikey(this) or other + /** + * Create a [Multikey] that requires presence of this or [other] key + */ + infix fun or(other: SigningSecretKey) = Multikey(this) or other + /** + * Create a [Multikey] that requires presence of this or [other] key + */ + infix fun or(other: Multikey) = Multikey(this) or other + + /** + * Create a [Multikey] that requires presence of this and [other] + */ + infix fun and(other: VerifyingPublicKey) = Multikey(this) and other + /** + * Create a [Multikey] that requires presence of this and [other] + */ + infix fun and(other: SigningSecretKey) = Multikey(this) and other + /** + * Create a [Multikey] that requires presence of this and [other] + */ + infix fun and(other: Multikey) = Multikey(this) and other + + } \ No newline at end of file diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index 0e03723..c2ffdfd 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -276,4 +276,72 @@ class KeysTest { // and restored from id should be the same: assertEquals( k.verifyingKey, dk2.id.id.asVerifyingKey) } + + @Test + fun multiKeyTestSom() = runTest { + initCrypto() + val k1 = SigningSecretKey.new() + val k2 = SigningSecretKey.new() + val k3 = SigningSecretKey.new() + val k4 = SigningSecretKey.new() + val k5 = SigningSecretKey.new() +// val k6 = SigningSecretKey.new() + val mk: Multikey = Multikey.Keys(1, setOf(k1.verifyingKey)) + val mk23: Multikey = Multikey.Keys(2, setOf(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey)) + val mk13: Multikey = Multikey.Keys(1, setOf(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey)) + + assertTrue { mk.check(k1.verifyingKey) } + assertFalse { mk.check(k2.verifyingKey) } + + assertTrue { mk23.check(k1.verifyingKey, k2.verifyingKey, k4.verifyingKey) } + assertTrue { mk23.check(k3.verifyingKey, k2.verifyingKey, k4.verifyingKey) } + assertFalse { mk23.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) } + assertTrue { mk13.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) } + + println(pack(mk23).toDump()) + println(pack(mk23).size) + + val smk23: Multikey = Multikey.someOf(2, k1.verifyingKey, k2.verifyingKey, k3.verifyingKey) +// val smk13: Multikey = Multikey.Keys(1, setOf(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey)) + + assertTrue { smk23.check(k1.verifyingKey, k2.verifyingKey, k4.verifyingKey) } + assertTrue { smk23.check(k3.verifyingKey, k2.verifyingKey, k4.verifyingKey) } + assertFalse { smk23.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) } +// assertTrue { smk13.check(k4.verifyingKey, k2.verifyingKey, k5.verifyingKey) } + + println(pack(smk23).toDump()) + println(pack(smk23).size) + + val s1 = k1 or k2 or k3 + println(pack(s1).toDump()) + println(pack(s1).size) + assertTrue { s1.check(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey) } + assertTrue { s1.check(k1.verifyingKey) } + assertTrue { s1.check(k2.verifyingKey) } + assertTrue { s1.check(k3.verifyingKey) } + assertFalse { s1.check(k4.verifyingKey) } + + val s2 = (k1 or k2) and k3 + println(pack(s2).toDump()) + println(pack(s2).size) + assertTrue { s2.check(k1.verifyingKey, k3.verifyingKey) } + assertTrue { s2.check(k2.verifyingKey, k3.verifyingKey) } + assertTrue { s2.check(k1.verifyingKey, k2.verifyingKey, k3.verifyingKey) } + assertFalse { s2.check(k4.verifyingKey) } + assertFalse { s2.check(k1.verifyingKey) } + assertFalse { s2.check(k2.verifyingKey) } + assertFalse { s2.check(k3.verifyingKey) } + assertFalse { s2.check(k1.verifyingKey, k2.verifyingKey) } + + val s3 = (k1 and k2) or k3 + println(pack(s3).toDump()) + println(pack(s3).size) + assertTrue { s3.check(k1.verifyingKey, k3.verifyingKey) } + assertTrue { s3.check(k3.verifyingKey) } + assertTrue { s3.check(k2.verifyingKey, k1.verifyingKey) } + assertFalse { s3.check(k1.verifyingKey) } + assertFalse { s3.check(k2.verifyingKey) } + assertFalse { s3.check(k1.verifyingKey, k4.verifyingKey) } + + } } \ No newline at end of file