mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-29 02:52:56 +02:00
Cryptographic support for NIP24
This commit is contained in:
@@ -194,6 +194,10 @@ dependencies {
|
|||||||
// GeoHash
|
// GeoHash
|
||||||
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
|
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
|
// Video compression lib
|
||||||
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
|
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
|
||||||
// Image compression lib
|
// Image compression lib
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1110,6 +1110,40 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
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) {
|
fun consume(event: LnZapPaymentRequestEvent) {
|
||||||
// Does nothing without a response callback.
|
// Does nothing without a response callback.
|
||||||
}
|
}
|
||||||
@@ -1337,10 +1371,12 @@ object LocalCache {
|
|||||||
is DeletionEvent -> consume(event)
|
is DeletionEvent -> consume(event)
|
||||||
is EmojiPackEvent -> consume(event)
|
is EmojiPackEvent -> consume(event)
|
||||||
is EmojiPackSelectionEvent -> consume(event)
|
is EmojiPackSelectionEvent -> consume(event)
|
||||||
|
is SealedGossipEvent -> consume(event, relay)
|
||||||
|
|
||||||
is FileHeaderEvent -> consume(event, relay)
|
is FileHeaderEvent -> consume(event, relay)
|
||||||
is FileStorageEvent -> consume(event, relay)
|
is FileStorageEvent -> consume(event, relay)
|
||||||
is FileStorageHeaderEvent -> consume(event, relay)
|
is FileStorageHeaderEvent -> consume(event, relay)
|
||||||
|
is GiftWrapEvent -> consume(event, relay)
|
||||||
is HighlightEvent -> consume(event, relay)
|
is HighlightEvent -> consume(event, relay)
|
||||||
is LiveActivitiesEvent -> consume(event, relay)
|
is LiveActivitiesEvent -> consume(event, relay)
|
||||||
is LiveActivitiesChatMessageEvent -> consume(event, relay)
|
is LiveActivitiesChatMessageEvent -> consume(event, relay)
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
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.Hex
|
||||||
import fr.acinq.secp256k1.Secp256k1
|
import fr.acinq.secp256k1.Secp256k1
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -58,6 +64,56 @@ object CryptoUtils {
|
|||||||
return String(cipher.doFinal(encryptedMsg))
|
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(
|
fun verifySignature(
|
||||||
signature: ByteArray,
|
signature: ByteArray,
|
||||||
hash: ByteArray,
|
hash: ByteArray,
|
||||||
@@ -85,3 +141,83 @@ fun Int.toByteArray(): ByteArray {
|
|||||||
}
|
}
|
||||||
return bytes
|
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
|
||||||
|
}
|
||||||
|
@@ -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<List<String>>,
|
||||||
|
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<String>? = null,
|
||||||
|
replyTos: List<String>? = null,
|
||||||
|
mentions: List<String>? = 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<List<String>>()
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -29,13 +29,19 @@ class EventFactory {
|
|||||||
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
DeletionEvent.kind -> DeletionEvent(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)
|
EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(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)
|
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
GenericRepostEvent.kind -> GenericRepostEvent(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)
|
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
@@ -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<List<String>>,
|
||||||
|
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<EncryptedInfo>(
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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<HexKey>,
|
||||||
|
from: ByteArray
|
||||||
|
): List<GiftWrapEvent> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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<List<String>>,
|
||||||
|
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<EncryptedInfo>(
|
||||||
|
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<List<String>>()
|
||||||
|
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<List<String>>,
|
||||||
|
val content: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(event: Event): Gossip {
|
||||||
|
return Gossip(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user