From 759b0c8b964740f65c3df551e81686a4decec5fa Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 15:31:33 +0100 Subject: [PATCH] Foundations(iOS Sourceset): Use alternative implementation for MacInstance, due to complications with mac length under previous implementation, which makes NIP49 tests(with all other relevant tests) pass. Some spotless fixes. --- quartz/build.gradle.kts | 4 +- .../quartz/utils/LibSodiumInstance.ios.kt | 67 +++++----- .../quartz/utils/mac/MacInstance.kt | 42 ++----- .../quartz/nip49PrivKeyEnc/NIP49Test.kt | 119 ++++++++++++++++++ 4 files changed, 169 insertions(+), 63 deletions(-) create mode 100644 quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 3a5305c33..9f6cf2b36 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -292,8 +292,8 @@ kotlin { implementation(libs.charlietap.cachemap) implementation(libs.net.thauvin.erik.urlencoder.lib) implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal) -// implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") -// implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") + implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") + implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt index a14715e88..3b56e181b 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt @@ -36,17 +36,18 @@ actual object LibSodiumInstance { nPub: ByteArray, k: ByteArray, ): Boolean { - val returnCode = Clibsodium.crypto_aead_xchacha20poly1305_ietf_decrypt( - m = message.uRefTo(0), - mlen_p = ulongArrayOf(message.size.toULong()).refTo(0), - nsec = nSec.uRefTo(0), - c = ciphertext.uRefTo(0), - clen = ciphertext.size.toULong(), - ad = ad.uRefTo(0), - adlen = ad.size.toULong(), - npub = nPub.uRefTo(0), - k = k.uRefTo(0) - ) + val returnCode = + Clibsodium.crypto_aead_xchacha20poly1305_ietf_decrypt( + m = message.uRefTo(0), + mlen_p = ulongArrayOf(message.size.toULong()).refTo(0), + nsec = nSec.uRefTo(0), + c = ciphertext.uRefTo(0), + clen = ciphertext.size.toULong(), + ad = ad.uRefTo(0), + adlen = ad.size.toULong(), + npub = nPub.uRefTo(0), + k = k.uRefTo(0), + ) return returnCode == 0 } @@ -59,17 +60,18 @@ actual object LibSodiumInstance { nPub: ByteArray, k: ByteArray, ): Boolean { - val retCode = Clibsodium.crypto_aead_xchacha20poly1305_ietf_encrypt( - c = ciphertext.uRefTo(0), - clen_p = ulongArrayOf(ciphertext.size.toULong()).refTo(0), - m = message.uRefTo(0), - mlen = message.size.toULong(), - ad = ad.uRefTo(0), - adlen = ad.size.toULong(), - nsec = nSec.uRefTo(0), - npub = nPub.uRefTo(0), - k = k.uRefTo(0) - ) + val retCode = + Clibsodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + c = ciphertext.uRefTo(0), + clen_p = ulongArrayOf(ciphertext.size.toULong()).refTo(0), + m = message.uRefTo(0), + mlen = message.size.toULong(), + ad = ad.uRefTo(0), + adlen = ad.size.toULong(), + nsec = nSec.uRefTo(0), + npub = nPub.uRefTo(0), + k = k.uRefTo(0), + ) return retCode == 0 } @@ -86,7 +88,7 @@ actual object LibSodiumInstance { m = message.uRefTo(0), mlen = message.size.toULong(), n = nonce?.uRefTo(0), - k = key?.uRefTo(0) + k = key?.uRefTo(0), ) return ciphertext } @@ -107,17 +109,18 @@ actual object LibSodiumInstance { out = k2.uRefTo(0), nonce.uRefTo(0), k = key.uRefTo(0), - c = null + c = null, ) - val resultCode = Clibsodium.crypto_stream_chacha20_xor_ic( - c = cipher.uRefTo(0), - m = messageBytes.uRefTo(0), - mlen = messageBytes.size.toULong(), - n = nonceChaCha.uRefTo(0), - ic = 0L.toULong(), - k = k2.uRefTo(0) - ) + val resultCode = + Clibsodium.crypto_stream_chacha20_xor_ic( + c = cipher.uRefTo(0), + m = messageBytes.uRefTo(0), + mlen = messageBytes.size.toULong(), + n = nonceChaCha.uRefTo(0), + ic = 0L.toULong(), + k = k2.uRefTo(0), + ) return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message") } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt index 52b65b39e..9b18fb579 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt @@ -20,62 +20,46 @@ */ package com.vitorpamplona.quartz.utils.mac -import dev.whyoleg.cryptography.CryptographyProvider -import dev.whyoleg.cryptography.algorithms.HMAC -import dev.whyoleg.cryptography.algorithms.SHA256 -import dev.whyoleg.cryptography.algorithms.SHA512 -import dev.whyoleg.cryptography.providers.apple.Apple +import io.github.andreypfau.kotlinx.crypto.HMac +import io.github.andreypfau.kotlinx.crypto.Sha256 +import io.github.andreypfau.kotlinx.crypto.Sha512 actual class MacInstance actual constructor( algorithm: String, key: ByteArray, ) { - private val cryptoProvider = CryptographyProvider.Apple - - private var internalMacInstance: HMAC.Key = - cryptoProvider - .get(HMAC) - .keyDecoder(digestForAlgorithm(algorithm)) - .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key) - - private var hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction() + private var nativeHmac = HMac(altDigestForAlgorithm(algorithm), key) actual fun init( key: ByteArray, algorithm: String, ) { - internalMacInstance = - cryptoProvider - .get(HMAC) - .keyDecoder(digestForAlgorithm(algorithm)) - .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key) - - hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction() + nativeHmac = HMac(altDigestForAlgorithm(algorithm), key) } - actual fun getMacLength(): Int = hmacSignFunction.signIntoByteArray(internalMacInstance.encodeToByteArrayBlocking(HMAC.Key.Format.RAW)) + actual fun getMacLength(): Int = nativeHmac.macSize actual fun update(array: ByteArray) { - hmacSignFunction.update(array) + nativeHmac.update(array) } actual fun update(byte: Byte) { - hmacSignFunction.update(byteArrayOf(byte)) + nativeHmac.update(byte) } - actual fun doFinal(): ByteArray = hmacSignFunction.signToByteArray() + actual fun doFinal(): ByteArray = nativeHmac.digest() actual fun doFinal( output: ByteArray, offset: Int, ) { - hmacSignFunction.signIntoByteArray(output, offset) + nativeHmac.digest(output, offset) } - private fun digestForAlgorithm(algorithm: String) = + private fun altDigestForAlgorithm(algorithm: String) = when (algorithm) { - "HmacSHA256" -> SHA256 - "HmacSHA512" -> SHA512 + "HmacSHA256" -> Sha256() + "HmacSHA512" -> Sha512() else -> error("Algorithm is not yet supported.") } } diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt new file mode 100644 index 000000000..c011e785e --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt @@ -0,0 +1,119 @@ +/* + * 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.nip49PrivKeyEnc + +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.fail + +public class NIP49Test { + companion object { + val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" + + val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" + val TEST_CASE_PASSWORD = "nostr" + + val MAIN_TEST_CASES = + listOf( + Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00), + Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01), + Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02), + Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00), + Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01), + Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 4, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 5, 0x01), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 1, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x01), + ) + } + + val nip49 = Nip49() + + @Test + fun decodeBech32() { + val data = + Nip49.EncryptedInfo.decodePayload( + TEST_CASE, + )!! + + assertEquals(2.toByte(), data.version) + assertEquals(16.toByte(), data.logn) + assertEquals("52d7c3f8580e7b41953381e5bc49646b", data.salt.toHexKey()) + assertEquals("c33f02a7dcaac8bdd8da23cd449783240b6ebc12edeea7bf", data.nonce.toHexKey()) + assertEquals(0.toByte(), data.keySecurity) + assertEquals("b8e8803440de7b3e9519c3e734cb2ac9a211ea2dc52312e5117a11a3022d813ab438719ca0b504a1193be510c3aee776", data.encryptedKey.toHexKey()) + } + + @Test + fun decrypt() { + val decrypted = nip49.decrypt(TEST_CASE, TEST_CASE_PASSWORD) + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + @Test + fun encryptDecryptTestCase() { + val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0) + val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD) + + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + @Test + fun encryptDecrypt() { + MAIN_TEST_CASES.forEach { + val encrypted = nip49.encrypt(it.secretKey, it.password, it.logn, it.ksb) + + assertNotNull(encrypted) + + val decrypted = nip49.decrypt(encrypted!!, it.password) + + assertEquals(it.secretKey, decrypted) + } + } + + @Test + fun normalization() { + val samePassword1 = byteArrayOf(0xE2.toByte(), 0x84.toByte(), 0xAB.toByte(), 0xE2.toByte(), 0x84.toByte(), 0xA6.toByte(), 0xE1.toByte(), 0xBA.toByte(), 0x9B.toByte(), 0xCC.toByte(), 0xA3.toByte()).decodeToString() + val samePassword2 = byteArrayOf(0xC3.toByte(), 0x85.toByte(), 0xCE.toByte(), 0xA9.toByte(), 0xE1.toByte(), 0xB9.toByte(), 0xA9.toByte()).decodeToString() + + if (samePassword1.encodeToByteArray().contentEquals(samePassword2.encodeToByteArray())) { + fail("Passwords should have a different byte representation") + } + + val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, samePassword1, 8, 0) + + assertNotNull(encrypted) + + val decrypted = nip49.decrypt(encrypted!!, samePassword2) + + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + class Nip49TestCase( + val password: String, + val secretKey: String, + val logn: Int, + val ksb: Byte, + ) +}