diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ChaCha20Benchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ChaCha20Benchmark.kt new file mode 100644 index 000000000..7b572b34e --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ChaCha20Benchmark.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.nip44Encryption.Nip44v2 +import com.vitorpamplona.quartz.nip44Encryption.crypto.ChaCha20 +import com.vitorpamplona.quartz.utils.RandomInstance +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ChaCha20Benchmark { + @get:Rule val benchmarkRule = BenchmarkRule() + + companion object Companion { + val nip44v2 = Nip44v2() + val msg = "Hi, how are you? this is supposed to be representative of an average message on Nostr" + + val privateKey = Nip01.privKeyCreate() + val publicKey = Nip01.pubKeyCreate(privateKey) + + val sharedKey = nip44v2.getConversationKey(privateKey, publicKey) + + val nonce = RandomInstance.bytes(32) + val messageKeys = nip44v2.getMessageKeys(sharedKey, nonce) + val padded = nip44v2.pad(msg) + + val chaCha = ChaCha20() + } + + @Test + fun encryptLibSodium() { + benchmarkRule.measureRepeated { + chaCha.encryptLibSodium(padded, messageKeys.chachaNonce, messageKeys.chachaKey) + } + } + + @Test + fun encryptNative() { + benchmarkRule.measureRepeated { + chaCha.encryptNative(padded, messageKeys.chachaNonce, messageKeys.chachaKey) + } + } + + @Test + fun decryptLibSodium() { + benchmarkRule.measureRepeated { + chaCha.decryptLibSodium(padded, messageKeys.chachaNonce, messageKeys.chachaKey) + } + } + + @Test + fun decryptNative() { + benchmarkRule.measureRepeated { + chaCha.decryptNative(padded, messageKeys.chachaNonce, messageKeys.chachaKey) + } + } +} diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt index b5fd1289b..9433108c1 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip44Encryption import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip44Encryption.crypto.ChaCha20 import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf -import com.vitorpamplona.quartz.utils.LibSodiumInstance import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.Secp256k1Instance import kotlinx.coroutines.CancellationException @@ -33,6 +33,7 @@ import kotlin.math.log2 class Nip44v2 { private val sharedKeyCache = SharedKeyCache() private val hkdf = Hkdf() + private val chaCha = ChaCha20() private val saltPrefix = "nip44-v2".toByteArray(Charsets.UTF_8) private val hashLength = 32 @@ -69,12 +70,7 @@ class Nip44v2 { val messageKeys = getMessageKeys(conversationKey, nonce) val padded = pad(plaintext) - val ciphertext = - LibSodiumInstance.cryptoStreamChaCha20IetfXor( - padded, - messageKeys.chachaNonce, - messageKeys.chachaKey, - ) + val ciphertext = chaCha.encrypt(padded, messageKeys.chachaNonce, messageKeys.chachaKey) val mac = hmacAad(messageKeys.hmacKey, ciphertext, nonce) @@ -122,11 +118,7 @@ class Nip44v2 { checkHMacAad(messageKey, decoded) return unpad( - LibSodiumInstance.cryptoStreamChaCha20IetfXor( - message = decoded.ciphertext, - nonce = messageKey.chachaNonce, - key = messageKey.chachaKey, - ), + chaCha.decrypt(decoded.ciphertext, messageKey.chachaNonce, messageKey.chachaKey), ) } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/crypto/ChaCha20.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/crypto/ChaCha20.kt new file mode 100644 index 000000000..1ddba1261 --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/crypto/ChaCha20.kt @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip44Encryption.crypto + +import com.vitorpamplona.quartz.utils.LibSodiumInstance +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec + +/** + * Encapsulates the ChaCha20 options. LibSodium is faster on real hardware: 851ns vs 2,535ns encrypt/decrypt times + */ +class ChaCha20 { + fun encrypt( + message: ByteArray, + nonce: ByteArray, + key: ByteArray, + ) = encryptLibSodium(message, nonce, key) + + fun decrypt( + message: ByteArray, + nonce: ByteArray, + key: ByteArray, + ) = decryptLibSodium(message, nonce, key) + + fun encryptNative( + message: ByteArray, + nonce: ByteArray, + key: ByteArray, + ): ByteArray { + val cipher = Cipher.getInstance("ChaCha20") + cipher.init(Cipher.ENCRYPT_MODE, FixedKey(key, "ChaCha20"), IvParameterSpec(nonce)) + return cipher.doFinal(message) + } + + fun decryptNative( + message: ByteArray, + nonce: ByteArray, + key: ByteArray, + ): ByteArray { + val cipher = Cipher.getInstance("ChaCha20") + cipher.init(Cipher.DECRYPT_MODE, FixedKey(key, "ChaCha20"), IvParameterSpec(nonce)) + return cipher.doFinal(message) + } + + fun encryptLibSodium( + message: ByteArray, + nonce: ByteArray, + key: ByteArray, + ) = LibSodiumInstance.cryptoStreamChaCha20IetfXor(message, nonce, key) + + fun decryptLibSodium( + message: ByteArray, + nonce: ByteArray, + key: ByteArray, + ) = LibSodiumInstance.cryptoStreamChaCha20IetfXor(message, nonce, key) +}