asymmetric key cryptography basics

This commit is contained in:
Sergey Chernov 2024-06-12 11:29:21 +07:00
parent 382c3277af
commit 98cba5b129
5 changed files with 142 additions and 11 deletions

View File

@ -0,0 +1,96 @@
package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.box.Box
import com.ionspin.kotlin.crypto.box.BoxCorruptedOrTamperedDataException
import com.ionspin.kotlin.crypto.box.crypto_box_NONCEBYTES
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
object Asymmetric {
@Serializable
class Message(
private val nonce: UByteArray,
private val encryptedMessage: UByteArray,
val senderPublicKey: PublicKey? = null,
) {
fun decrypt(recipientKey: SecretKey): UByteArray {
check(senderPublicKey != null)
return decryptWithSenderKey(senderPublicKey, recipientKey)
}
fun decryptWithSenderKey(senderKey: PublicKey, recipientKey: SecretKey): UByteArray {
return try {
Box.openEasy(encryptedMessage, nonce, senderKey.keyBytes, recipientKey.keyBytes)
} catch (_: BoxCorruptedOrTamperedDataException) {
throw DecryptionFailedException()
}
}
}
fun createMessage(
from: SecretKey, recipient: PublicKey, plainData: UByteArray,
excludeSenderKey: Boolean = false,
): Message {
val nonce = randomNonce()
return Message(
nonce,
Box.easy(plainData, nonce, recipient.keyBytes, from.keyBytes),
if (excludeSenderKey) null else from.publicKey
)
}
data class KeyPair(val secretKey: SecretKey, public val senderKey: PublicKey)
fun generateKeys(): KeyPair {
val p = Box.keypair()
val pk = PublicKey(p.publicKey)
return KeyPair(SecretKey(p.secretKey, pk), pk)
}
private fun randomNonce(): UByteArray = randomBytes(crypto_box_NONCEBYTES)
@Serializable
class PublicKey(val keyBytes: UByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PublicKey) return false
return keyBytes contentEquals other.keyBytes
}
override fun hashCode(): Int {
return keyBytes.hashCode()
}
}
@Serializable
class SecretKey(
val keyBytes: UByteArray,
@Transient
val _cachedPublicKey: PublicKey? = null,
) {
private var cachedPublicKey: PublicKey? = _cachedPublicKey
val publicKey: PublicKey by lazy {
PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes))
.also { cachedPublicKey = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SecretKey) return false
return keyBytes contentEquals other.keyBytes
}
override fun hashCode(): Int {
return keyBytes.hashCode()
}
}
}

View File

