diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 11316a775..140a30849 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -4,6 +4,7 @@ import android.util.Log import androidx.lifecycle.LiveData import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.vitorpamplona.amethyst.service.NostrAccountDataSource.account import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.components.BundledInsert @@ -594,6 +595,16 @@ object LocalCache { fun consume(event: LnZapEvent) { val note = getOrCreateNote(event.id) + var decryptedContent = LnZapRequestEvent.checkForPrivateZap(event.zapRequest!!, account.loggedIn.privKey!!) + if (decryptedContent != null) { + Log.e( + "DC", + "Decrypted Content from Anon Tag: Sender: {${decryptedContent.pubKey}}, Message: {${decryptedContent.content}} " + + // TODO Update Notification with this Sender and Message + ) + } + // Already processed this event. if (note.event != null) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index 5f08c6a6e..92384c92b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -16,7 +16,7 @@ class LnZapEvent( // This event is also kept in LocalCache (same object) @Transient val zapRequest: LnZapRequestEvent? - private fun containedPost(): LnZapRequestEvent? = try { + override fun containedPost(): LnZapRequestEvent? = try { description()?.ifBlank { null }?.let { fromJson(it, Client.lenient) } as? LnZapRequestEvent @@ -53,8 +53,7 @@ class LnZapEvent( null } } - - override fun message(): String { + override fun content(): String { return content } @@ -68,7 +67,7 @@ class LnZapEvent( enum class ZapType() { PUBLIC, - PRIVATE, // not yet implemented + PRIVATE, ANONYMOUS, NONZAP } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt index ada0d609d..934dba55b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt @@ -16,5 +16,5 @@ interface LnZapEventInterface : EventInterface { fun amount(): BigDecimal? - fun message(): String + fun containedPost(): Event? } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index eba7a18a9..32583b549 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -1,9 +1,15 @@ package com.vitorpamplona.amethyst.service.model -import com.vitorpamplona.amethyst.model.HexKey -import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.model.* +import nostr.postr.Bech32 import nostr.postr.Utils -import java.util.Date +import java.nio.charset.Charset +import java.security.SecureRandom +import java.util.* +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec class LnZapRequestEvent( id: HexKey, @@ -30,7 +36,7 @@ class LnZapRequestEvent( zapType: LnZapEvent.ZapType, createdAt: Long = Date().time / 1000 ): LnZapRequestEvent { - val content = message + var content = message var privkey = privateKey var pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( @@ -48,6 +54,14 @@ class LnZapRequestEvent( tags = tags + listOf(listOf("anon", "")) privkey = Utils.privkeyCreate() pubKey = Utils.pubkeyCreate(privkey).toHexKey() + } else if (zapType == LnZapEvent.ZapType.PRIVATE) { + var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt) + var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson() + var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().toByteArray()) + tags = tags + listOf(listOf("anon", encryptedContent)) + content = "" // make sure public content is empty, as the content is encrypted + privkey = encryptionPrivateKey // sign event with generated privkey + pubKey = Utils.pubkeyCreate(encryptionPrivateKey).toHexKey() // updated event with according pubkey } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privkey) @@ -62,7 +76,7 @@ class LnZapRequestEvent( zapType: LnZapEvent.ZapType, createdAt: Long = Date().time / 1000 ): LnZapRequestEvent { - val content = message + var content = message var privkey = privateKey var pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( @@ -70,18 +84,90 @@ class LnZapRequestEvent( listOf("relays") + relays ) if (zapType == LnZapEvent.ZapType.ANONYMOUS) { - tags = tags + listOf(listOf("anon", "")) privkey = Utils.privkeyCreate() pubKey = Utils.pubkeyCreate(privkey).toHexKey() + tags = tags + listOf(listOf("anon", "")) + } else if (zapType == LnZapEvent.ZapType.PRIVATE) { + var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt) + var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson() + var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.toByteArray()) + tags = tags + listOf(listOf("anon", encryptedContent)) + content = "" + privkey = encryptionPrivateKey + pubKey = Utils.pubkeyCreate(encryptionPrivateKey).toHexKey() } - val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privkey) return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } + + fun createEncryptionPrivateKey(privkey: String, id: String, createdAt: Long): ByteArray { + var str = privkey + id + createdAt.toString() + var strbyte = str.toByteArray(Charset.forName("utf-8")) + return sha256.digest(strbyte) + } + + fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String { + var sharedSecret = Utils.getSharedSecret(privkey, pubkey) + val iv = ByteArray(16) + SecureRandom().nextBytes(iv) + + val keySpec = SecretKeySpec(sharedSecret, "AES") + val ivSpec = IvParameterSpec(iv) + + var utf8message = msg.toByteArray(Charset.forName("utf-8")) + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) + val encryptedMsg = cipher.doFinal(utf8message) + + val encryptedMsgBech32 = Bech32.encode("pzap", Bech32.eight2five(encryptedMsg), Bech32.Encoding.Bech32) + val ivBech32 = Bech32.encode("iv", Bech32.eight2five(iv), Bech32.Encoding.Bech32) + + return encryptedMsgBech32 + "_" + ivBech32 + } + + fun decryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String { + var sharedSecret = Utils.getSharedSecret(privkey, pubkey) + if (sharedSecret.size != 16 && sharedSecret.size != 32) { + throw IllegalArgumentException("Invalid shared secret size") + } + val parts = msg.split("_") + if (parts.size != 2) { + throw IllegalArgumentException("Invalid message format") + } + val iv = parts[1].run { Bech32.decode(this) } + val encryptedMsg = parts.first().run { Bech32.decode(this) } + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(Bech32.five2eight(iv.second, 0))) + + try { + val decryptedMsgBytes = cipher.doFinal(Bech32.five2eight(encryptedMsg.second, 0)) + return String(decryptedMsgBytes) + } catch (ex: BadPaddingException) { + throw IllegalArgumentException("Bad padding") + } + } + + fun checkForPrivateZap(zaprequest: Event, loggedInUserPrivKey: ByteArray): Event? { + val anonTag = zaprequest.tags.firstOrNull { t -> t.count() >= 2 && t[0] == "anon" } + if (anonTag != null && anonTag.size > 1) { + val encnote = anonTag?.elementAt(1) + if (encnote != null && encnote != "") { + try { + val note = decryptPrivateZapMessage(encnote, loggedInUserPrivKey, zaprequest.pubKey.toByteArray()) + val decryptedEvent = fromJson(note) + if (decryptedEvent.kind == 9733) { + return decryptedEvent + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + return null + } } } - /* { "pubkey": "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 5fe601a6a..6239a0abb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -67,6 +67,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc val zapTypes = listOf( Pair(LnZapEvent.ZapType.PUBLIC, "Public"), + Pair(LnZapEvent.ZapType.PRIVATE, "Private"), Pair(LnZapEvent.ZapType.ANONYMOUS, "Anonymous"), Pair(LnZapEvent.ZapType.NONZAP, "Non-Zap") ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index f7e1a24c0..cf9a85301 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -89,6 +89,8 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { if (zappedPost != null) { val zapRequest = zappedPost.zaps.filter { it.value == zapEvent }.keys.firstOrNull() if (zapRequest != null) { + // var newZapRequestEvent = LocalCache.checkPrivateZap(zapRequest.event as Event) + // zapRequest.event = newZapRequestEvent zapsPerEvent.getOrPut(zappedPost, { mutableMapOf() }).put(zapRequest, zapEvent) } } else {