Public-key encryption

This commit is contained in:
Sergey Chernov 2024-06-12 13:25:45 +07:00
parent 98cba5b129
commit a71a666568
2 changed files with 112 additions and 3 deletions

View File

@ -6,22 +6,55 @@ import com.ionspin.kotlin.crypto.box.crypto_box_NONCEBYTES
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import net.sergeych.crypto2.Asymmetric.Message
import net.sergeych.crypto2.Asymmetric.PublicKey
import net.sergeych.crypto2.Asymmetric.SecretKey
/**
* Public-key encryption implementation. Generally should be libsodium-compatible.
*
* ## How to
*
* - [Asymmetric.generateKeys] create a key pair. Keys are serializable.
* - [SecretKey] provides authenticated encryption for a [PublicKey] receiver.
* - [PublicKey] provides decryption and anonymous encryption.
* - [Message] is a serializable container with encrypted message and all necessary data to decrypt it.
*
* __Algorithms:__
*
* - Key exchange: X25519.
* - Encryption: XSalsa20
* - Authentication: Poly1305
*/
object Asymmetric { object Asymmetric {
/**
* Encrypted message holder.
*
* Do not instantiate it directly, use [SecretKey.encrypt], [PublicKey.encryptAnonymously] or [createMessage]
* 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.
* This class carries all this information; serialize and pass it to the recipient.
*/
@Serializable @Serializable
class Message( class Message(
private val nonce: UByteArray, private val nonce: UByteArray,
private val encryptedMessage: UByteArray, private val encryptedMessage: UByteArray,
val senderPublicKey: PublicKey? = null, val senderPublicKey: PublicKey? = null,
) { ) {
/**
* Decrypt the message, same as [SecretKey.decrypt]
*/
fun decrypt(recipientKey: SecretKey): UByteArray { fun decrypt(recipientKey: SecretKey): UByteArray {
check(senderPublicKey != null) check(senderPublicKey != null)
return decryptWithSenderKey(senderPublicKey, recipientKey) return decryptWithSenderKey(senderPublicKey, recipientKey)
} }
/**
* Decrypt a message which is not include sender's public key (which should somehow be
* known to the recipient). Use it if [senderPublicKey] is null.
*/
fun decryptWithSenderKey(senderKey: PublicKey, recipientKey: SecretKey): UByteArray { fun decryptWithSenderKey(senderKey: PublicKey, recipientKey: SecretKey): UByteArray {
return try { return try {
Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes) Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes)
@ -31,6 +64,21 @@ object Asymmetric {
} }
} }
/**
* Encrypt the [plainData] using [from] sender for [recipient] public key. Note that to decrypt it
* the [SecretKey] that corresponds to the [recipient] public key is needed, Sender can't decrypt the message!
*
* 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].
*
* @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( fun createMessage(
from: SecretKey, recipient: PublicKey, plainData: UByteArray, from: SecretKey, recipient: PublicKey, plainData: UByteArray,
excludeSenderKey: Boolean = false, excludeSenderKey: Boolean = false,
@ -43,8 +91,14 @@ object Asymmetric {
) )
} }
data class KeyPair(val secretKey: SecretKey, public val senderKey: PublicKey) /**
* The generated key pair. See [generateKeys]
*/
data class KeyPair(val secretKey: SecretKey,val senderKey: PublicKey)
/**
* Generate a new random pair of public and secret keys.
*/
fun generateKeys(): KeyPair { fun generateKeys(): KeyPair {
val p = Box.keypair() val p = Box.keypair()
val pk = PublicKey(p.publicKey) val pk = PublicKey(p.publicKey)
@ -53,6 +107,10 @@ object Asymmetric {
private fun randomNonce(): UByteArray = randomBytes(crypto_box_NONCEBYTES) private fun randomNonce(): UByteArray = randomBytes(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]
*/
@Serializable @Serializable
class PublicKey(val keyBytes: UByteArray) { class PublicKey(val keyBytes: UByteArray) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -62,11 +120,24 @@ object Asymmetric {
return keyBytes contentEquals other.keyBytes return keyBytes contentEquals other.keyBytes
} }
/**
* 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.
* 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)
override fun hashCode(): Int { override fun hashCode(): Int {
return keyBytes.hashCode() return keyBytes.hashCode()
} }
} }
/**
* The secret key
*/
@Serializable @Serializable
class SecretKey( class SecretKey(
val keyBytes: UByteArray, val keyBytes: UByteArray,
@ -74,8 +145,38 @@ object Asymmetric {
val _cachedPublicKey: PublicKey? = null, val _cachedPublicKey: PublicKey? = null,
) { ) {
/**
* 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,
* 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,
*/
fun decrypt(message: Message): UByteArray = message.decrypt(this)
/**
* Decrypt using [senderPublicKey] as a sender key (overriding the [Message.senderPublicKey] if set).
* See [decrypt] for more.
*/
fun decryptWithSenderKey(message: Message,senderPublicKey: PublicKey): UByteArray =
message.decryptWithSenderKey(senderPublicKey,this)
private var cachedPublicKey: PublicKey? = _cachedPublicKey private var cachedPublicKey: PublicKey? = _cachedPublicKey
/**
* The corresponding public key
*/
val publicKey: PublicKey by lazy { val publicKey: PublicKey by lazy {
PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes)) PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes))
.also { cachedPublicKey = it } .also { cachedPublicKey = it }

View File

@ -124,6 +124,14 @@ class KeysTest {
assertContentEquals(plain, m.decrypt(sk2)) assertContentEquals(plain, m.decrypt(sk2))
} }
m = pk2.encryptAnonymously(plain)
assertContentEquals(plain, m.decrypt(sk2))
assertContentEquals(plain, sk2.decrypt(m))
assertContentEquals(plain, sk2.decrypt(sk1.encrypt(plain, pk2)))
assertThrows<DecryptionFailedException> {
assertContentEquals(plain, m.decrypt(sk1))
}
} }
@Test @Test
fun asymmetricKeySerializationTest() = runTest { fun asymmetricKeySerializationTest() = runTest {