Enables decryption by nip04 and nip44 on NostrWalletConnect objects, NIP-51 lists and NIP-04 messages

This commit is contained in:
Vitor Pamplona 2024-08-28 15:20:25 -04:00
parent 0f86e3d8fb
commit cb4a73bb9c
9 changed files with 137 additions and 6 deletions

View File

@ -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=="))
}
}

View File

@ -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"

View File

@ -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,

View File

@ -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=")

View File

@ -108,7 +108,7 @@ abstract class GeneralListEvent(
}
try {
signer.nip04Decrypt(content, pubKey) {
signer.decrypt(content, pubKey) {
privateTagsCache = mapper.readValue<Array<Array<String>>>(it)
privateTagsCache?.let { onReady(it) }
}

View File

@ -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

View File

@ -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}")
}

View File

@ -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)

View File

@ -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 <T : Event> assembleRumor(
createdAt: Long,
kind: Int,