diff --git a/README.md b/README.md index a8eb5c59b..c6d9da94b 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,12 @@ Or get the latest APK from the [Releases Section](https://github.com/vitorpamplo - [x] External Identity Support (NIP-39) - [x] Multiple Accounts - [x] Markdown Support +- [x] Relay Authentication (NIP-42) - [ ] Local Database - [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post - [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51) - [ ] Sensitive Content (NIP-36) - [ ] Relay Pages (NIP-11) -- [ ] Relay Authentication (NIP-42) - [ ] Generic Tags (NIP-12) - [ ] Proof of Work in the Phone (NIP-13, NIP-20) - [ ] Events with a Subject (NIP-14) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c81f0c970..e09112dae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -569,6 +569,12 @@ class Account( LocalCache.consume(event) } + fun createAuthEvent(relay: Relay, challenge: String): RelayAuthEvent? { + if (!isWriteable()) return null + + return RelayAuthEvent.create(relay.url, challenge, loggedIn.privKey!!) + } + fun removePublicBookmark(note: Note) { if (!isWriteable()) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index a3546f003..3025a742f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -14,8 +14,10 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES +import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.JsonFilter +import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.TypedFilter object NostrAccountDataSource : NostrDataSource("AccountData") { @@ -113,4 +115,19 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { createAccountBookmarkListFilter() ).ifEmpty { null } } + + override fun auth(relay: Relay, challenge: String) { + super.auth(relay, challenge) + + if (this::account.isInitialized) { + val event = account.createAuthEvent(relay, challenge) + + if (event != null) { + Client.send( + event, + relay.url + ) + } + } + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 206a6538a..bdabd984f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -126,6 +126,10 @@ abstract class NostrDataSource(val debugName: String) { override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) { } + + override fun onAuth(relay: Relay, challenge: String) { + auth(relay, challenge) + } } init { @@ -221,4 +225,5 @@ abstract class NostrDataSource(val debugName: String) { } abstract fun updateChannelFilters() + open fun auth(relay: Relay, challenge: String) = Unit } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt new file mode 100644 index 000000000..6a7e04228 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt @@ -0,0 +1,34 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +class RelayAuthEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun relay() = tags.firstOrNull() { it.size > 1 && it[0] == "relay" }?.get(1) + fun challenge() = tags.firstOrNull() { it.size > 1 && it[0] == "challenge" }?.get(1) + + companion object { + const val kind = 22242 + + fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RelayAuthEvent { + val content = "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf( + listOf("relay", relay), + listOf("challenge", challenge) + ) + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return RelayAuthEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index f5631b27e..256e1b804 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -160,6 +160,14 @@ object Client : RelayPool.Listener { } } + override fun onAuth(relay: Relay, challenge: String) { + // Releases the Web thread for the new payload. + // May need to add a processing queue if processing new events become too costly. + GlobalScope.launch(Dispatchers.Default) { + listeners.forEach { it.onAuth(relay, challenge) } + } + } + fun subscribe(listener: Listener) { listeners = listeners.plus(listener) } @@ -196,5 +204,7 @@ object Client : RelayPool.Listener { * When an relay saves or rejects a new event. */ open fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) = Unit + + open fun onAuth(relay: Relay, challenge: String) = Unit } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 21928f64a..68137cd57 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -122,6 +122,10 @@ class Relay( // Log.w("Relay", "Relay on OK $url, $channel") it.onSendResponse(this@Relay, msg[1].asString, msg[2].asBoolean, msg[3].asString) } + "AUTH" -> listeners.forEach { + // Log.w("Relay", "Relay AUTH $url, $channel") + it.onAuth(this@Relay, msg[1].asString) + } else -> listeners.forEach { // Log.w("Relay", "Relay something else $url, $channel") it.onError( @@ -272,6 +276,8 @@ class Relay( fun onSendResponse(relay: Relay, eventId: String, success: Boolean, message: String) + fun onAuth(relay: Relay, challenge: String) + /** * Connected to or disconnected from a relay * diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index e1d5bb215..702bdc1b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -93,6 +93,8 @@ object RelayPool : Relay.Listener { fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) + + fun onAuth(relay: Relay, challenge: String) } override fun onEvent(relay: Relay, subscriptionId: String, event: Event) { @@ -113,6 +115,10 @@ object RelayPool : Relay.Listener { listeners.forEach { it.onSendResponse(eventId, success, message, relay) } } + override fun onAuth(relay: Relay, challenge: String) { + listeners.forEach { it.onAuth(relay, challenge) } + } + // Observers line up here. val live: RelayPoolLiveData = RelayPoolLiveData(this)