From cb4a73bb9c6138d960c543e9b54330be32a4b85f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Aug 2024 15:20:25 -0400 Subject: [PATCH] Enables decryption by nip04 and nip44 on NostrWalletConnect objects, NIP-51 lists and NIP-04 messages --- .../quartz/crypto/nip04/Nip04Test.kt | 96 +++++++++++++++++++ .../quartz/encoders/HexEncodingTest.kt | 3 + .../quartz/crypto/CryptoUtils.kt | 11 +++ .../quartz/crypto/nip04/Nip04.kt | 12 ++- .../quartz/events/GeneralListEvent.kt | 2 +- .../quartz/events/LnZapPaymentRequestEvent.kt | 2 +- .../events/LnZapPaymentResponseEvent.kt | 2 +- .../quartz/events/PrivateDmEvent.kt | 2 +- .../quartz/signers/NostrSigner.kt | 13 +++ 9 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/crypto/nip04/Nip04Test.kt diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/crypto/nip04/Nip04Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/crypto/nip04/Nip04Test.kt new file mode 100644 index 000000000..da3f42214 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/crypto/nip04/Nip04Test.kt @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2024 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.crypto.nip04 + +import com.vitorpamplona.quartz.crypto.nip01.Nip01 +import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.encoders.toHexKey +import fr.acinq.secp256k1.Secp256k1 +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import org.junit.Test +import java.security.SecureRandom + +class Nip04Test { + private val random = SecureRandom() + private val nip01 = Nip01(Secp256k1.get(), random) + private val nip04 = Nip04(Secp256k1.get(), random) + + val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray() + val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray() + val pk1 = nip01.pubkeyCreate(sk1) + val pk2 = nip01.pubkeyCreate(sk2) + + val expectedShared = "7ce22696eb0e303ddaa491bdf2a56b79d249f2d861b8e012a933e01dc4beba81" + + @Test + fun conversationKeyTest() { + assertEquals( + expectedShared, + nip04.computeSharedSecret(sk2, pk1).toHexKey(), + ) + + assertEquals( + expectedShared, + nip04.computeSharedSecret(sk1, pk2).toHexKey(), + ) + } + + @Test + fun encryptDecryptTest() { + val message = "testing" + val cipher = nip04.encrypt(message, sk2, pk1) + + assertEquals(message, nip04.decrypt(cipher, sk2, pk1)) + assertEquals(message, nip04.decrypt(cipher, sk1, pk2)) + + val cipher2 = nip04.encrypt(message, sk1, pk2) + + assertEquals(message, nip04.decrypt(cipher2, sk2, pk1)) + assertEquals(message, nip04.decrypt(cipher2, sk1, pk2)) + } + + @Test + fun decryptTest() { + val cipher = "zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ==" + + assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk2, pk1))) + assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk1, pk2))) + } + + @Test + fun decryptLargePayloadTest() { + val ciphertext = + "6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g==" + + val expected = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + + assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk2, pk1))) + assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk1, pk2))) + } + + @Test + fun isNIP04Encode() { + assertTrue(Nip04.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA==")) + assertTrue(Nip04.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ==")) + assertTrue(Nip04.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g==")) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt index 5d6451b54..485444251 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt @@ -20,10 +20,13 @@ */ package com.vitorpamplona.quartz.encoders +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.crypto.CryptoUtils import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) class HexEncodingTest { val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt index da9450d6a..76ecb05da 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt @@ -81,6 +81,17 @@ object CryptoUtils { return nip01.sha256(data) } + fun decrypt( + msg: String, + privateKey: ByteArray, + pubKey: ByteArray, + ): String? = + if (Nip04.isNIP04(msg)) { + decryptNIP04(msg, privateKey, pubKey) + } else { + decryptNIP44(msg, privateKey, pubKey) + } + /** NIP 04 Utils */ fun encryptNIP04( msg: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/nip04/Nip04.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/nip04/Nip04.kt index 5d242dd66..c9cb73a69 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/nip04/Nip04.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/nip04/Nip04.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.crypto.nip04 import android.util.Log import com.vitorpamplona.quartz.crypto.SharedKeyCache -import com.vitorpamplona.quartz.crypto.nip44.Nip44v1 import com.vitorpamplona.quartz.encoders.Hex import fr.acinq.secp256k1.Secp256k1 import java.security.SecureRandom @@ -128,6 +127,10 @@ class Nip04( pubKey: ByteArray, ): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) + companion object { + fun isNIP04(encoded: String) = EncryptedInfo.isNIP04(encoded) + } + class EncryptedInfo( val ciphertext: ByteArray, val nonce: ByteArray, @@ -138,7 +141,7 @@ class Nip04( fun decodePayload(payload: String): EncryptedInfo? { return try { val byteArray = Base64.getDecoder().decode(payload) - check(byteArray[0].toInt() == Nip44v1.EncryptedInfo.V) + check(byteArray[0].toInt() == V) return EncryptedInfo( nonce = byteArray.copyOfRange(1, 25), ciphertext = byteArray.copyOfRange(25, byteArray.size), @@ -149,6 +152,11 @@ class Nip04( } } + fun isNIP04(encoded: String): Boolean { + val l = encoded.length + return encoded[l - 28] == '?' && encoded[l - 27] == 'i' && encoded[l - 26] == 'v' && encoded[l - 25] == '=' + } + fun decodeFromNIP04(payload: String): EncryptedInfo? = try { val parts = payload.split("?iv=") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index f26b9f3bc..d27221426 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -108,7 +108,7 @@ abstract class GeneralListEvent( } try { - signer.nip04Decrypt(content, pubKey) { + signer.decrypt(content, pubKey) { privateTagsCache = mapper.readValue>>(it) privateTagsCache?.let { onReady(it) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt index 3efd97eaf..cd7ed54ff 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt @@ -62,7 +62,7 @@ class LnZapPaymentRequestEvent( } try { - signer.nip04Decrypt(content, talkingWith(signer.pubKey)) { jsonText -> + signer.decrypt(content, talkingWith(signer.pubKey)) { jsonText -> val payInvoiceMethod = mapper.readValue(jsonText, Request::class.java) lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt index 60c83dc13..e9e1cc7ce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt @@ -57,7 +57,7 @@ class LnZapPaymentResponseEvent( onReady: (String) -> Unit, ) { try { - signer.nip04Decrypt(content, talkingWith(signer.pubKey)) { content -> onReady(content) } + signer.decrypt(content, talkingWith(signer.pubKey)) { content -> onReady(content) } } catch (e: Exception) { Log.w("PrivateDM", "Error decrypting the message ${e.message}") } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt index 83b8d8770..b0584c8d4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt @@ -92,7 +92,7 @@ class PrivateDmEvent( return } - signer.nip04Decrypt(content, talkingWith(signer.pubKey)) { retVal -> + signer.decrypt(content, talkingWith(signer.pubKey)) { retVal -> val content = if (retVal.startsWith(NIP_18_ADVERTISEMENT)) { retVal.substring(16) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt index dad1dac91..866dc31fe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.signers +import com.vitorpamplona.quartz.crypto.nip04.Nip04 import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.Event @@ -67,6 +68,18 @@ abstract class NostrSigner( onReady: (LnZapPrivateEvent) -> Unit, ) + fun decrypt( + encryptedContent: String, + fromPublicKey: HexKey, + onReady: (String) -> Unit, + ) { + if (Nip04.isNIP04(encryptedContent)) { + nip04Decrypt(encryptedContent, fromPublicKey, onReady) + } else { + nip44Decrypt(encryptedContent, fromPublicKey, onReady) + } + } + fun assembleRumor( createdAt: Long, kind: Int,