diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt index 1748e8c..df146de 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt @@ -80,7 +80,7 @@ sealed class Container { * @return decrypted data or null if this ring contains no proper key for it */ fun decryptWith(vararg keys: DecryptingKey): UByteArray? = - decryptWith(UniversalRing(*keys)) + decryptWith(UniversalRing.from(*keys)) @Transient var decryptedData: UByteArray? = null diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt index 0f23026..9d22198 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt @@ -3,13 +3,28 @@ package net.sergeych.crypto2 import kotlinx.serialization.Serializable /** - * Tag that identifies in some way the _decrypting key_. Is used in keyrings and - * containers to fast find a proper key + * Key Identity. Can be used to find matching keys for decryption or verifying, etc. The identity + * may contain [KDF] parameters if the corresponding key could be derived from a password. + * Note that [kdf] part is not respected in [equals]. + * + * Important. `KeyId` of matching keys are the same, so you can use it to identify + * and find matching keys in the [UniversalRing], etc. For example: + * + * - [Asymmetric.SecretKey] and [Asymmetric.PublicKey] from the same pair have the same `KeyId`, thus the former + * can decrypt what was encrypted with the latter. + * + * - [SigningSecretKey] and corresponding [VerifyingKey] have the same `KeyId`. Use it to pick a proper key for + * signing from a ring with [UniversalRing.findKey] + * */ @Serializable -data class KeyId(val id: BinaryId, val kdp: KeyDerivationParams?=null ) { +data class KeyId(val id: BinaryId, val kdf: KeyDerivationParams?=null ) { - val tag: UByteArray by lazy { id.id } + /** + * Binary array representation of the [id], not including the [kdf]. Used in [SafeKeyExchange] + * and other key exchanges to generate session tokens, etc. + */ + val binaryTag: UByteArray by lazy { id.id } override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt index 117d58a..a62d548 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt @@ -47,9 +47,9 @@ class SafeKeyExchange { @Suppress("unused") val sessionTag: UByteArray by lazy { if (!isClient) - blake2b(id.tag + id.tag) + blake2b(id.binaryTag + id.binaryTag) else - blake2b(id.tag + id.tag) + blake2b(id.binaryTag + id.binaryTag) } override val id: KeyId diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt index 0ea3d2e..bfb4a14 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalRing.kt @@ -4,53 +4,101 @@ import kotlinx.serialization.Serializable @Serializable class UniversalRing( - val keyWithTags: Map> + val keyWithTags: Map>, ) { constructor(vararg keys: UniversalKey) : this(keys.associateWith { setOf() }) - constructor(vararg keys: DecryptingKey) : this(keys.associate { UniversalKey.from(it) to setOf() }) + constructor(vararg keyTags: Pair) : this(keyTags.associate { it.first to setOf(it.second) }) val decryptingKeys: Set by lazy { keys() } + + /** + * Select keys of the specified type + */ + inline fun keys(): Set = + allKeys.mapNotNull { it as? T }.toSet() - inline fun keys(): Set = - keyWithTags.keys.mapNotNull { it as? T }.toSet() + val allKeys: Set by lazy { keyWithTags.keys } + + inline fun findKey(id: KeyId): UniversalKey? = + allKeys.find { it is T && it.id == id } - inline fun findKey(id: KeyId): UniversalKey? = - keyWithTags.keys.find { it is T && it.id == id } + fun keysById(id: KeyId): List = allKeys.filter { it.id == id } - fun allByAnyOfTags(vararg tags: String) = sequence { - for( e in keyWithTags.entries) { - if( tags.any { it in e.value }) yield(e.key) + /** + * Return sequence of keys that have at least one of the [tags] + */ + fun keysByTags(vararg tags: String) = sequence { + for (e in keyWithTags.entries) { + if (tags.any { it in e.value }) yield(e.key) } } - inline fun keyByTag(tag: String) = allByAnyOfTags(tag).first { it is T } + /** + * Get the first key of the specified type having the [tag] + */ + inline fun keyByTag(tag: String) = keysByTags(tag).first { it is T } @Suppress("unused") - inline fun keyByAnyTag(vararg tags: String) = allByAnyOfTags(*tags).first { it is T } + inline fun keyByAnyTag(vararg tags: String) = keysByTags(*tags).first { it is T } - operator fun get(keyId: KeyId): Collection = keyWithTags.keys.filter { it.id == keyId } + /** + * Get all keys with a given id. Note that _matching keys_ have the same id, see [KeyId] for more. + */ + operator fun get(keyId: KeyId): Collection = allKeys.filter { it.id == keyId } fun getTags(key: UniversalKey): Set? = keyWithTags[key] operator fun contains(element: UniversalKey): Boolean = element in keyWithTags + /** + * Add a key if not already in a ring, otherwise return existing ring. + */ operator fun plus(key: UniversalKey): UniversalRing = - if( key in this ) this else UniversalRing(keyWithTags + (key to setOf()) ) + if (key in this) this else UniversalRing(keyWithTags + (key to setOf())) - operator fun plus(keyTag: Pair): UniversalRing = - if( keyTag.first in this ) - addTags(keyTag.first,keyTag.second) - else UniversalRing(keyWithTags + keyWithTags) + /** + * Add a key and tags to the ring. If the key exists, add the tag to it. + * See also [addTags]. + */ + operator fun plus(keyTag: Pair): UniversalRing = + if (keyTag.first in this) + addTags(keyTag.first, keyTag.second) + else add(keyTag.first, keyTag.second) + /** + * Merge two rings. The result will contain all keys and all tags from + * both rings. + */ + operator fun plus(other: UniversalRing): UniversalRing { + var result = keyWithTags.toMutableMap() + for (e in other.keyWithTags.entries) { + result[e.key]?.let { + result[e.key] = it + e.value + } ?: run { + result[e.key] = e.value + } + } + return UniversalRing(result) + } + + /** + * Add key and tags to the ring. If the key already exists, tags are merged. + */ fun add(key: UniversalKey, vararg tags: String): UniversalRing = - UniversalRing(keyWithTags + (key to tags.toSet()) ) + UniversalRing(keyWithTags + (key to tags.toSet())) + /** + * Add key and tags to the ring. If the key already exists, tags are merged. + */ fun add(key: UniversalKey, tags: Collection): UniversalRing = - UniversalRing(keyWithTags + (key to tags.toSet()) ) + UniversalRing(keyWithTags + (key to tags.toSet())) - fun addTags(key: UniversalKey,tags: Set): UniversalRing { + /** + * Add tags to the key, and add key if it is not in the ring. + */ + fun addTags(key: UniversalKey, tags: Set): UniversalRing { val kt1 = keyWithTags.toMutableMap() kt1[key]?.let { kt1[key] = it + tags @@ -60,11 +108,17 @@ class UniversalRing( return UniversalRing(kt1) } - fun addTags(key: UniversalKey,vararg tags: String): UniversalRing = - addTags(key,tags.toSet()) + /** + * Add tags to the key, and add key if it is not in the ring. + */ + fun addTags(key: UniversalKey, vararg tags: String): UniversalRing = + addTags(key, tags.toSet()) + /** + * return the ring without [key] if existed, otherwise returns `this`. + */ operator fun minus(key: UniversalKey): UniversalRing = - if( key in this ) UniversalRing(keyWithTags.filter { it.key != key }) else this + if (key in this) UniversalRing(keyWithTags.filter { it.key != key }) else this override fun equals(other: Any?): Boolean { if (this === other) return true @@ -72,27 +126,64 @@ class UniversalRing( return keyWithTags == other.keyWithTags } - override fun toString(): String { - val ss = keyWithTags.entries - .joinToString(","){"${it.value.joinToString{ ":" }}:${it.key}"} - return "Kr[$ss]" - } - override fun hashCode(): Int { return keyWithTags.hashCode() } - infix fun equalKeys(other: UniversalRing): Boolean = keyWithTags.keys == other.keyWithTags.keys - - fun removeTags(key: UniversalKey, vararg tags: String): UniversalRing = removeTags(key, tags.toSet()) - - fun removeTags(key: UniversalKey, tags: Set): UniversalRing { - val kt1 = keyWithTags.toMutableMap() - kt1[key]?.let { - kt1[key] = it - tags - } - return UniversalRing(kt1) + override fun toString(): String { + val ss = keyWithTags.entries + .joinToString(",") { "${it.value.joinToString { ":" }}:${it.key}" } + return "Kr[$ss]" } + infix fun equalKeys(other: UniversalRing): Boolean = allKeys == other.allKeys + + /** + * Create a collection where no [tags] are specified for the [key]. If the key is not + * in the collection, it returns this. + */ + fun removeTags(key: UniversalKey, vararg tags: String): UniversalRing = removeTags(key, tags.toSet()) + + /** + * Create a collection where no [tags] are specified for the [key]. If the key is not + * in the collection, it returns this. + */ + fun removeTags(key: UniversalKey, tags: Set): UniversalRing = + keyWithTags[key]?.let { existingTags -> + val kt1 = keyWithTags.toMutableMap() + kt1[key] = existingTags - tags + UniversalRing(kt1) + } ?: this + + /** + * Create string "report" of the ring contents. Note it has no trailing `\n` + */ + fun ls(): String { + val result = mutableListOf() + for( e in keyWithTags.entries) { + result += "${e.key} ${e.value.joinToString(" ")}" + } + return result.joinToString("\n") + } + + companion object { + val EMPTY = UniversalRing(keyWithTags = emptyMap()) + + /** + * Join a collection of keyrings together (same as reducing with `+`). Correctly + * works if there is no keyring (returns [EMPTY]), or only one keyring (returns + * it with no extra work). + * + * Note that for the known number of rings using [plus] is more clear and expressive + */ + fun join(keyRings: Collection): UniversalRing { + if (keyRings.isEmpty()) return EMPTY + if (keyRings.size == 1) return keyRings.first() + return keyRings.reduce { l, r -> l + r } + } + + fun from(vararg keys: DecryptingKey): UniversalRing = + UniversalRing(keys.associate { UniversalKey.from(it) to setOf() } ) + } } diff --git a/src/commonTest/kotlin/RingTest.kt b/src/commonTest/kotlin/RingTest.kt index 8044d25..8cccfa1 100644 --- a/src/commonTest/kotlin/RingTest.kt +++ b/src/commonTest/kotlin/RingTest.kt @@ -114,6 +114,40 @@ class RingTest { @Test fun testSize() = runTest { -// val sy1 = SymmetricKey.random().toUniversal() + initCrypto() + val r = UniversalRing(UniversalKey.newSymmetricKey()) + val rd = BipackEncoder.encode(r) + println(rd.size) + println(rd.toDump()) + assertTrue { rd.size <= 46 } + } + + @Test + fun testJoin() = runTest { + initCrypto() + val a = UniversalKey.newSecretKey() + val b = UniversalKey.newSigningKey() + val c = UniversalKey.newSymmetricKey() + val d = UniversalKey.newSigningKey() + + val ra = UniversalRing(a) + val rb = UniversalRing(b) + val rc = UniversalRing(c) + val rd = UniversalRing(d) + (a to "foo_a") + + var r1 = ra + rb + rc + rd + + assertEquals(a, r1.findKey(a.id)) + assertEquals(a, r1.keyByTag("foo_a")) + assertEquals(b, r1.findKey(b.id)) + assertEquals(c, r1.keysById(c.key.id).first()) + + r1 = UniversalRing.join(listOf(ra, rb, rc, rd)) + + assertEquals(a, r1.findKey(a.id)) + assertEquals(a, r1.keyByTag("foo_a")) + assertEquals(b, r1.findKey(b.id)) + assertEquals(c, r1.keysById(c.key.id).first()) + } } \ No newline at end of file