@ -6,8 +6,6 @@ import net.sergeych.bipack.BipackEncoder
import net.sergeych.bipack.decodeFromBipack import net.sergeych.bipack.decodeFromBipack
import net.sergeych.crypto2.Seal.Companion.create import net.sergeych.crypto2.Seal.Companion.create
import net.sergeych.utools.now import net.sergeych.utools.now
import kotlin.random.Random
import kotlin.random.nextUBytes
/** /**
* Extended public-key signature. * Extended public-key signature.
@ -138,7 +136,7 @@ class Seal(
expiresAt: Instant? = null, expiresAt: Instant? = null,
nonDeterministic: Boolean = false nonDeterministic: Boolean = false
): Seal { ): Seal {
val nonce = if( nonDeterministic ) Random.nextUBytes(32) else null val nonce = if( nonDeterministic ) randomBytes(32) else null
val data = BipackEncoder.encode(SealedData(message, nonce, createdAt, expiresAt)).toUByteArray() val data = BipackEncoder.encode(SealedData(message, nonce, createdAt, expiresAt)).toUByteArray()
return Seal(key.publicKey, key.sign(data), nonce, createdAt, expiresAt) return Seal(key.publicKey, key.sign(data), nonce, createdAt, expiresAt)
} }

View File

@ -5,16 +5,12 @@ import kotlinx.serialization.Serializable
import net.sergeych.bintools.toDataSource import net.sergeych.bintools.toDataSource
import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder import net.sergeych.bipack.BipackEncoder
import net.sergeych.crypto2.SymmetricKey.Companion.random
import kotlin.random.Random
import kotlin.random.nextInt
import kotlin.random.nextUBytes
/** /**
* Symmetric key implements authenticated encrypting with random nonce and optional fill. * Symmetric key implements authenticated encrypting with random nonce and optional fill.
* Random fill is normally used when cryptanalysis of the message size is a threat. * Random fill is normally used when cryptanalysis of the message size is a threat.
* *
* Do not call this constructor directly, use [random] or deserialize it. * Do not call this constructor directly, use [randomInt] or deserialize it.
* *
* __Algorithms:__ * __Algorithms:__
* *
@ -49,10 +45,10 @@ class SymmetricKey(
override fun encrypt(plainData: UByteArray,randomFill: IntRange?): UByteArray { override fun encrypt(plainData: UByteArray,randomFill: IntRange?): UByteArray {
val fill = randomFill?.let { val fill = randomFill?.let {
require(it.start >= 0) require(it.start >= 0)
Random.nextUBytes(Random.nextInt(it)) randomBytes(randomInt(it))
} }
val filled = BipackEncoder.encode(WithFill(plainData, fill)) val filled = BipackEncoder.encode(WithFill(plainData, fill))
val nonce = randomNonce() val nonce = randomSecretboxNonce()
val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, keyBytes) val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, keyBytes)
return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray() return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray()
} }

View File

@ -44,6 +44,13 @@ fun randomBytes(n: UInt): UByteArray = if (n > 0u) LibsodiumRandom.buf(n.toInt()
fun randomUInt(max: UInt) = LibsodiumRandom.uniform(max) fun randomUInt(max: UInt) = LibsodiumRandom.uniform(max)
fun randomUInt(max: Int) = LibsodiumRandom.uniform(max.toUInt()) fun randomUInt(max: Int) = LibsodiumRandom.uniform(max.toUInt())
fun randomInt(range: IntRange): Int {
val l = range.last - range.first + 1
return randomUInt(l.toUInt()).toInt() + range.first
}
fun randomInt() = randomInt( Int.MIN_VALUE ..< Int.MAX_VALUE)
fun <T: Comparable<T>>T.limit(range: ClosedRange<T>) = when { fun <T: Comparable<T>>T.limit(range: ClosedRange<T>) = when {
this < range.start -> range.start this < range.start -> range.start
this > range.endInclusive -> range.endInclusive this > range.endInclusive -> range.endInclusive
@ -53,6 +60,6 @@ fun <T: Comparable<T>>T.limit(range: ClosedRange<T>) = when {
fun <T: Comparable<T>>T.limitMax(max: T) = if( this < max ) this else max fun <T: Comparable<T>>T.limitMax(max: T) = if( this < max ) this else max
fun <T: Comparable<T>>T.limitMin(min: T) = if( this > min ) this else min fun <T: Comparable<T>>T.limitMin(min: T) = if( this > min ) this else min
fun randomNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES) fun randomSecretboxNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES)

View File

@ -1,6 +1,8 @@
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
import com.ionspin.kotlin.crypto.util.encodeToUByteArray import com.ionspin.kotlin.crypto.util.encodeToUByteArray
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.sergeych.crypto2.* import net.sergeych.crypto2.*
import net.sergeych.utools.now import net.sergeych.utools.now
import net.sergeych.utools.pack import net.sergeych.utools.pack
@ -105,4 +107,36 @@ class KeysTest {
} }
@Test
fun asymmetricKeyTest() = runTest {
initCrypto()
val (sk0, pk0) = Asymmetric.generateKeys()
assertEquals(pk0, sk0.publicKey)
val (sk1, pk1) = Asymmetric.generateKeys()
val (sk2, pk2) = Asymmetric.generateKeys()
val plain = "The fake vaccine kills".encodeToUByteArray()
var m = Asymmetric.createMessage(sk0, pk1, plain)
assertContentEquals(plain, m.decrypt(sk1))
assertThrows<DecryptionFailedException> {
assertContentEquals(plain, m.decrypt(sk2))
}
}
@Test
fun asymmetricKeySerializationTest() = runTest {
initCrypto()
val (sk0, pk0) = Asymmetric.generateKeys()
// println(sk0.publicKey)
val j = Json { prettyPrint = true}
val sk1 = j.decodeFromString<Asymmetric.SecretKey>(j.encodeToString(sk0))
assertEquals(sk0, sk1)
assertEquals(pk0, sk1.publicKey)
// println(j.encodeToString(sk1))
}
} }