Container started

This commit is contained in:
sergeych committed 2024-06-19 16:46:32 +07:00
1 parent e4e3b5ba8b
commit e2916d0fe2
12 files changed
+360 -76

No files matched your search

@@ -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
@@ -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())
}
}
}
//
//
// /**
@@ -28,7 +28,7 @@ interface DecryptingKey : NonceBased {
fun decryptString(cipherData: UByteArray): String = decrypt(cipherData).decodeFromUByteArray()
val decryptingTag: KeyTag
val tag: KeyTag
}
@@ -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
}
@@ -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
@@ -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 {
@@ -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"
}
@@ -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()
@@ -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
}
}