diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt index 7996d1d..c018f24 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt @@ -5,57 +5,129 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackEncoder +import net.sergeych.crypto2.Container.Companion.createWith -/* -The problem with container is the following. When we encrypt it with asymmetric key, -we provide public key as the recipient. But public key alone is not effective -as it is restricted to anonymous encryption which is very slow. - -We might need an alternative to specify the (sender key,recipient key) pair also. We need a good -solution for it. +/** + * Multi-key encrypted container with simple adding new keys function that does not need to + * know all existing keys, e.g., you can add recipients to the container data if you can + * decrypt it. This is sometimes very important to be able to add recipients to the message + * keeping existing recipients you know no keys of. + * + * See: + * - [createWith] for more on create a new container + * - [addRecipients] and various [plus] operators to add recipients + * - [decryptWith] to decrypt */ - - @Serializable sealed class Container { + /** + * Exception thrown when container inner structure is bad. It could mean it was altered. + */ class InvalidContainerException : Crypto2Exception("the container is invalid") + /** + * Attempt to decrypt container contents. This function caches decrypted data, so it is ok + * to call it more than once. + * + * @param keyRing a key ring with keys that caller wants to be used + * @return decrypted data or null if this ring contains no proper key for it + */ abstract fun decryptWith(keyRing: UniversalRing): UByteArray? - fun decryptWith(vararg decryptingKeys: DecryptingKey): UByteArray? = - decryptWith(UniversalRing(*decryptingKeys)) + /** + * Attempt to decrypt container contents. This function caches decrypted data, so it is ok + * to call it more than once. + * + * @param keys keys that caller wants to be used to decrypt with + * @return decrypted data or null if this ring contains no proper key for it + */ + fun decryptWith(vararg keys: DecryptingKey): UByteArray? = + decryptWith(UniversalRing(*keys)) @Transient var decryptedData: UByteArray? = null + /** + * Check the container is already decrypted. It is important, for example, to be sure it is + * before adding more keys. + */ val isDecrypted: Boolean get() = decryptedData != null + /** + * @suppress + * Single-key variant, to conserve space it does not use the main key logic and just encrypts the data. + */ @Serializable @SerialName("1") - class Single(val keyTag: KeyTag, val encryptedMessage: UByteArray) : Container() { + internal class Single(val keyTag: KeyTag, val encryptedMessage: UByteArray) : Container() { + + @Transient + private var decryptedWithKey: DecryptingKey? = null + override fun decryptWith(keyRing: UniversalRing): UByteArray? { decryptedData?.let { return it } for (k in keyRing) { if (k.tag == keyTag) { kotlin.runCatching { k.decrypt(encryptedMessage) }.getOrNull()?.let { decryptedData = it + decryptedWithKey = k return it } } } return null } + + internal val asOpenMulti: Container by lazy { + check(isDecrypted) { "container should be decrypted" } + create(decryptedData!!) { + alwaysMulti() + when (val k = decryptedWithKey!!) { + is Asymmetric.SecretKey -> { + key(k.publicKey) + } + + is EncryptingKey -> { + key(k) + } + + is UniversalKey.Secret -> { + key(k.key.publicKey) + } + + else -> { + throw IllegalStateException("unknown key type to convert container: ${k::class.simpleName}") + } + } + }.apply { decryptWith(decryptedWithKey!!) ?: throw Crypto2Exception("internal error in container update (1)") } + } } + /** + * @suppress + * Implementation of 2+ keys container + */ @Serializable @SerialName("*") - class Multi(val encryptedKeys: List, val encryptedMessage: UByteArray) : Container() { + internal class Multi(val encryptedKeys: List, val encryptedMessage: UByteArray) : Container() { @Serializable - class EncryptedKey(val tag: KeyTag, val cipherData: UByteArray) + class EncryptedKey(val tag: KeyTag, val cipherData: UByteArray) { + constructor(key: EncryptingKey, encodeMainKey: UByteArray) : + this(key.tag, key.encrypt(encodeMainKey)) - private var mainKey: SymmetricKey? = null + constructor(sender: Asymmetric.SecretKey?, recipient: Asymmetric.PublicKey, encodeMainKey: UByteArray) : + this( + recipient.tag, + recipient.encryptMessage( + encodeMainKey, + senderKey = sender ?: Asymmetric.randomSecretKey(), + ).encoded + ) + } + + internal var mainKey: SymmetricKey? = null override fun decryptWith(keyRing: UniversalRing): UByteArray? { decryptedData?.let { return it } @@ -68,7 +140,8 @@ sealed class Container { key.decrypt(encryptedKey.cipherData).toByteArray() ) }.getOrNull()?.let { k -> - if (kotlin.runCatching { decryptedData = k.decrypt(encryptedKey.cipherData) }.isFailure) + println(k) + if (kotlin.runCatching { decryptedData = k.decrypt(encryptedMessage) }.isFailure) throw InvalidContainerException() mainKey = k } @@ -80,61 +153,204 @@ sealed class Container { } } + fun addRecipients(builder: Builder.() -> Unit): Container = + if (this is Single) asOpenMulti.addRecipients(builder) + else { + Builder(this).apply(builder).build() + } + operator fun plus(recipient: Asymmetric.PublicKey) = addRecipients { key(recipient) } + operator fun plus(recipient: EncryptingKey) = addRecipients { key(recipient) } + operator fun plus(pair: Pair) = addRecipients { key(pair) } + + /** + * Binary encoded version. It is desirable to include [Container] as an object, though, + * especially when using custom serialization (Json, Boss, etc), it is serializable. + * Still, if you need it in binary form, this is a shortcut. You can use [decode] or call + * [BipackDecoder.decode] to deserialize the binary form. + */ val encoded: UByteArray by lazy { BipackEncoder.encode(this).toUByteArray() } companion object { - class Builder internal constructor(private val plainData: UByteArray) { + /** + * The builder to create container with various parameters. + * Use [create] to create container using a builder. Usage sample: + * + * ```kotlin + * Container.create(plainData) { + * // optional: add a random filling from 10 to 20 bytes + * fill( 10 .. 20 ) + * + * key(symmetricKey1, symmetricKey2) // add two SymmetricKey recipients + * + * key(publicKey1) // add a Asymmetric.PublicKey recipient anonymously + * + * // More interesting: add publicKey2 and publicKey3 recipients using my + * // secret key as authority. IT is faster and allow to owner of the listed public keys + * // to know it was me who added them to this container. + * key(mySecretKey to publicKey2,mySecretKey to publicKey3) + * } + * ``` + + */ + class Builder internal constructor( + private val plainData: UByteArray, + private var parent: Container? = null, + ) { + + internal constructor(parent: Container) : + this( + parent.decryptedData ?: throw IllegalStateException("container is not decrypted"), + parent + ) + + private val plainKeys = mutableListOf() - private val keyPairs = mutableListOf>() + private val keyPairs = mutableListOf>() private var fillRange: IntRange? = null - fun key(vararg keys: EncryptingKey) { plainKeys.addAll(keys) } + /** + * Add one or more encrypting keys + */ + fun key(vararg keys: EncryptingKey) { + plainKeys.addAll(keys) + } - fun key(vararg pairs: Pair) { + /** + * Add one or more [Asymmetric.SecretKey] as sender authority coupled with [Asymmetric.PublicKey] as + * a recipient. This is faster than anonymous usage of [Asymmetric.PublicKey] only + */ + fun key(vararg pairs: Pair) { keyPairs.addAll(pairs) } + /** + * Add one or more public keys as recipients. This is slower than using pairs of sender -> recipient. + */ fun key(vararg publicKeys: Asymmetric.PublicKey) { keyPairs.addAll(publicKeys.map { null to it }) } + /** + * Causes random filling of the encrypted message in the specified interval + */ @Suppress("unused") fun fill(range: IntRange) { - require(range.first >= 0 ) { "range must be positive"} + require(range.first >= 0) { "range must be positive" } fillRange = range } - fun build(): Container { - return when( plainKeys.size + keyPairs.size ) { - 0 -> throw IllegalArgumentException("Container needs at least one key") - 1 -> { - plainKeys.firstOrNull()?.let { - Single(it.tag, it.encrypt(plainData, fillRange)) - } ?: run { - val (sk, pk) = keyPairs.first() - Single(pk.tag, pk.encryptMessage(plainData, - senderKey = sk ?: Asymmetric.randomSecretKey(), - randomFill = fillRange).encoded) - } + private var makeMulti = false + + /** + * @suppress + * will produce multikey internal variant even with only one key. User internally + */ + internal fun alwaysMulti() { + makeMulti = true + } + + /** + * Create a Container + */ + internal fun build(): Container { + val countNewKeys = plainKeys.size + keyPairs.size + if (parent != null) require(parent is Multi) { "parent container mut be a multikey variant" } + return when { + countNewKeys == 0 -> throw IllegalArgumentException("Container needs at least one key") + countNewKeys == 1 && makeMulti == false && parent == null -> { + createSingle() } + else -> { - TODO("multikey") + val eks: MutableList + val mainKey: SymmetricKey + val p = parent + if (p != null) { + p as Multi + eks = p.encryptedKeys.toMutableList() + mainKey = p.mainKey ?: throw IllegalStateException("parent container must be decrypted") + } else { + eks = mutableListOf() + mainKey = SymmetricKey.random() + } + val encodedMainKey = BipackEncoder.encode(mainKey).toUByteArray() + createMulti(eks, encodedMainKey, mainKey) } } } + + private fun createSingle() = plainKeys.firstOrNull()?.let { + Single(it.tag, it.encrypt(plainData, fillRange)) + } ?: run { + val (sk, pk) = keyPairs.first() + Single( + pk.tag, pk.encryptMessage( + plainData, + senderKey = sk ?: Asymmetric.randomSecretKey(), + randomFill = fillRange + ).encoded + ) + } + + private fun createMulti( + eks: MutableList, + encodedMainKey: UByteArray, + mainKey: SymmetricKey, + ): Multi { + for (k in plainKeys) + eks += Multi.EncryptedKey(k, encodedMainKey) + for (p in keyPairs) { + val (sender, recipient) = p + eks += Multi.EncryptedKey(sender, recipient, encodedMainKey) + } + return Multi(eks, mainKey.encrypt(plainData, fillRange)) + } } - fun create(plainData: UByteArray,builder: Builder.()->Unit) = + /** + * Create a container using a [Builder] instance. + * Usage sample: + * ```kotlin + * Container.create(plainData) { + * // optional: add a random filling from 10 to 20 bytes + * fill( 10 .. 20 ) + * + * key(symmetricKey1, symmetricKey2) // add two SymmetricKey recipients + * + * key(publicKey1) // add a Asymmetric.PublicKey recipient anonymously + * + * // More interesting: add publicKey2 and publicKey3 recipients using my + * // secret key as authority. IT is faster and allow to owner of the listed public keys + * // to know it was me who added them to this container. + * key(mySecretKey to publicKey2,mySecretKey to publicKey3) + * } + * ``` + * At least one key should be provided. + * + * @param plainData data to encrypt + */ + fun create(plainData: UByteArray, builder: Builder.() -> Unit) = Builder(plainData).also { it.builder() }.build() - fun create(plainData: UByteArray, vararg keys: EncryptingKey): Container = - create(plainData) { key(*keys) } + /** + * Create container using one or more [EncryptingKey] and a builder, see [create] + * for builder usage sample. + */ + fun createWith( + plainData: UByteArray, vararg keys: EncryptingKey, + builder: (Builder.() -> Unit)? = null, + ): Container = + create(plainData) { key(*keys); builder?.invoke(this) } - fun create(plainData: UByteArray, vararg keys: Pair) = + /** + * Create the container using one or more `sender to recipient` asymmetric keys and a builder. See [create] + * for builder usage sample. + */ + fun createWith(plainData: UByteArray, vararg keys: Pair) = create(plainData) { key(*keys) } fun decode(encoded: UByteArray): Container { diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt index 79691de..aea47d0 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt @@ -45,6 +45,7 @@ sealed class UniversalKey : DecryptingKey { companion object { fun from(key: DecryptingKey) = when (key) { + is UniversalKey -> key is Asymmetric.SecretKey -> Secret(key) is SymmetricKey -> Symmetric(key) is SafeKeyExchange.SessionKey -> Session(key) diff --git a/src/commonTest/kotlin/ContainerTest.kt b/src/commonTest/kotlin/ContainerTest.kt index 308ae25..fc00f85 100644 --- a/src/commonTest/kotlin/ContainerTest.kt +++ b/src/commonTest/kotlin/ContainerTest.kt @@ -1,9 +1,6 @@ import com.ionspin.kotlin.crypto.util.encodeToUByteArray import kotlinx.coroutines.test.runTest -import net.sergeych.crypto2.Asymmetric -import net.sergeych.crypto2.Container -import net.sergeych.crypto2.SymmetricKey -import net.sergeych.crypto2.initCrypto +import net.sergeych.crypto2.* import kotlin.test.* class ContainerTest { @@ -14,7 +11,7 @@ class ContainerTest { val syk2 = SymmetricKey.random() val data = "sergeych, ohm many.".encodeToUByteArray() - val c = Container.create(data, syk1) + val c = Container.createWith(data, syk1) assertFalse { c.isDecrypted } val c1 = Container.decode(c.encoded) assertFalse { c.isDecrypted } @@ -35,7 +32,7 @@ class ContainerTest { val p3 = Asymmetric.generateKeys() val data = "sergeych, ohm many.".encodeToUByteArray() - val c = Container.create(data, p1.secretKey to p2.publicKey) + val c = Container.createWith(data, p1.secretKey to p2.publicKey) assertFalse { c.isDecrypted } val c1 = Container.decode(c.encoded) assertFalse { c.isDecrypted } @@ -74,13 +71,125 @@ class ContainerTest { initCrypto() val syk1 = SymmetricKey.random() val syk2 = SymmetricKey.random() -// val syk3 = SymmetricKey.random() + val syk3 = SymmetricKey.random() + val p1 = Asymmetric.generateKeys() + val p2 = Asymmetric.generateKeys() + val p3 = Asymmetric.generateKeys() + val p4 = Asymmetric.generateKeys() val data = "Translating the name 'Sergey Chernov' from Russian to archaic Sanskrit would be 'Ramo Krishna'" .encodeToUByteArray() - val c = Container.create(data, syk1, syk2) + val c = Container.createWith(data, syk1, syk2) { + key(p1.secretKey to p3.publicKey) + key(p4.publicKey) + } assertFalse { c.isDecrypted } - val c1 = Container.decode(c.encoded) + var c1 = Container.decode(c.encoded) assertFalse { c1.isDecrypted } + + assertNull(c1.decryptWith(syk3)) + assertFalse { c1.isDecrypted } + + assertContentEquals(data, c1.decryptWith(syk3, syk1)) + assertTrue { c1.isDecrypted } + + c1 = Container.decode(c.encoded) + assertFalse { c1.isDecrypted } + assertNull(c1.decryptWith(p2.secretKey, p1.secretKey)) + assertContentEquals(data, c1.decryptWith(syk3, p3.secretKey)) + + c1 = Container.decode(c.encoded) + assertFalse { c1.isDecrypted } + assertContentEquals(data, c1.decryptWith(syk3, p4.secretKey)) + } + + @Test + fun testSingleGrowSymmetric() = runTest { + initCrypto() + val syk1 = SymmetricKey.random() + val syk2 = SymmetricKey.random() + val syk3 = SymmetricKey.random() + val p1 = Asymmetric.generateKeys() + val p3 = Asymmetric.generateKeys() + val p4 = Asymmetric.generateKeys() + val data = "Translating the name 'Sergey Chernov' from Russian to archaic Sanskrit would be 'Ramo Krishna'" + .encodeToUByteArray() + + var c = Container.createWith(data, syk1) + + fun expectOpen(k: DecryptingKey) { + val c1 = Container.decode(c.encoded) + assertContentEquals(data, c1.decryptWith(k)) + } + + fun expectNotOpen(k: DecryptingKey) { + val c1 = Container.decode(c.encoded) + assertNull(c1.decryptWith(k)) + } + + expectOpen(syk1) + expectNotOpen(syk2) + expectNotOpen(p3.secretKey) + + c.decryptWith(syk1) + assertTrue { c.isDecrypted } + assertNotNull(c.decryptedData) + c += syk2 + expectOpen(syk1) + expectOpen(syk2) + expectNotOpen(syk3) + expectNotOpen(p3.secretKey) + + c.decryptWith(syk1) + c += p3.publicKey + expectOpen(syk1) + expectOpen(syk2) + expectOpen(p3.secretKey) + expectNotOpen(syk3) + expectNotOpen(p4.secretKey) + + c.decryptWith(syk1) + c += p1.secretKey to p4.publicKey + expectOpen(syk1) + expectOpen(syk2) + expectOpen(p3.secretKey) + expectNotOpen(syk3) + expectOpen(p4.secretKey) + } + + @Test + fun testSingleGrowAsymmetric() = runTest { + initCrypto() + val syk1 = SymmetricKey.random() + val syk2 = SymmetricKey.random() + val syk3 = SymmetricKey.random() + val p1 = Asymmetric.generateKeys() + val p2 = Asymmetric.generateKeys() + val p3 = Asymmetric.generateKeys() + val p4 = Asymmetric.generateKeys() + val data = "Translating the name 'Sergey Chernov' from Russian to archaic Sanskrit would be 'Ramo Krishna'" + .encodeToUByteArray() + + var c = Container.createWith(data, p1.secretKey to p3.publicKey) + + fun expectOpen(k: DecryptingKey) { + val c1 = Container.decode(c.encoded) + assertContentEquals(data, c1.decryptWith(k)) + } + + fun expectNotOpen(k: DecryptingKey) { + val c1 = Container.decode(c.encoded) + assertNull(c1.decryptWith(k)) + } + + expectNotOpen(syk1) + expectOpen(p3.secretKey) + + c.decryptWith(p3.secretKey) + + c += syk1 + expectOpen(syk1) + expectNotOpen(syk2) + expectOpen(p3.secretKey) } } \ No newline at end of file