mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-22 20:22:03 +02:00
Cryptographic support for NIP24
This commit is contained in:
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
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)
|
||||
|
@@ -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