diff --git a/app/build.gradle b/app/build.gradle index 4e9d8a0e5..609a10005 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,6 +194,10 @@ dependencies { // GeoHash implementation 'com.github.drfonfon:android-kotlin-geohash:1.0' + // LibSodium for XChaCha encryption + implementation "com.goterl:lazysodium-android:5.1.0@aar" + implementation "net.java.dev.jna:jna:5.12.1@aar" + // Video compression lib implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1' // Image compression lib diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/CryptoUtilsTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/CryptoUtilsTest.kt new file mode 100644 index 000000000..a69f33d3d --- /dev/null +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/CryptoUtilsTest.kt @@ -0,0 +1,83 @@ +package com.vitorpamplona.amethyst + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.CryptoUtils +import com.vitorpamplona.amethyst.service.KeyPair +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CryptoUtilsTest { + @Test() + fun testSharedSecret() { + val sender = KeyPair() + val receiver = KeyPair() + + val sharedSecret1 = CryptoUtils.getSharedSecret(sender.privKey!!, receiver.pubKey) + val sharedSecret2 = CryptoUtils.getSharedSecret(receiver.privKey!!, sender.pubKey) + + assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey()) + + val secretKey1 = KeyPair(privKey = sharedSecret1) + val secretKey2 = KeyPair(privKey = sharedSecret2) + + assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey()) + assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey()) + } + + @Test + fun encryptDecryptNIP4Test() { + val msg = "Hi" + + val privateKey = CryptoUtils.privkeyCreate() + val publicKey = CryptoUtils.pubkeyCreate(privateKey) + + val encrypted = CryptoUtils.encrypt(msg, privateKey, publicKey) + val decrypted = CryptoUtils.decrypt(encrypted, privateKey, publicKey) + + assertEquals(msg, decrypted) + } + + @Test + fun encryptDecryptNIP24Test() { + val msg = "Hi" + + val privateKey = CryptoUtils.privkeyCreate() + val publicKey = CryptoUtils.pubkeyCreate(privateKey) + + val encrypted = CryptoUtils.encryptXChaCha(msg, privateKey, publicKey) + val decrypted = CryptoUtils.decryptXChaCha(encrypted, privateKey, publicKey) + + assertEquals(msg, decrypted) + } + + @Test + fun encryptSharedSecretDecryptNIP4Test() { + val msg = "Hi" + + val privateKey = CryptoUtils.privkeyCreate() + val publicKey = CryptoUtils.pubkeyCreate(privateKey) + val sharedSecret = CryptoUtils.getSharedSecret(privateKey, publicKey) + + val encrypted = CryptoUtils.encrypt(msg, sharedSecret) + val decrypted = CryptoUtils.decrypt(encrypted, sharedSecret) + + assertEquals(msg, decrypted) + } + + @Test + fun encryptSharedSecretDecryptNIP24Test() { + val msg = "Hi" + + val privateKey = CryptoUtils.privkeyCreate() + val publicKey = CryptoUtils.pubkeyCreate(privateKey) + val sharedSecret = CryptoUtils.getSharedSecret(privateKey, publicKey) + + val encrypted = CryptoUtils.encryptXChaCha(msg, sharedSecret) + val decrypted = CryptoUtils.decryptXChaCha(encrypted, sharedSecret) + + assertEquals(msg, decrypted) + } +} diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/GiftWrapEventTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/GiftWrapEventTest.kt new file mode 100644 index 000000000..adda369e7 --- /dev/null +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/GiftWrapEventTest.kt @@ -0,0 +1,432 @@ +package com.vitorpamplona.amethyst + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.model.hexToByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.CryptoUtils +import com.vitorpamplona.amethyst.service.KeyPair +import com.vitorpamplona.amethyst.service.model.ChatMessageEvent +import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.GiftWrapEvent +import com.vitorpamplona.amethyst.service.model.NIP24Factory +import com.vitorpamplona.amethyst.service.model.SealedGossipEvent +import com.vitorpamplona.amethyst.service.relays.Client +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class GiftWrapEventTest { + @Test() + fun testNip24Utils() { + val sender = KeyPair() + val receiver = KeyPair() + val message = "Hola, que tal?" + + val events = NIP24Factory().createMsgNIP24( + message, + listOf(receiver.pubKey.toHexKey()), + sender.privKey!! + ) + + // Simulate Receiver + val eventsReceiverGets = events.filter { it.isTaggedUser(receiver.pubKey.toHexKey()) } + eventsReceiverGets.forEach { + val event = it.unwrap(receiver.privKey!!) + if (event is SealedGossipEvent) { + val innerData = event.unseal(receiver.privKey!!) + assertEquals(message, innerData?.content) + } else { + fail("Wrong Event") + } + } + + // Simulate Sender + val eventsSenderGets = events.filter { it.isTaggedUser(sender.pubKey.toHexKey()) } + eventsSenderGets.forEach { + val event = it.unwrap(sender.privKey!!) + if (event is SealedGossipEvent) { + val innerData = event.unseal(sender.privKey!!) + assertEquals(message, innerData?.content) + } else { + fail("Wrong Event") + } + } + } + + @Test() + fun testNip24UtilsForGroups() { + val sender = KeyPair() + println("AAAA - ${sender.privKey?.toHexKey()}") + val receiver1 = KeyPair() + val receiver2 = KeyPair() + val receiver3 = KeyPair() + val receiver4 = KeyPair() + val message = "Hola, que tal?" + + val receivers = listOf( + receiver1, + receiver2, + receiver3, + receiver4 + ) + + val events = NIP24Factory().createMsgNIP24( + message, + receivers.map { it.pubKey.toHexKey() }, + sender.privKey!! + ) + + // Simulate Receiver + receivers.forEach { receiver -> + val eventsReceiverGets = events.filter { it.isTaggedUser(receiver.pubKey.toHexKey()) } + eventsReceiverGets.forEach { + val event = it.unwrap(receiver.privKey!!) + if (event is SealedGossipEvent) { + val innerData = event.unseal(receiver.privKey!!) + assertEquals(message, innerData?.content) + } else { + fail("Wrong Event") + } + } + } + + // Simulate Sender + val eventsSenderGets = events.filter { it.isTaggedUser(sender.pubKey.toHexKey()) } + eventsSenderGets.forEach { + val event = it.unwrap(sender.privKey!!) + if (event is SealedGossipEvent) { + val innerData = event.unseal(sender.privKey!!) + assertEquals(message, innerData?.content) + } else { + fail("Wrong Event") + } + } + } + + @Test() + fun testInternalsSimpleMessage() { + val sender = KeyPair() + val receiver = KeyPair() + + val senderMessage = ChatMessageEvent.create( + msg = "Hi There!", + to = listOf(receiver.pubKey.toHexKey()), + privateKey = sender.privKey!! + ) + + // MsgFor the Receiver + + val encMsgFromSenderToReceiver = SealedGossipEvent.create( + event = senderMessage, + encryptTo = receiver.pubKey.toHexKey(), + privateKey = sender.privKey!! + ) + + // Should expose sender + assertEquals(encMsgFromSenderToReceiver.pubKey, sender.pubKey.toHexKey()) + // Should not expose receiver + assertTrue(encMsgFromSenderToReceiver.tags.isEmpty()) + + val giftWrapEventToReceiver = GiftWrapEvent.create( + event = encMsgFromSenderToReceiver, + recipientPubKey = receiver.pubKey.toHexKey() + ) + + // Should not be signed by neither sender nor receiver + assertNotEquals(giftWrapEventToReceiver.pubKey, sender.pubKey.toHexKey()) + assertNotEquals(giftWrapEventToReceiver.pubKey, receiver.pubKey.toHexKey()) + + // Should not include sender as recipient + assertNotEquals(giftWrapEventToReceiver.recipientPubKey(), sender.pubKey.toHexKey()) + + // Should be addressed to the receiver + assertEquals(giftWrapEventToReceiver.recipientPubKey(), receiver.pubKey.toHexKey()) + + // MsgFor the Sender + + val encMsgFromSenderToSender = SealedGossipEvent.create( + event = senderMessage, + encryptTo = sender.pubKey.toHexKey(), + privateKey = sender.privKey!! + ) + + // Should expose sender + assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey.toHexKey()) + // Should not expose receiver + assertTrue(encMsgFromSenderToSender.tags.isEmpty()) + + val giftWrapEventToSender = GiftWrapEvent.create( + event = encMsgFromSenderToSender, + recipientPubKey = sender.pubKey.toHexKey() + ) + + // Should not be signed by neither the sender, not the receiver + assertNotEquals(giftWrapEventToSender.pubKey, sender.pubKey.toHexKey()) + assertNotEquals(giftWrapEventToSender.pubKey, receiver.pubKey.toHexKey()) + + // Should not be addressed to the receiver + assertNotEquals(giftWrapEventToSender.recipientPubKey(), receiver.pubKey.toHexKey()) + // Should be addressed to the sender + assertEquals(giftWrapEventToSender.recipientPubKey(), sender.pubKey.toHexKey()) + + // Done + + println(senderMessage.toJson()) + println(encMsgFromSenderToReceiver.toJson()) + println(giftWrapEventToReceiver.toJson()) + println(giftWrapEventToSender.toJson()) + + // Receiver's side + // Unwrapping + + val unwrappedMsgForSenderBySender = giftWrapEventToSender.unwrap(sender.privKey!!) + val unwrappedMsgForReceiverBySender = giftWrapEventToReceiver.unwrap(sender.privKey!!) + + assertNotNull(unwrappedMsgForSenderBySender) + assertNull(unwrappedMsgForReceiverBySender) + + val unwrappedMsgForSenderByReceiver = giftWrapEventToSender.unwrap(receiver.privKey!!) + val unwrappedMsgForReceiverByReceiver = giftWrapEventToReceiver.unwrap(receiver.privKey!!) + + assertNull(unwrappedMsgForSenderByReceiver) + assertNotNull(unwrappedMsgForReceiverByReceiver) + + assertEquals(SealedGossipEvent.kind, unwrappedMsgForSenderBySender?.kind) + assertEquals(SealedGossipEvent.kind, unwrappedMsgForReceiverByReceiver?.kind) + + assertTrue(unwrappedMsgForSenderBySender is SealedGossipEvent) + assertTrue(unwrappedMsgForReceiverByReceiver is SealedGossipEvent) + + if (unwrappedMsgForSenderBySender is SealedGossipEvent && + unwrappedMsgForReceiverByReceiver is SealedGossipEvent + ) { + val unwrappedGossipToSenderByReceiver = unwrappedMsgForSenderBySender.unseal(receiver.privKey!!) + val unwrappedGossipToReceiverByReceiver = unwrappedMsgForReceiverByReceiver.unseal(receiver.privKey!!) + + assertNull(unwrappedGossipToSenderByReceiver) + assertNotNull(unwrappedGossipToReceiverByReceiver) + + val unwrappedGossipToSenderBySender = unwrappedMsgForSenderBySender.unseal(sender.privKey!!) + val unwrappedGossipToReceiverBySender = unwrappedMsgForReceiverByReceiver.unseal(sender.privKey!!) + + assertNotNull(unwrappedGossipToSenderBySender) + assertNull(unwrappedGossipToReceiverBySender) + + assertEquals("Hi There!", unwrappedGossipToReceiverByReceiver?.content) + assertEquals("Hi There!", unwrappedGossipToSenderBySender?.content) + } + } + + @Test() + fun testInternalsGroupMessage() { + val sender = KeyPair() + val receiverA = KeyPair() + val receiverB = KeyPair() + + val senderMessage = ChatMessageEvent.create( + msg = "Hi There!", + to = listOf(receiverA.pubKey.toHexKey(), receiverB.pubKey.toHexKey()), + privateKey = sender.privKey!! + ) + + val encMsgFromSenderToReceiverA = SealedGossipEvent.create( + event = senderMessage, + encryptTo = receiverA.pubKey.toHexKey(), + privateKey = sender.privKey!! + ) + + val encMsgFromSenderToReceiverB = SealedGossipEvent.create( + event = senderMessage, + encryptTo = receiverB.pubKey.toHexKey(), + privateKey = sender.privKey!! + ) + + val encMsgFromSenderToSender = SealedGossipEvent.create( + event = senderMessage, + encryptTo = sender.pubKey.toHexKey(), + privateKey = sender.privKey!! + ) + + // Should expose sender + assertEquals(encMsgFromSenderToReceiverA.pubKey, sender.pubKey.toHexKey()) + // Should not expose receiver + assertTrue(encMsgFromSenderToReceiverA.tags.isEmpty()) + + // Should expose sender + assertEquals(encMsgFromSenderToReceiverB.pubKey, sender.pubKey.toHexKey()) + // Should not expose receiver + assertTrue(encMsgFromSenderToReceiverB.tags.isEmpty()) + + // Should expose sender + assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey.toHexKey()) + // Should not expose receiver + assertTrue(encMsgFromSenderToSender.tags.isEmpty()) + + val giftWrapEventForReceiverA = GiftWrapEvent.create( + event = encMsgFromSenderToReceiverA, + recipientPubKey = receiverA.pubKey.toHexKey() + ) + + val giftWrapEventForReceiverB = GiftWrapEvent.create( + event = encMsgFromSenderToReceiverB, + recipientPubKey = receiverB.pubKey.toHexKey() + ) + + // Should not be signed by neither sender nor receiver + assertNotEquals(giftWrapEventForReceiverA.pubKey, sender.pubKey.toHexKey()) + assertNotEquals(giftWrapEventForReceiverA.pubKey, receiverA.pubKey.toHexKey()) + assertNotEquals(giftWrapEventForReceiverA.pubKey, receiverB.pubKey.toHexKey()) + + // Should not include sender as recipient + assertNotEquals(giftWrapEventForReceiverA.recipientPubKey(), sender.pubKey.toHexKey()) + + // Should be addressed to the receiver + assertEquals(giftWrapEventForReceiverA.recipientPubKey(), receiverA.pubKey.toHexKey()) + + // Should not be signed by neither sender nor receiver + assertNotEquals(giftWrapEventForReceiverB.pubKey, sender.pubKey.toHexKey()) + assertNotEquals(giftWrapEventForReceiverB.pubKey, receiverA.pubKey.toHexKey()) + assertNotEquals(giftWrapEventForReceiverB.pubKey, receiverB.pubKey.toHexKey()) + + // Should not include sender as recipient + assertNotEquals(giftWrapEventForReceiverB.recipientPubKey(), sender.pubKey.toHexKey()) + + // Should be addressed to the receiver + assertEquals(giftWrapEventForReceiverB.recipientPubKey(), receiverB.pubKey.toHexKey()) + + val giftWrapEventToSender = GiftWrapEvent.create( + event = encMsgFromSenderToSender, + recipientPubKey = sender.pubKey.toHexKey() + ) + + // Should not be signed by neither the sender, not the receiver + assertNotEquals(giftWrapEventToSender.pubKey, sender.pubKey.toHexKey()) + assertNotEquals(giftWrapEventToSender.pubKey, receiverA.pubKey.toHexKey()) + assertNotEquals(giftWrapEventToSender.pubKey, receiverB.pubKey.toHexKey()) + + // Should not be addressed to the receiver + assertNotEquals(giftWrapEventToSender.recipientPubKey(), receiverA.pubKey.toHexKey()) + assertNotEquals(giftWrapEventToSender.recipientPubKey(), receiverB.pubKey.toHexKey()) + // Should be addressed to the sender + assertEquals(giftWrapEventToSender.recipientPubKey(), sender.pubKey.toHexKey()) + + println(senderMessage.toJson()) + println(encMsgFromSenderToReceiverA.toJson()) + println(encMsgFromSenderToReceiverB.toJson()) + println(giftWrapEventForReceiverA.toJson()) + println(giftWrapEventForReceiverB.toJson()) + println(giftWrapEventToSender.toJson()) + + val unwrappedMsgForSenderBySender = giftWrapEventToSender.unwrap(sender.privKey!!) + val unwrappedMsgForReceiverBySenderA = giftWrapEventForReceiverA.unwrap(sender.privKey!!) + val unwrappedMsgForReceiverBySenderB = giftWrapEventForReceiverB.unwrap(sender.privKey!!) + + assertNotNull(unwrappedMsgForSenderBySender) + assertNull(unwrappedMsgForReceiverBySenderA) + assertNull(unwrappedMsgForReceiverBySenderB) + + val unwrappedMsgForSenderByReceiverA = giftWrapEventToSender.unwrap(receiverA.privKey!!) + val unwrappedMsgForReceiverAByReceiverA = giftWrapEventForReceiverA.unwrap(receiverA.privKey!!) + val unwrappedMsgForReceiverBByReceiverA = giftWrapEventForReceiverB.unwrap(receiverA.privKey!!) + + assertNull(unwrappedMsgForSenderByReceiverA) + assertNotNull(unwrappedMsgForReceiverAByReceiverA) + assertNull(unwrappedMsgForReceiverBByReceiverA) + + val unwrappedMsgForSenderByReceiverB = giftWrapEventToSender.unwrap(receiverB.privKey!!) + val unwrappedMsgForReceiverAByReceiverB = giftWrapEventForReceiverA.unwrap(receiverB.privKey!!) + val unwrappedMsgForReceiverBByReceiverB = giftWrapEventForReceiverB.unwrap(receiverB.privKey!!) + + assertNull(unwrappedMsgForSenderByReceiverB) + assertNull(unwrappedMsgForReceiverAByReceiverB) + assertNotNull(unwrappedMsgForReceiverBByReceiverB) + + assertEquals(SealedGossipEvent.kind, unwrappedMsgForSenderBySender?.kind) + assertEquals(SealedGossipEvent.kind, unwrappedMsgForReceiverAByReceiverA?.kind) + assertEquals(SealedGossipEvent.kind, unwrappedMsgForReceiverBByReceiverB?.kind) + + assertTrue(unwrappedMsgForSenderBySender is SealedGossipEvent) + assertTrue(unwrappedMsgForReceiverAByReceiverA is SealedGossipEvent) + assertTrue(unwrappedMsgForReceiverBByReceiverB is SealedGossipEvent) + + if (unwrappedMsgForSenderBySender is SealedGossipEvent && + unwrappedMsgForReceiverAByReceiverA is SealedGossipEvent && + unwrappedMsgForReceiverBByReceiverB is SealedGossipEvent + ) { + val unwrappedGossipToSenderByReceiverA = unwrappedMsgForSenderBySender.unseal(receiverA.privKey!!) + val unwrappedGossipToReceiverAByReceiverA = unwrappedMsgForReceiverAByReceiverA.unseal(receiverA.privKey!!) + val unwrappedGossipToReceiverBByReceiverA = unwrappedMsgForReceiverBByReceiverB.unseal(receiverA.privKey!!) + + assertNull(unwrappedGossipToSenderByReceiverA) + assertNotNull(unwrappedGossipToReceiverAByReceiverA) + assertNull(unwrappedGossipToReceiverBByReceiverA) + + val unwrappedGossipToSenderByReceiverB = unwrappedMsgForSenderBySender.unseal(receiverB.privKey!!) + val unwrappedGossipToReceiverAByReceiverB = unwrappedMsgForReceiverAByReceiverA.unseal(receiverB.privKey!!) + val unwrappedGossipToReceiverBByReceiverB = unwrappedMsgForReceiverBByReceiverB.unseal(receiverB.privKey!!) + + assertNull(unwrappedGossipToSenderByReceiverB) + assertNull(unwrappedGossipToReceiverAByReceiverB) + assertNotNull(unwrappedGossipToReceiverBByReceiverB) + + val unwrappedGossipToSenderBySender = unwrappedMsgForSenderBySender.unseal(sender.privKey!!) + val unwrappedGossipToReceiverABySender = unwrappedMsgForReceiverAByReceiverA.unseal(sender.privKey!!) + val unwrappedGossipToReceiverBBySender = unwrappedMsgForReceiverBByReceiverB.unseal(sender.privKey!!) + + assertNotNull(unwrappedGossipToSenderBySender) + assertNull(unwrappedGossipToReceiverABySender) + assertNull(unwrappedGossipToReceiverBBySender) + + assertEquals("Hi There!", unwrappedGossipToReceiverAByReceiverA?.content) + assertEquals("Hi There!", unwrappedGossipToReceiverBByReceiverB?.content) + assertEquals("Hi There!", unwrappedGossipToSenderBySender?.content) + } + } + + @Test + fun testDecryptFromCoracle() { + val json = """ +{ + "content": "{\"ciphertext\":\"fo0/Ywyfu86cXKoOOeFFlMRv+LYM6GJUL+F/J4ARv6EcAufOZP46vimlurPBLPjNgzuGemGjlTFfC3vtq84AqIsqFo3dqKunq8Vp+mmubvxIQUDzOGYvM0WE/XOiW5LEe3U3Vq399Dq07xRpXELcp4EZxGyu4Fowv2Ppb4SKpH8g+9N3z2+bwYcSxBvI6SrL+hgmVMrRlgvsUHN1d53ni9ehRseBqrLj/DqyyEiygsKm6vnEZAPKnl1MrBaVOBZmGsyjAa/G4SBVVmk78sW7xWWvo4cV+C22FluDWUOdj/bYabH4aR4scnBX3GLYqfLuzGnuQlRNsb5unXVX41+39uXzROrmNP6iYVyYxy5tfoyN7PPZ4osoKpLDUGldmXHD6RjMcAFuou4hXt2JlTPmXpj/x8qInXId5mkmU4nTGiasvsCIpJljbCujwCjbjLTcD4QrjuhMdtSsAzjT0CDv5Lmc632eKRYtDu/9B+lkqBBkp7amhzbqp8suNTnybkvbGFQQGEQnsLfNJw/GGopAuthfi8zkTgUZR/LxFR7ZKAX73G+5PQSDSjPuGH/dQEnsFo45zsh1Xro8SfUQBsPphbX2GS31Lwu5vA30O922T4UiWuU+EdNgZR0JankQ5NPgvr1uS56C3v84VwdrNWQUCwC4eYJl4Mb/OdpEy9qwsisisppq6uuzxmxd1qx3JfocnGsvB7h2g2sG+0lyZADDSobOEZEKHaBP3w+dRcJW9D95EmzPym9GO0n+33OfqFQbda7G0rzUWfPDV0gXIuZcKs/HmDqepgIZN8FG7JhRBeAv0bCbKQACre0c8tzVEn5yCYemltScdKop3pC/r6gH50jRhAlFAiIKx8R+XwuMmJRqOcH4WfkpZlfVU85/I0XJOCHWKk6BnJi/NPP9zYiZiJe+5LecqMUVjtO0YAlv138+U/3FIT/anQ4H5bjVWBZmajwf\",\"nonce\":\"Mv70S6jgrs4D1rlqV9b5DddiymGAcVVe\",\"v\":1}", + "created_at": 1690528373, + "id": "6b108e4236c3c338236ee589388ce0f91f921e1532ae52e75d1d2add6f8e691a", + "kind": 1059, + "pubkey": "627dc0248335e2bf9adac14be9494139ebbeb12c422d7df5b0e3cd72d04c209c", + "sig": "be11da487196db298e4ffb7a03e74176c37441c88b39c95b518fadce6fd02f23c58b2c435ca38c24d512713935ab719dae80bf952267630809e1f84be8e95174", + "tags": [ + [ + "p", + "e7764a227c12ac1ef2db79ae180392c90903b2cec1e37f5c1a4afed38117185e" + ] + ], + "seenOn": [ + "wss://relay.damus.io/" + ] +} + """.trimIndent() + + val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771".hexToByteArray() + val wrap = Event.fromJson(json, Client.lenient) as GiftWrapEvent + + wrap.checkSignature() + + assertEquals(CryptoUtils.pubkeyCreate(privateKey).toHexKey(), wrap.recipientPubKey()) + + val event = wrap.unwrap(privateKey) + assertNotNull(event) + + if (event is SealedGossipEvent) { + val innerData = event.unseal(privateKey) + assertEquals("Hi", innerData?.content) + } else { + println(event?.toJson()) + fail() + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 39e197154..7e4d04904 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1110,6 +1110,40 @@ object LocalCache { refreshObservers(note) } + private fun consume(event: SealedGossipEvent, relay: Relay?) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + + private fun consume(event: GiftWrapEvent, relay: Relay?) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + fun consume(event: LnZapPaymentRequestEvent) { // Does nothing without a response callback. } @@ -1337,10 +1371,12 @@ object LocalCache { is DeletionEvent -> consume(event) is EmojiPackEvent -> consume(event) is EmojiPackSelectionEvent -> consume(event) + is SealedGossipEvent -> consume(event, relay) is FileHeaderEvent -> consume(event, relay) is FileStorageEvent -> consume(event, relay) is FileStorageHeaderEvent -> consume(event, relay) + is GiftWrapEvent -> consume(event, relay) is HighlightEvent -> consume(event, relay) is LiveActivitiesEvent -> consume(event, relay) is LiveActivitiesChatMessageEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CryptoUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CryptoUtils.kt index 1a6cb3971..cc6627067 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CryptoUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CryptoUtils.kt @@ -1,5 +1,11 @@ package com.vitorpamplona.amethyst.service +import com.goterl.lazysodium.LazySodium +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.Sodium +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.utils.Base64MessageEncoder +import com.goterl.lazysodium.utils.Key import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Secp256k1 import java.security.MessageDigest @@ -58,6 +64,56 @@ object CryptoUtils { return String(cipher.doFinal(encryptedMsg)) } + fun encryptXChaCha(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo { + val sharedSecret = getSharedSecret(privateKey, pubKey) + return encryptXChaCha(msg, sharedSecret) + } + + fun encryptXChaCha(msg: String, sharedSecret: ByteArray): EncryptedInfo { + val lazySodium = LazySodiumAndroid(SodiumAndroid(), Base64MessageEncoder()) + + val key = Key.fromBytes(sharedSecret) + + val nonce: ByteArray = lazySodium.nonce(24) + val messageBytes: ByteArray = msg.toByteArray() + + val cipher = lazySodium.cryptoStreamXChaCha20Xor( + messageBytes = messageBytes, + nonce = nonce, + key = key + ) + + val cipherBase64 = Base64.getEncoder().encodeToString(cipher) + val nonceBase64 = Base64.getEncoder().encodeToString(nonce) + + return EncryptedInfo( + ciphertext = cipherBase64, + nonce = nonceBase64, + v = Nip44Version.XChaCha20.versionCode + ) + } + + fun decryptXChaCha(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String { + val sharedSecret = getSharedSecret(privateKey, pubKey) + return decryptXChaCha(encryptedInfo, sharedSecret) + } + + fun decryptXChaCha(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String { + val lazySodium = LazySodiumAndroid(SodiumAndroid(), Base64MessageEncoder()) + + val key = Key.fromBytes(sharedSecret) + val nonceBytes = Base64.getDecoder().decode(encryptedInfo.nonce) + val messageBytes = Base64.getDecoder().decode(encryptedInfo.ciphertext) + + val cipher = lazySodium.cryptoStreamXChaCha20Xor( + messageBytes = messageBytes, + nonce = nonceBytes, + key = key + ) + + return cipher.decodeToString() + } + fun verifySignature( signature: ByteArray, hash: ByteArray, @@ -85,3 +141,83 @@ fun Int.toByteArray(): ByteArray { } return bytes } + +data class EncryptedInfo(val ciphertext: String, val nonce: String, val v: Int) + +enum class Nip44Version(val versionCode: Int) { + Reserved(0), + XChaCha20(1) +} + +fun Sodium.crypto_stream_xchacha20_xor_ic( + cipher: ByteArray, + message: ByteArray, + messageLen: Long, + nonce: ByteArray, + ic: Long, + key: ByteArray +): Int { + /** + * + * unsigned char k2[crypto_core_hchacha20_OUTPUTBYTES]; + + crypto_core_hchacha20(k2, n, k, NULL); + return crypto_stream_chacha20_xor_ic( + c, m, mlen, n + crypto_core_hchacha20_INPUTBYTES, ic, k2); + */ + + val k2 = ByteArray(32) + + val nonceChaCha = nonce.drop(16).toByteArray() + assert(nonceChaCha.size == 8) + + crypto_core_hchacha20(k2, nonce, key, null) + return crypto_stream_chacha20_xor_ic( + cipher, + message, + messageLen, + nonceChaCha, + ic, + k2 + ) +} + +fun Sodium.crypto_stream_xchacha20_xor( + cipher: ByteArray, + message: ByteArray, + messageLen: Long, + nonce: ByteArray, + key: ByteArray +): Int { + return crypto_stream_xchacha20_xor_ic(cipher, message, messageLen, nonce, 0, key) +} + +fun LazySodium.cryptoStreamXChaCha20Xor( + cipher: ByteArray, + message: ByteArray, + messageLen: Long, + nonce: ByteArray, + key: ByteArray +): Boolean { + require(!(messageLen < 0 || messageLen > message.size)) { "messageLen out of bounds: $messageLen" } + return successful( + getSodium().crypto_stream_xchacha20_xor( + cipher, + message, + messageLen, + nonce, + key + ) + ) +} + +private fun LazySodium.cryptoStreamXChaCha20Xor( + messageBytes: ByteArray, + nonce: ByteArray, + key: Key +): ByteArray { + val mLen = messageBytes.size + val cipher = ByteArray(mLen) + cryptoStreamXChaCha20Xor(cipher, messageBytes, mLen.toLong(), nonce, key.asBytes) + return cipher +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChatMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChatMessageEvent.kt new file mode 100644 index 000000000..236e23b78 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChatMessageEvent.kt @@ -0,0 +1,72 @@ +package com.vitorpamplona.amethyst.service.model + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.TimeUtils +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.CryptoUtils + +@Immutable +class ChatMessageEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + /** + * Recepients intended to receive this conversation + */ + private fun recipientsPubKey() = tags.mapNotNull { + if (it.size > 1 && it[0] == "p") it[1] else null + } + + fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + + companion object { + const val kind = 14 + + fun create( + msg: String, + to: List? = null, + replyTos: List? = null, + mentions: List? = null, + zapReceiver: String? = null, + markAsSensitive: Boolean = false, + zapRaiserAmount: Long? = null, + geohash: String? = null, + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): ChatMessageEvent { + val content = msg + val tags = mutableListOf>() + to?.forEach { + tags.add(listOf("p", it)) + } + replyTos?.forEach { + tags.add(listOf("e", it)) + } + mentions?.forEach { + tags.add(listOf("p", it, "", "mention")) + } + zapReceiver?.let { + tags.add(listOf("zap", it)) + } + if (markAsSensitive) { + tags.add(listOf("content-warning", "")) + } + zapRaiserAmount?.let { + tags.add(listOf("zapraiser", "$it")) + } + geohash?.let { + tags.add(listOf("g", it)) + } + + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, ClassifiedsEvent.kind, tags, content) + val sig = CryptoUtils.sign(id, privateKey) + return ChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt index 7fb0bbd36..42fa69c69 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt @@ -29,13 +29,19 @@ class EventFactory { CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) + + // Will never happen. + // DirectMessageEvent.kind -> DirectMessageEvent(createdAt, tags, content) + EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) + SealedGossipEvent.kind -> SealedGossipEvent(id, pubKey, createdAt, tags, content, sig) FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig) FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig) FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) + GiftWrapEvent.kind -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GiftWrapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GiftWrapEvent.kt new file mode 100644 index 000000000..8d22caf0b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GiftWrapEvent.kt @@ -0,0 +1,87 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.TimeUtils +import com.vitorpamplona.amethyst.model.hexToByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.CryptoUtils +import com.vitorpamplona.amethyst.service.EncryptedInfo +import com.vitorpamplona.amethyst.service.relays.Client + +@Immutable +class GiftWrapEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + private var innerEvent: Event? = null + + fun cachedInnerEvent(privKey: ByteArray): Event? { + if (innerEvent != null) return innerEvent + + val myInnerEvent = unwrap(privKey = privKey) + innerEvent = myInnerEvent + return myInnerEvent + } + + fun unwrap(privKey: ByteArray) = try { + plainContent(privKey)?.let { println("$it"); fromJson(it, Client.lenient) } + } catch (e: Exception) { + Log.e("UnwrapError", "Couldn't Decrypt the content", e) + null + } + + private fun plainContent(privKey: ByteArray): String? { + if (content.isBlank()) return null + + return try { + val sharedSecret = CryptoUtils.getSharedSecret(privKey, pubKey.hexToByteArray()) + + val toDecrypt = gson.fromJson( + content, + EncryptedInfo::class.java + ) + + println(toDecrypt.ciphertext) + println(toDecrypt.nonce) + println(toDecrypt.v) + + return CryptoUtils.decryptXChaCha(toDecrypt, sharedSecret) + } catch (e: Exception) { + Log.w("GeneralList", "Error decrypting the message ${e.message}") + null + } + } + + fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + + companion object { + const val kind = 1059 + + fun create( + event: Event, + recipientPubKey: HexKey, + createdAt: Long = TimeUtils.now() + ): GiftWrapEvent { + val privateKey = CryptoUtils.privkeyCreate() // GiftWrap is always a random key + val sharedSecret = CryptoUtils.getSharedSecret(privateKey, recipientPubKey.hexToByteArray()) + + val content = gson.toJson( + CryptoUtils.encryptXChaCha( + gson.toJson(event), + sharedSecret + ) + ) + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf(listOf("p", recipientPubKey)) + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = CryptoUtils.sign(id, privateKey) + return GiftWrapEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/NIP24Factory.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/NIP24Factory.kt new file mode 100644 index 000000000..32ffa0f25 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/NIP24Factory.kt @@ -0,0 +1,32 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.CryptoUtils + +class NIP24Factory { + fun createMsgNIP24( + msg: String, + to: List, + from: ByteArray + ): List { + val senderPublicKey = CryptoUtils.pubkeyCreate(from).toHexKey() + + val senderMessage = ChatMessageEvent.create( + msg = msg, + to = to, + privateKey = from + ) + + return to.plus(senderPublicKey).map { + GiftWrapEvent.create( + event = SealedGossipEvent.create( + event = senderMessage, + encryptTo = it, + privateKey = from + ), + recipientPubKey = it + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/SealedGossipEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/SealedGossipEvent.kt new file mode 100644 index 000000000..b7de10615 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/SealedGossipEvent.kt @@ -0,0 +1,104 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.TimeUtils +import com.vitorpamplona.amethyst.model.hexToByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.CryptoUtils +import com.vitorpamplona.amethyst.service.EncryptedInfo + +@Immutable +class SealedGossipEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + private var innerEvent: Gossip? = null + + fun cachedGossip(privKey: ByteArray): Gossip? { + if (innerEvent != null) return innerEvent + + val myInnerEvent = unseal(privKey = privKey) + innerEvent = myInnerEvent + return myInnerEvent + } + + fun unseal(privKey: ByteArray): Gossip? = try { + plainContent(privKey)?.let { gson.fromJson(it, Gossip::class.java) } + } catch (e: Exception) { + null + } + + private fun plainContent(privKey: ByteArray): String? { + if (content.isBlank()) return null + + return try { + val sharedSecret = CryptoUtils.getSharedSecret(privKey, pubKey.hexToByteArray()) + + val toDecrypt = gson.fromJson( + content, + EncryptedInfo::class.java + ) + + return CryptoUtils.decryptXChaCha(toDecrypt, sharedSecret) + } catch (e: Exception) { + Log.w("GeneralList", "Error decrypting the message ${e.message}") + null + } + } + + companion object { + const val kind = 13 + + fun create( + event: Event, + encryptTo: HexKey, + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): SealedGossipEvent { + val gossip = Gossip.create(event) + return create(gossip, encryptTo, privateKey, createdAt) + } + + fun create( + gossip: Gossip, + encryptTo: HexKey, + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): SealedGossipEvent { + val sharedSecret = CryptoUtils.getSharedSecret(privateKey, encryptTo.hexToByteArray()) + + val content = gson.toJson( + CryptoUtils.encryptXChaCha( + gson.toJson(gossip), + sharedSecret + ) + ) + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf>() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = CryptoUtils.sign(id, privateKey) + return SealedGossipEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} + +open class Gossip( + val id: HexKey, + val pubKey: HexKey, + val createdAt: Long, + val kind: Int, + val tags: List>, + val content: String +) { + companion object { + fun create(event: Event): Gossip { + return Gossip(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content) + } + } +}