Container started

This commit is contained in:
Sergey Chernov 2024-06-19 16:46:32 +07:00
parent e4e3b5ba8b
commit e2916d0fe2
12 changed files with 360 additions and 76 deletions

View File

@ -7,6 +7,7 @@ import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto2.Asymmetric.Message
import net.sergeych.crypto2.Asymmetric.PublicKey
import net.sergeych.crypto2.Asymmetric.SecretKey
@ -32,7 +33,7 @@ object Asymmetric {
/**
* Encrypted message holder.
*
* Do not instantiate it directly, use [SecretKey.encrypt], [PublicKey.encryptAnonymously] or [createMessage]
* Do not instantiate it directly, use [PublicKey.encryptMessage], [PublicKey.encryptAnonymousMessage], etc.
* instead. Also [SecretKey.decrypt] can be used to decrypt it same as [decrypt] or [decryptWithSenderKey].
*
* To successfully decrypt the message, it is necessary to know a sender public key, and non-secret nonce.
@ -42,13 +43,12 @@ object Asymmetric {
class Message(
private val nonce: UByteArray,
private val encryptedMessage: UByteArray,
val senderPublicKey: PublicKey? = null,
val senderPublicKey: PublicKey,
) {
/**
* Decrypt the message, same as [SecretKey.decrypt]
*/
fun decrypt(recipientKey: SecretKey): UByteArray {
check(senderPublicKey != null)
return decryptWithSenderKey(senderPublicKey, recipientKey)
}
@ -58,11 +58,15 @@ object Asymmetric {
*/
fun decryptWithSenderKey(senderKey: PublicKey, recipientKey: SecretKey): UByteArray {
return try {
Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes)
WithFill.decode(
Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes)
)
} catch (_: BoxCorruptedOrTamperedDataException) {
throw DecryptionFailedException()
}
}
val encoded: UByteArray by lazy { BipackEncoder.encode(this).toUByteArray() }
}
/**
@ -72,30 +76,27 @@ object Asymmetric {
* The authenticated encryption is used, is the message _is successfully decrypted_, it also means that
* it was signed by the sender, whose public key is known at the decryption time.
*
* When it is important not to provide senders' key, use [PublicKey.encryptAnonymously].
* When it is important not to provide senders' key, use [PublicKey.encryptAnonymousMessage].
*
* @param from the senders' secret key.
* @param recipient the recipients' public key.
* @param plainData data to encrypt
* @param excludeSenderKey if set to true, senders' public key will not be included in the message.
* In this case, the recipient must know it from other sources to be able to decrypt the message.
*/
fun createMessage(
private fun createMessage(
from: SecretKey, recipient: PublicKey, plainData: UByteArray,
excludeSenderKey: Boolean = false,
nonce: UByteArray = randomNonce(),
): Message {
val nonce = randomNonce()
return Message(
nonce,
Box.easy(plainData, nonce, recipient.keyBytes, from.keyBytes),
if (excludeSenderKey) null else from.publicKey
from.publicKey
)
}
/**
* The generated key pair. See [generateKeys]
*/
data class KeyPair(val secretKey: SecretKey, val senderKey: PublicKey)
data class KeyPair(val secretKey: SecretKey, val publicKey: PublicKey)
/**
* Generate a new random pair of public and secret keys.
@ -114,11 +115,18 @@ object Asymmetric {
val nonceBytesLength = crypto_box_NONCEBYTES
/**
* The public key used as the recipient for [Message] (see [SecretKey.encrypt], etc.). It also
* could be used to encrypt anonymous messages with [encryptAnonymously]
* The public key: [encryptMessage] so only a secret key owner can read it. Allows
* anonymous [encryptAnonymousMessage] and signed [encryptMessage] encryption.
*
* Anonymous encryption is very slow in comparison.
*/
@Serializable
class PublicKey(val keyBytes: UByteArray) {
val tag: KeyTag by lazy {
KeyTag(KeysMagickNumbers.defaultAssymmetric, blake2b(keyBytes))
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PublicKey) return false
@ -126,19 +134,57 @@ object Asymmetric {
return keyBytes contentEquals other.keyBytes
}
override fun hashCode(): Int {
return keyBytes.hashCode()
}
/**
* Create an anonymous message that could be decrypted only with the [SecretKey] that corresponds this.
* Anonymous message uses one-time secret key, the public part of which is included into the
* [Message], so the sender could not be identified.
*
* __Anonymous encryption is much slower__ as it generates new keys every time, use [encryptMessage]
* when possible
*
* The authentication is used despite the anonymity, and the fact of the successful decryption
* proves that the message was not altered after creation.
*/
fun encryptAnonymously(plainData: UByteArray): Message =
createMessage(generateKeys().secretKey, this, plainData)
fun encryptAnonymousMessage(plainData: UByteArray, randomFill: IntRange? = null): Message =
encryptMessage(plainData,randomFill=randomFill)
/**
* Anonymous encryption, see [encryptAnonymousMessage], to binary data. Sender could not be identified.
*/
@Suppress("unused")
fun encrypt(plainData: UByteArray, randomFill: IntRange? = null): UByteArray =
encryptMessage(plainData, randomFill = randomFill).encoded
/**
* Universal public-key encryption. Note that message authenticity is guaranteed if the decryption is successful
* whether [senderKey] is provider, the latter only allow to positively identify the sender.
*
* @param plainData data to encrypt
* @param nonce allows specifying exact nonce, default to random (safe)
* @param senderKey key to authenticate sending party. It is safe and much faster to specify it,
* otherwise an anonymous key will be created for each encryption, also safe and anonymous, but slow.
*/
fun encryptMessage(
plainData: UByteArray,
nonce: UByteArray = randomNonce(),
senderKey: SecretKey = randomSecretKey(),
randomFill: IntRange? = null,
) = createMessage(senderKey, this, WithFill.encode(plainData, randomFill), nonce)
/**
* Encrypt message using the specified secret key as sender authentication. Recipient, the party having
* [SecretKey] corresponding to this one, will be able to decrypt the message and be sure that [senderKey]
* was the author and the message was not altered.
*/
fun encryptMessage(plainData: UByteArray,
senderKey: SecretKey,
randomFill: IntRange? = null): Message =
createMessage(senderKey, this, WithFill.encode(plainData, randomFill))
override fun hashCode(): Int {
return keyBytes.hashCode()
}
}
/**
@ -151,19 +197,10 @@ object Asymmetric {
val _cachedPublicKey: PublicKey? = null,
) : DecryptingKey {
/**
* Encrypt the message for [recipient]. The [Message] will include the our [publicKey]
* and thus ready to be decrypted.
*
* The message is authenticated by this secret key.
*/
fun encrypt(plainData: UByteArray, recipient: PublicKey): Message =
createMessage(this, recipient, plainData)
/**
* Decrypt with authentication checks the message which must have [Message.senderPublicKey] set.
* Use [decryptWithSenderKey] otherwise. Note that the authenticated encryption is always use, even if
* the [PublicKey.encryptAnonymously] was used to create a message, if it is successfully decrypted,
* the [PublicKey.encryptAnonymousMessage] was used to create a message, if it is successfully decrypted,
* it is guaranteed that the message was not altered after creation.
*
* @throws DecryptionFailedException If the message is tampered (changed after creation) or was not intended for us,
@ -213,9 +250,7 @@ object Asymmetric {
return message.decrypt(this)
}
override val decryptingTag: KeyTag by lazy {
KeyTag(KeysMagickNumbers.defaultAssymmetric, blake2b(publicKey.keyBytes))
}
override val tag: KeyTag by lazy { publicKey.tag }
override val nonceBytesLength: Int
get() = 0

View File

@ -1,9 +1,148 @@
//package net.sergeych.crypto2
//
//import kotlinx.serialization.Serializable
//
//@Serializable
//sealed class Container {
package net.sergeych.crypto2
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
/*
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.
*/
@Serializable
sealed class Container {
class InvalidContainerException : Crypto2Exception("the container is invalid")
abstract fun decryptWith(keyRing: UniversalRing): UByteArray?
fun decryptWith(vararg decryptingKeys: DecryptingKey): UByteArray? =
decryptWith(UniversalRing(*decryptingKeys))
@Transient
var decryptedData: UByteArray? = null
val isDecrypted: Boolean get() = decryptedData != null
@Serializable
@SerialName("1")
class Single(val keyTag: KeyTag, val encryptedMessage: UByteArray) : Container() {
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
return it
}
}
}
return null
}
}
@Serializable
@SerialName("*")
class Multi(val encryptedKeys: List<EncryptedKey>, val encryptedMessage: UByteArray) : Container() {
@Serializable
class EncryptedKey(val tag: KeyTag, val cipherData: UByteArray)
private var mainKey: SymmetricKey? = null
override fun decryptWith(keyRing: UniversalRing): UByteArray? {
decryptedData?.let { return it }
for (key in keyRing) {
val tag = key.tag
for (encryptedKey in encryptedKeys) {
if (tag == encryptedKey.tag) {
kotlin.runCatching {
BipackDecoder.decode<SymmetricKey>(
key.decrypt(encryptedKey.cipherData).toByteArray()
)
}.getOrNull()?.let { k ->
if (kotlin.runCatching { decryptedData = k.decrypt(encryptedKey.cipherData) }.isFailure)
throw InvalidContainerException()
mainKey = k
}
if (decryptedData != null) return decryptedData
}
}
}
return null
}
}
val encoded: UByteArray by lazy {
BipackEncoder.encode(this).toUByteArray()
}
companion object {
class Builder internal constructor(private val plainData: UByteArray) {
private val plainKeys = mutableListOf<EncryptingKey>()
private val keyPairs = mutableListOf<Pair<Asymmetric.SecretKey?,Asymmetric.PublicKey>>()
private var fillRange: IntRange? = null
fun key(vararg keys: EncryptingKey) { plainKeys.addAll(keys) }
fun key(vararg pairs: Pair<Asymmetric.SecretKey,Asymmetric.PublicKey>) {
keyPairs.addAll(pairs)
}
fun key(vararg publicKeys: Asymmetric.PublicKey) {
keyPairs.addAll(publicKeys.map { null to it })
}
@Suppress("unused")
fun fill(range: IntRange) {
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)
}
}
else -> {
TODO("multikey")
}
}
}
}
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) }
fun create(plainData: UByteArray, vararg keys: Pair<Asymmetric.SecretKey,Asymmetric.PublicKey>) =
create(plainData) { key(*keys) }
fun decode(encoded: UByteArray): Container {
return BipackDecoder.decode(encoded.toByteArray())
}
}
}
//
//
// /**

