From b5709f95271e5b8fed25986da7f109dc8e2b8d43 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Aug 2024 17:39:25 -0400 Subject: [PATCH] Adds basic support for NIP-46 events. --- .../quartz/events/nip46/Nip46Test.kt | 254 ++++++++++++++++++ .../com/vitorpamplona/quartz/events/Event.kt | 12 +- .../quartz/events/EventFactory.kt | 2 + .../quartz/events/nip46/BunkerMessage.kt | 46 ++++ .../quartz/events/nip46/BunkerRequest.kt | 89 ++++++ .../events/nip46/BunkerRequestConnect.kt | 42 +++ .../events/nip46/BunkerRequestGetPublicKey.kt | 36 +++ .../events/nip46/BunkerRequestGetRelays.kt | 36 +++ .../events/nip46/BunkerRequestNip04Decrypt.kt | 39 +++ .../events/nip46/BunkerRequestNip04Encrypt.kt | 39 +++ .../events/nip46/BunkerRequestNip44Decrypt.kt | 39 +++ .../events/nip46/BunkerRequestNip44Encrypt.kt | 39 +++ .../quartz/events/nip46/BunkerRequestPing.kt | 36 +++ .../quartz/events/nip46/BunkerRequestSign.kt | 38 +++ .../quartz/events/nip46/BunkerResponse.kt | 103 +++++++ .../quartz/events/nip46/BunkerResponseAck.kt | 37 +++ .../events/nip46/BunkerResponseDecrypt.kt | 36 +++ .../events/nip46/BunkerResponseEncrypt.kt | 36 +++ .../events/nip46/BunkerResponseError.kt | 36 +++ .../events/nip46/BunkerResponseEvent.kt | 37 +++ .../events/nip46/BunkerResponseGetRelays.kt | 39 +++ .../quartz/events/nip46/BunkerResponsePong.kt | 37 +++ .../events/nip46/BunkerResponsePublicKey.kt | 37 +++ .../quartz/events/nip46/NostrConnectEvent.kt | 108 ++++++++ 24 files changed, 1251 insertions(+), 2 deletions(-) create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/events/nip46/Nip46Test.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerMessage.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestConnect.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestGetPublicKey.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestGetRelays.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Decrypt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Encrypt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Decrypt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Encrypt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestPing.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestSign.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseAck.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseDecrypt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEncrypt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseError.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseGetRelays.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePong.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePublicKey.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/NostrConnectEvent.kt diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/events/nip46/Nip46Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/events/nip46/Nip46Test.kt new file mode 100644 index 000000000..38ce6f725 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/events/nip46/Nip46Test.kt @@ -0,0 +1,254 @@ +/** + * 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.events.nip46 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.events.ContactListEvent +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.signers.NostrSignerInternal +import com.vitorpamplona.quartz.utils.TimeUtils +import junit.framework.TestCase.assertEquals +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +internal class Nip46Test { + val remoteKey = NostrSignerInternal(KeyPair()) + val signer = NostrSignerInternal(KeyPair()) + val peer = NostrSignerInternal(KeyPair()) + + val dummyEvent = + Event( + id = "", + pubKey = "", + createdAt = TimeUtils.now(), + kind = 1, + tags = emptyArray(), + content = "test", + sig = "", + ) + + fun encodeDecodeEvent(req: T): T { + var countDownLatch = CountDownLatch(1) + var eventStr: String? = null + + NostrConnectEvent.create(req, remoteKey.pubKey, signer) { + eventStr = it.toJson() + countDownLatch.countDown() + } + + countDownLatch.await() + + countDownLatch = CountDownLatch(1) + var innerMessage: T? = null + + (Event.fromJson(eventStr!!) as NostrConnectEvent).plainContent(signer) { + innerMessage = it as T + countDownLatch.countDown() + } + + countDownLatch.await() + + return innerMessage!! + } + + @Test + fun signEncoder() { + val expected = BunkerRequestSign(event = dummyEvent) + val actual = encodeDecodeEvent(expected) + + assertEquals(BunkerRequestSign.METHOD_NAME, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(dummyEvent.id, actual.event.id) + } + + @Test + fun connectEncoder() { + val expected = BunkerRequestConnect(remoteKey = remoteKey.pubKey) + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + } + + @Test + fun pingEncoder() { + val expected = BunkerRequestPing() + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + } + + @Test + fun getPubkeyEncoder() { + val expected = BunkerRequestGetPublicKey() + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + } + + @Test + fun getRelaysEncoder() { + val expected = BunkerRequestGetRelays() + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + } + + @Test + fun testNip04Encrypt() { + val expected = BunkerRequestNip04Encrypt(pubKey = peer.pubKey, message = "Test") + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.message, actual.message) + } + + @Test + fun testNip44Encrypt() { + val expected = BunkerRequestNip44Encrypt(pubKey = peer.pubKey, message = "Test") + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.message, actual.message) + } + + @Test + fun testNip04Decrypt() { + val expected = BunkerRequestNip04Decrypt(pubKey = peer.pubKey, ciphertext = "Test") + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.ciphertext, actual.ciphertext) + } + + @Test + fun testNip44Decrypt() { + val expected = BunkerRequestNip44Decrypt(pubKey = peer.pubKey, ciphertext = "Test") + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.ciphertext, actual.ciphertext) + } + + // Responses + + @Test + fun testAckResponse() { + val expected = BunkerResponseAck() + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + } + + @Test + fun testPongResponse() { + val expected = BunkerResponsePong() + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + } + + @Test + fun testErrorResponse() { + val expected = BunkerResponseError(error = "Error") + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + } + + @Test + fun testEventResponse() { + val expected = BunkerResponseEvent(event = dummyEvent) + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(dummyEvent.id, actual.event.id) + } + + @Test + fun testPubkeyResponse() { + val expected = BunkerResponsePublicKey(pubkey = peer.pubKey) + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.pubkey, actual.pubkey) + } + + @Test + fun testRelaysResponse() { + val expected = BunkerResponseGetRelays(relays = mapOf("url" to ContactListEvent.ReadWrite(true, false))) + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.relays["url"], actual.relays["url"]) + } + + @Test + @Ignore("Impossible to recreate the class since there are no hints on the json") + fun testDecryptResponse() { + val expected = BunkerResponseDecrypt(plaintext = "Test") + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.plaintext, actual.plaintext) + } + + @Test + @Ignore("Impossible to recreate the class since there are no hints on the json") + fun testEncryptResponse() { + val expected = BunkerResponseEncrypt(ciphertext = "Test") + val actual = encodeDecodeEvent(expected) + + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.ciphertext, actual.ciphertext) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index ced139145..5968b1452 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -43,6 +43,9 @@ import com.vitorpamplona.quartz.encoders.Nip01Serializer import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.PoWRank import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.events.nip46.BunkerMessage +import com.vitorpamplona.quartz.events.nip46.BunkerRequest +import com.vitorpamplona.quartz.events.nip46.BunkerResponse import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -435,7 +438,12 @@ open class Event( .addSerializer(Gossip::class.java, GossipSerializer()) .addDeserializer(Gossip::class.java, GossipDeserializer()) .addDeserializer(Response::class.java, ResponseDeserializer()) - .addDeserializer(Request::class.java, RequestDeserializer()), + .addDeserializer(Request::class.java, RequestDeserializer()) + .addDeserializer(BunkerMessage::class.java, BunkerMessage.BunkerMessageDeserializer()) + .addSerializer(BunkerRequest::class.java, BunkerRequest.BunkerRequestSerializer()) + .addDeserializer(BunkerRequest::class.java, BunkerRequest.BunkerRequestDeserializer()) + .addSerializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseSerializer()) + .addDeserializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseDeserializer()), ) fun fromJson(jsonObject: JsonNode): Event = @@ -452,7 +460,7 @@ open class Event( sig = jsonObject.get("sig").asText(), ) - private inline fun JsonNode.toTypedArray(transform: (JsonNode) -> R): Array = Array(size()) { transform(get(it)) } + inline fun JsonNode.toTypedArray(transform: (JsonNode) -> R): Array = Array(size()) { transform(get(it)) } fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index ed64ea91e..b7315d553 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.events import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.events.nip46.NostrConnectEvent class EventFactory { companion object { @@ -120,6 +121,7 @@ class EventFactory { MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig) NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig) + NostrConnectEvent.KIND -> NostrConnectEvent(id, pubKey, createdAt, tags, content, sig) NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig) NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) NIP90ContentDiscoveryResponseEvent.KIND -> NIP90ContentDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerMessage.kt new file mode 100644 index 000000000..4d5b145f5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerMessage.kt @@ -0,0 +1,46 @@ +/** + * 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.events.nip46 + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.deser.std.StdDeserializer + +abstract class BunkerMessage { + abstract fun countMemory(): Long + + class BunkerMessageDeserializer : StdDeserializer(BunkerMessage::class.java) { + override fun deserialize( + jp: JsonParser, + ctxt: DeserializationContext, + ): BunkerMessage { + val jsonObject: JsonNode = jp.codec.readTree(jp) + val isRequest = jsonObject.has("method") + + if (isRequest) { + return jp.codec.treeToValue(jsonObject, BunkerRequest::class.java) + } else { + return jp.codec.treeToValue(jsonObject, BunkerResponse::class.java) + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequest.kt new file mode 100644 index 000000000..9425b8fa6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequest.kt @@ -0,0 +1,89 @@ +/** + * 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.events.nip46 + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.vitorpamplona.quartz.events.Event.Companion.toTypedArray +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +open class BunkerRequest( + val id: String, + val method: String, + val params: Array = emptyArray(), +) : BunkerMessage() { + override fun countMemory(): Long = + 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) + id.bytesUsedInMemory() + + method.bytesUsedInMemory() + + params.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } + + class BunkerRequestDeserializer : StdDeserializer(BunkerRequest::class.java) { + override fun deserialize( + jp: JsonParser, + ctxt: DeserializationContext, + ): BunkerRequest { + val jsonObject: JsonNode = jp.codec.readTree(jp) + val id = jsonObject.get("id").asText().intern() + val method = jsonObject.get("method").asText().intern() + val params = jsonObject.get("params")?.toTypedArray { it.asText().intern() } ?: emptyArray() + + return when (method) { + BunkerRequestConnect.METHOD_NAME -> BunkerRequestConnect.parse(id, params) + BunkerRequestGetPublicKey.METHOD_NAME -> BunkerRequestGetPublicKey.parse(id, params) + BunkerRequestGetRelays.METHOD_NAME -> BunkerRequestGetRelays.parse(id, params) + + BunkerRequestNip04Decrypt.METHOD_NAME -> BunkerRequestNip04Decrypt.parse(id, params) + BunkerRequestNip04Encrypt.METHOD_NAME -> BunkerRequestNip04Encrypt.parse(id, params) + BunkerRequestNip44Decrypt.METHOD_NAME -> BunkerRequestNip44Decrypt.parse(id, params) + BunkerRequestNip44Encrypt.METHOD_NAME -> BunkerRequestNip44Encrypt.parse(id, params) + + BunkerRequestPing.METHOD_NAME -> BunkerRequestPing.parse(id, params) + BunkerRequestSign.METHOD_NAME -> BunkerRequestSign.parse(id, params) + else -> BunkerRequest(id, method, params) + } + } + } + + class BunkerRequestSerializer : StdSerializer(BunkerRequest::class.java) { + override fun serialize( + value: BunkerRequest, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartObject() + gen.writeStringField("id", value.id) + gen.writeStringField("method", value.method) + gen.writeArrayFieldStart("params") + value.params.forEach { + gen.writeString(it) + } + gen.writeEndArray() + gen.writeEndObject() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestConnect.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestConnect.kt new file mode 100644 index 000000000..531f40386 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestConnect.kt @@ -0,0 +1,42 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.encoders.HexKey +import java.util.UUID + +class BunkerRequestConnect( + id: String = UUID.randomUUID().toString(), + val remoteKey: HexKey, + val secret: HexKey? = null, + val permissions: String? = null, +) : BunkerRequest(id, METHOD_NAME, listOfNotNull(remoteKey, secret, permissions).toTypedArray()) { + companion object { + val METHOD_NAME = "connect" + + fun parse( + id: String, + params: Array, + ): BunkerRequestConnect = BunkerRequestConnect(id, params[0], params.getOrNull(1), params.getOrNull(2)) + } +} + +public fun Array.getOrNull(index: Int): T? = if (index in 0.., + ): BunkerRequestGetPublicKey = BunkerRequestGetPublicKey(id) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestGetRelays.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestGetRelays.kt new file mode 100644 index 000000000..704d88c39 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestGetRelays.kt @@ -0,0 +1,36 @@ +/** + * 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.events.nip46 + +import java.util.UUID + +class BunkerRequestGetRelays( + id: String = UUID.randomUUID().toString(), +) : BunkerRequest(id, METHOD_NAME, arrayOf()) { + companion object { + val METHOD_NAME = "get_relays" + + fun parse( + id: String, + params: Array, + ): BunkerRequestGetRelays = BunkerRequestGetRelays(id) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Decrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Decrypt.kt new file mode 100644 index 000000000..af7b90f94 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Decrypt.kt @@ -0,0 +1,39 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.encoders.HexKey +import java.util.UUID + +class BunkerRequestNip04Decrypt( + id: String = UUID.randomUUID().toString(), + val pubKey: HexKey, + val ciphertext: String, +) : BunkerRequest(id, METHOD_NAME, listOfNotNull(pubKey, ciphertext).toTypedArray()) { + companion object { + val METHOD_NAME = "nip04_decrypt" + + fun parse( + id: String, + params: Array, + ): BunkerRequestNip04Decrypt = BunkerRequestNip04Decrypt(id, params[0], params.get(1)) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Encrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Encrypt.kt new file mode 100644 index 000000000..eed216c91 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip04Encrypt.kt @@ -0,0 +1,39 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.encoders.HexKey +import java.util.UUID + +class BunkerRequestNip04Encrypt( + id: String = UUID.randomUUID().toString(), + val pubKey: HexKey, + val message: String, +) : BunkerRequest(id, METHOD_NAME, listOfNotNull(pubKey, message).toTypedArray()) { + companion object { + val METHOD_NAME = "nip04_encrypt" + + fun parse( + id: String, + params: Array, + ): BunkerRequestNip04Encrypt = BunkerRequestNip04Encrypt(id, params[0], params.get(1)) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Decrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Decrypt.kt new file mode 100644 index 000000000..85416bd92 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Decrypt.kt @@ -0,0 +1,39 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.encoders.HexKey +import java.util.UUID + +class BunkerRequestNip44Decrypt( + id: String = UUID.randomUUID().toString(), + val pubKey: HexKey, + val ciphertext: String, +) : BunkerRequest(id, METHOD_NAME, listOfNotNull(pubKey, ciphertext).toTypedArray()) { + companion object { + val METHOD_NAME = "nip44_decrypt" + + fun parse( + id: String, + params: Array, + ): BunkerRequestNip44Decrypt = BunkerRequestNip44Decrypt(id, params[0], params.get(1)) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Encrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Encrypt.kt new file mode 100644 index 000000000..3009f66ad --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestNip44Encrypt.kt @@ -0,0 +1,39 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.encoders.HexKey +import java.util.UUID + +class BunkerRequestNip44Encrypt( + id: String = UUID.randomUUID().toString(), + val pubKey: HexKey, + val message: String, +) : BunkerRequest(id, METHOD_NAME, listOfNotNull(pubKey, message).toTypedArray()) { + companion object { + val METHOD_NAME = "nip44_encrypt" + + fun parse( + id: String, + params: Array, + ): BunkerRequestNip44Encrypt = BunkerRequestNip44Encrypt(id, params[0], params.get(1)) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestPing.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestPing.kt new file mode 100644 index 000000000..3ad3d0c43 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestPing.kt @@ -0,0 +1,36 @@ +/** + * 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.events.nip46 + +import java.util.UUID + +class BunkerRequestPing( + id: String = UUID.randomUUID().toString(), +) : BunkerRequest(id, METHOD_NAME, arrayOf()) { + companion object { + val METHOD_NAME = "ping" + + fun parse( + id: String, + params: Array, + ): BunkerRequestPing = BunkerRequestPing(id) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestSign.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestSign.kt new file mode 100644 index 000000000..0f4d94012 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerRequestSign.kt @@ -0,0 +1,38 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.events.Event +import java.util.UUID + +class BunkerRequestSign( + id: String = UUID.randomUUID().toString(), + val event: Event, +) : BunkerRequest(id, METHOD_NAME, arrayOf(event.toJson())) { + companion object { + val METHOD_NAME = "sign_event" + + fun parse( + id: String, + params: Array, + ): BunkerRequestSign = BunkerRequestSign(id, Event.fromJson(params[0])) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponse.kt new file mode 100644 index 000000000..a25cf8755 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponse.kt @@ -0,0 +1,103 @@ +/** + * 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.events.nip46 + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.vitorpamplona.quartz.encoders.HexValidator +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +open class BunkerResponse( + val id: String, + val result: String?, + val error: String?, +) : BunkerMessage() { + override fun countMemory(): Long = + 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) + id.bytesUsedInMemory() + + (result?.bytesUsedInMemory() ?: 0) + + (error?.bytesUsedInMemory() ?: 0) + + class BunkerResponseDeserializer : StdDeserializer(BunkerResponse::class.java) { + override fun deserialize( + jp: JsonParser, + ctxt: DeserializationContext, + ): BunkerResponse { + val jsonObject: JsonNode = jp.codec.readTree(jp) + + val id = jsonObject.get("id").asText().intern() + val result = jsonObject.get("result")?.asText() + val error = jsonObject.get("error")?.asText() + + if (error != null) { + return BunkerResponseError.parse(id, result, error) + } + + if (result != null) { + when (result) { + BunkerResponseAck.RESULT -> return BunkerResponseAck.parse(id, result, error) + BunkerResponsePong.RESULT -> return BunkerResponsePong.parse(id, result, error) + else -> { + if (result.length == 64 && HexValidator.isHex(result)) { + return BunkerResponsePublicKey.parse(id, result) + } + + if (result.get(0) == '{') { + try { + return BunkerResponseEvent.parse(id, result) + } catch (_: Exception) { + } + + try { + return BunkerResponseGetRelays.parse(id, result) + } catch (_: Exception) { + } + } + + return BunkerResponse(id, result, error) + } + } + } + + return BunkerResponse(id, result, error) + } + } + + class BunkerResponseSerializer : StdSerializer(BunkerResponse::class.java) { + override fun serialize( + value: BunkerResponse, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartObject() + gen.writeStringField("id", value.id) + value.result?.let { gen.writeStringField("result", value.result) } + value.error?.let { gen.writeStringField("error", it) } + gen.writeEndObject() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseAck.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseAck.kt new file mode 100644 index 000000000..9d5e0edf2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseAck.kt @@ -0,0 +1,37 @@ +/** + * 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.events.nip46 + +import java.util.UUID + +class BunkerResponseAck( + id: String = UUID.randomUUID().toString(), +) : BunkerResponse(id, RESULT, null) { + companion object { + val RESULT = "ack" + + fun parse( + id: String, + result: String, + error: String? = null, + ) = BunkerResponseAck(id) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseDecrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseDecrypt.kt new file mode 100644 index 000000000..0cea12bbc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseDecrypt.kt @@ -0,0 +1,36 @@ +/** + * 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.events.nip46 + +import java.util.UUID + +class BunkerResponseDecrypt( + id: String = UUID.randomUUID().toString(), + val plaintext: String, +) : BunkerResponse(id, plaintext, null) { + companion object { + fun parse( + id: String, + result: String, + error: String? = null, + ) = BunkerResponseDecrypt(id, result) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEncrypt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEncrypt.kt new file mode 100644 index 000000000..1b9d2b3fc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEncrypt.kt @@ -0,0 +1,36 @@ +/** + * 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.events.nip46 + +import java.util.UUID + +class BunkerResponseEncrypt( + id: String = UUID.randomUUID().toString(), + val ciphertext: String, +) : BunkerResponse(id, ciphertext, null) { + companion object { + fun parse( + id: String, + result: String, + error: String? = null, + ) = BunkerResponseEncrypt(id, result) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseError.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseError.kt new file mode 100644 index 000000000..97eb55c8b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseError.kt @@ -0,0 +1,36 @@ +/** + * 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.events.nip46 + +import java.util.UUID + +class BunkerResponseError( + id: String = UUID.randomUUID().toString(), + error: String, +) : BunkerResponse(id, null, error) { + companion object { + fun parse( + id: String, + result: String? = null, + error: String, + ) = BunkerResponseError(id, error) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEvent.kt new file mode 100644 index 000000000..de24f00d7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseEvent.kt @@ -0,0 +1,37 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.events.Event +import java.util.UUID + +class BunkerResponseEvent( + id: String = UUID.randomUUID().toString(), + val event: Event, +) : BunkerResponse(id, event.toJson(), null) { + companion object { + fun parse( + id: String, + result: String, + error: String? = null, + ) = BunkerResponseEvent(id, Event.fromJson(result)) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseGetRelays.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseGetRelays.kt new file mode 100644 index 000000000..adea55b72 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponseGetRelays.kt @@ -0,0 +1,39 @@ +/** + * 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.events.nip46 + +import com.fasterxml.jackson.core.type.TypeReference +import com.vitorpamplona.quartz.events.ContactListEvent.ReadWrite +import com.vitorpamplona.quartz.events.Event +import java.util.UUID + +class BunkerResponseGetRelays( + id: String = UUID.randomUUID().toString(), + val relays: Map, +) : BunkerResponse(id, Event.mapper.writeValueAsString(relays), null) { + companion object { + fun parse( + id: String, + result: String, + error: String? = null, + ) = BunkerResponseGetRelays(id, Event.mapper.readValue(result, object : TypeReference>() {})) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePong.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePong.kt new file mode 100644 index 000000000..70380a99f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePong.kt @@ -0,0 +1,37 @@ +/** + * 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.events.nip46 + +import java.util.UUID + +class BunkerResponsePong( + id: String = UUID.randomUUID().toString(), +) : BunkerResponse(id, RESULT, null) { + companion object { + val RESULT = "pong" + + fun parse( + id: String, + result: String, + error: String? = null, + ) = BunkerResponsePong(id) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePublicKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePublicKey.kt new file mode 100644 index 000000000..3794de71e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/BunkerResponsePublicKey.kt @@ -0,0 +1,37 @@ +/** + * 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.events.nip46 + +import com.vitorpamplona.quartz.encoders.HexKey +import java.util.UUID + +class BunkerResponsePublicKey( + id: String = UUID.randomUUID().toString(), + val pubkey: HexKey, +) : BunkerResponse(id, pubkey, null) { + companion object { + fun parse( + id: String, + result: String, + error: String? = null, + ) = BunkerResponsePublicKey(id, result) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/NostrConnectEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/NostrConnectEvent.kt new file mode 100644 index 000000000..a65e67a3b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/nip46/NostrConnectEvent.kt @@ -0,0 +1,108 @@ +/** + * 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.events.nip46 + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.HexValidator +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +class NostrConnectEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient private var decryptedContent: Map = mapOf() + + override fun countMemory(): Long = + super.countMemory() + + pointerSizeInBytes + (decryptedContent.values.sumOf { pointerSizeInBytes + it.countMemory() }) + + override fun isContentEncoded() = true + + private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + + fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull() + + fun verifiedRecipientPubKey(): HexKey? { + val recipient = recipientPubKey() + return if (HexValidator.isHex(recipient)) { + recipient + } else { + null + } + } + + fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey + + fun plainContent( + signer: NostrSigner, + onReady: (BunkerMessage) -> Unit, + ) { + decryptedContent[signer.pubKey]?.let { + onReady(it) + return + } + + // decrypts using NIP-04 or NIP-44 + signer.decrypt(content, talkingWith(signer.pubKey)) { retVal -> + val content = Event.mapper.readValue(retVal, BunkerMessage::class.java) + + decryptedContent = decryptedContent + Pair(signer.pubKey, content) + + onReady(content) + } + } + + companion object { + const val KIND = 24133 + const val ALT = "Nostr Connect Event" + + fun create( + message: BunkerMessage, + remoteKey: HexKey, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (NostrConnectEvent) -> Unit, + ) { + val tags = + arrayOf( + arrayOf("alt", ALT), + arrayOf("p", remoteKey), + ) + signer.sign(createdAt, KIND, tags, "", onReady) + + val encrypted = mapper.writeValueAsString(message) + + signer.nip44Encrypt(encrypted, remoteKey) { content -> + signer.sign(createdAt, KIND, tags, content, onReady) + } + } + } +}