Cryptographic support for NIP24
This commit is contained in:
@@ -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