View File

@ -28,7 +28,7 @@ interface DecryptingKey : NonceBased {
fun decryptString(cipherData: UByteArray): String = decrypt(cipherData).decodeFromUByteArray()
val decryptingTag: KeyTag
val tag: KeyTag
}

View File

@ -27,7 +27,7 @@ interface EncryptingKey : NonceBased {
fun encrypt(plainText: String,randomFill: IntRange? = null): UByteArray =
encrypt(plainText.encodeToUByteArray(),randomFill)
val encryptingTag: KeyTag
val tag: KeyTag
fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange? = null): UByteArray
}

View File

@ -47,11 +47,14 @@ class SafeKeyExchange {
@Suppress("unused")
val sessionTag: UByteArray by lazy {
if (!isClient)
blake2b(decryptingTag.tag + encryptingTag.tag)
blake2b(tag.tag + tag.tag)
else
blake2b(encryptingTag.tag + decryptingTag.tag)
blake2b(tag.tag + tag.tag)
}
override val tag: KeyTag
get() = if( isClient ) sendingKey.tag else receivingKey.tag
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SessionKey) return false

View File

@ -3,8 +3,6 @@ package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
import kotlinx.serialization.Serializable
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
/**
* Symmetric key implements authenticated encrypting with random nonce and optional fill.
@ -32,36 +30,18 @@ class SymmetricKey(
val nonce: UByteArray,
)
/**
* @suppress
* add some random data to the end of the plain message
*/
@Serializable
data class WithFill(
val data: UByteArray,
val safetyFill: UByteArray? = null,
)
override fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange?): UByteArray {
require(nonce.size == nonceByteLength)
val fill = randomFill?.let {
require(it.start >= 0)
randomUBytes(randomInt(it))
}
val filled = BipackEncoder.encode(WithFill(plainData, fill))
return SecretBox.easy(filled.toUByteArray(), nonce, keyBytes)
return SecretBox.easy(WithFill.encode(plainData, randomFill), nonce, keyBytes)
}
override val nonceBytesLength: Int = nonceByteLength
override val encryptingTag by lazy { KeyTag(KeysMagickNumbers.defaultSymmetric,blake2b3l(keyBytes)) }
override val decryptingTag get() = encryptingTag
override val tag by lazy { KeyTag(KeysMagickNumbers.defaultSymmetric,blake2b3l(keyBytes)) }
override fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray =
protectDecryption {
BipackDecoder.decode<WithFill>(SecretBox.openEasy(cipherData, nonce, keyBytes).toByteArray())
.data
WithFill.decode(SecretBox.openEasy(cipherData, nonce, keyBytes))
}
override fun equals(other: Any?): Boolean {

View File

@ -7,7 +7,7 @@ import kotlinx.serialization.Transient
@Serializable
sealed class UniversalKey : DecryptingKey {
abstract val tag: KeyTag
// abstract val tag: KeyTag
@ -15,7 +15,7 @@ sealed class UniversalKey : DecryptingKey {
@SerialName("sy")
data class Symmetric(val key: SymmetricKey) : UniversalKey(), EncryptingKey by key, DecryptingKey by key {
@Transient
override val tag: KeyTag = key.encryptingTag
override val tag: KeyTag = key.tag
@Transient
override val nonceBytesLength: Int = key.nonceBytesLength
@ -28,7 +28,7 @@ sealed class UniversalKey : DecryptingKey {
data class Session(val key: SafeKeyExchange.SessionKey) : UniversalKey(), EncryptingKey by key,
DecryptingKey by key {
@Transient
override val tag: KeyTag = key.encryptingTag
override val tag: KeyTag = key.tag
@Transient
override val nonceBytesLength: Int = key.nonceBytesLength
}
@ -36,7 +36,7 @@ sealed class UniversalKey : DecryptingKey {
@Serializable
@SerialName("se")
data class Secret(val key: Asymmetric.SecretKey) : UniversalKey(), DecryptingKey by key {
override val tag: KeyTag by lazy { key.decryptingTag }
override val tag: KeyTag by lazy { key.tag }
override fun toString() = "U.Sec:$tag"
}

View File

@ -8,6 +8,7 @@ class UniversalRing(
private val keys: Collection<UniversalKey>
): Collection<UniversalKey> by keys {
constructor(vararg keys: UniversalKey) : this(keys.toSet())
constructor(vararg keys: DecryptingKey) : this(keys.map { UniversalKey.from(it) }.toSet())
@Transient
val keySet = if( keys is Set<UniversalKey> ) keys else keys.toSet()

View File

@ -0,0 +1,37 @@
package net.sergeych.crypto2
import kotlinx.serialization.Serializable
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto2.WithFill.Companion.decode
import net.sergeych.crypto2.WithFill.Companion.encode
/**
* Utility class to optionally extend a message with random bytes added _before the message bytes_ that
* reduces chances to effective cryptanalysis from the first block. Use
* [encode] and [decode] to process filled data.
*/
@Serializable
class WithFill private constructor(
@Suppress("unused")
val safetyFill: UByteArray = ubyteArrayOf(),
val data: UByteArray,
) {
companion object {
/**
* Create binary data with specified fill
*/
fun encode(data: UByteArray,range: IntRange? = null) =
BipackEncoder.encode(WithFill(range?.let {
require(range.first >= 0) { "range should not be negative" }
randomUBytes(randomInt(it))
} ?: ubyteArrayOf(), data)).toUByteArray()
/**
* extract binary data from filled
*/
fun decode(data: UByteArray): UByteArray =
BipackDecoder.decode<WithFill>(data.toByteArray()).data
}
}

View File

@ -0,0 +1,86 @@
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 kotlin.test.*
class ContainerTest {
@Test
fun testSingle() = runTest {
initCrypto()
val syk1 = SymmetricKey.random()
val syk2 = SymmetricKey.random()
val data = "sergeych, ohm many.".encodeToUByteArray()
val c = Container.create(data, syk1)
assertFalse { c.isDecrypted }
val c1 = Container.decode(c.encoded)
assertFalse { c.isDecrypted }
assertIs<Container.Single>(c)
assertNull(c1.decryptWith(syk2))
val d = c1.decryptWith(syk2, syk1)
assertNotNull(d)
assertContentEquals(data, d)
assertTrue { c1.isDecrypted }
}
@Test
fun testSinglePair() = runTest {
initCrypto()
val p1 = Asymmetric.generateKeys()
val p2 = Asymmetric.generateKeys()
val p3 = Asymmetric.generateKeys()
val data = "sergeych, ohm many.".encodeToUByteArray()
val c = Container.create(data, p1.secretKey to p2.publicKey)
assertFalse { c.isDecrypted }
val c1 = Container.decode(c.encoded)
assertFalse { c.isDecrypted }
assertIs<Container.Single>(c)
assertNull(c1.decryptWith(p3.secretKey))
val d = c1.decryptWith(p3.secretKey, p2.secretKey)
assertNotNull(d)
assertContentEquals(data, d)
assertTrue { c1.isDecrypted }
}
@Test
fun testSingleAsymmetric() = runTest {
initCrypto()
// val p1 = Asymmetric.generateKeys()
val p2 = Asymmetric.generateKeys()
val p3 = Asymmetric.generateKeys()
val data = "sergeych, ohm many.".encodeToUByteArray()
val c = Container.create(data) { key(p2.publicKey) }
assertFalse { c.isDecrypted }
val c1 = Container.decode(c.encoded)
assertFalse { c.isDecrypted }
assertIs<Container.Single>(c)
assertNull(c1.decryptWith(p3.secretKey))
val d = c1.decryptWith(p3.secretKey, p2.secretKey)
assertNotNull(d)
assertContentEquals(data, d)
assertTrue { c1.isDecrypted }
}
@Test
fun testMultipleSymmetric() = runTest {
initCrypto()
val syk1 = SymmetricKey.random()
val syk2 = SymmetricKey.random()
// val syk3 = SymmetricKey.random()
val data = "Translating the name 'Sergey Chernov' from Russian to archaic Sanskrit would be 'Ramo Krishna'"
.encodeToUByteArray()
val c = Container.create(data, syk1, syk2)
assertFalse { c.isDecrypted }
val c1 = Container.decode(c.encoded)
assertFalse { c1.isDecrypted }
}
}

View File

@ -3,7 +3,6 @@ import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto2.*
import net.sergeych.utools.now
import net.sergeych.utools.pack
@ -140,25 +139,24 @@ class KeysTest {
val plain = "The fake vaccine kills".encodeToUByteArray()
var m = Asymmetric.createMessage(sk0, pk1, plain)
var m = pk1.encryptMessage(plain, sk1)
assertContentEquals(plain, m.decrypt(sk1))
assertThrows<DecryptionFailedException> {
assertContentEquals(plain, m.decrypt(sk2))
}
m = pk2.encryptAnonymously(plain)
m = pk2.encryptAnonymousMessage(plain)
assertContentEquals(plain, m.decrypt(sk2))
assertContentEquals(plain, sk2.decrypt(m))
assertContentEquals(plain, sk2.decrypt(sk1.encrypt(plain, pk2)))
// assertContentEquals(plain, sk2.decrypt(sk1.encrypt(plain, pk2)))
assertThrows<DecryptionFailedException> {
assertContentEquals(plain, m.decrypt(sk1))
}
val x1 = BipackEncoder.encode(Asymmetric.createMessage(sk0, pk1, plain))
val x2 = BipackEncoder.encode(Asymmetric.createMessage(sk0, pk1, plain))
val x1 = pk1.encryptMessage(plain, sk1).encoded
val x2 = pk1.encryptMessage(plain, sk1).encoded
assertFalse { x1 contentEquals x2 }
}
@Test
fun asymmetricKeySerializationTest() = runTest {

View File

@ -52,4 +52,9 @@ class RingTest {
assertEquals(r, r2)
}
@Test
@Ignore
fun testKeysWithSameTags() {
// it should be able to keep keys with same tags
}
}