diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt new file mode 100644 index 0000000..64e7833 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt @@ -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() + } + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt index 76413d2..296e3c7 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt @@ -6,8 +6,6 @@ import net.sergeych.bipack.BipackEncoder import net.sergeych.bipack.decodeFromBipack import net.sergeych.crypto2.Seal.Companion.create import net.sergeych.utools.now -import kotlin.random.Random -import kotlin.random.nextUBytes /** * Extended public-key signature. @@ -138,7 +136,7 @@ class Seal( expiresAt: Instant? = null, nonDeterministic: Boolean = false ): 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() return Seal(key.publicKey, key.sign(data), nonce, createdAt, expiresAt) } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt index 34ad337..6b0f5d0 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt @@ -5,16 +5,12 @@ import kotlinx.serialization.Serializable import net.sergeych.bintools.toDataSource import net.sergeych.bipack.BipackDecoder 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. * 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:__ * @@ -49,10 +45,10 @@ class SymmetricKey( override fun encrypt(plainData: UByteArray,randomFill: IntRange?): UByteArray { val fill = randomFill?.let { require(it.start >= 0) - Random.nextUBytes(Random.nextInt(it)) + randomBytes(randomInt(it)) } val filled = BipackEncoder.encode(WithFill(plainData, fill)) - val nonce = randomNonce() + val nonce = randomSecretboxNonce() val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, keyBytes) return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray() } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt b/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt index 74950ac..b86a752 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt @@ -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: 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.limit(range: ClosedRange) = when { this < range.start -> range.start this > range.endInclusive -> range.endInclusive @@ -53,6 +60,6 @@ fun >T.limit(range: ClosedRange) = when { fun >T.limitMax(max: T) = if( this < max ) this else max fun >T.limitMin(min: T) = if( this > min ) this else min -fun randomNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES) +fun randomSecretboxNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES) diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index 9c444a5..9d77807 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -1,6 +1,8 @@ import com.ionspin.kotlin.crypto.util.decodeFromUByteArray import com.ionspin.kotlin.crypto.util.encodeToUByteArray import kotlinx.coroutines.test.runTest +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import net.sergeych.crypto2.* import net.sergeych.utools.now 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 { + 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(j.encodeToString(sk0)) + assertEquals(sk0, sk1) + assertEquals(pk0, sk1.publicKey) +// println(j.encodeToString(sk1)) + } + } \ No newline at end of file