Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,6 @@
<data android:host="direct_signup" />
<data android:host="signup_grant" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="pubkyring"
android:host="signup" />
</intent-filter>
</activity-alias>

<service
Expand Down
26 changes: 3 additions & 23 deletions app/src/main/java/to/bitkit/models/PubkyAuthRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import to.bitkit.repositories.MethodId
import to.bitkit.utils.AppError
import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

@Immutable
Expand Down Expand Up @@ -129,7 +128,6 @@ data class PubkyAuthRequest(
val bitkitClaim: PubkyAuthClaim?,
val homeserverPublicKey: String? = null,
val signupToken: String? = null,
val authorizationUrl: String? = rawUrl,
) {
val isSignup: Boolean
get() = isSignupUrl(rawUrl)
Expand All @@ -147,7 +145,6 @@ data class PubkyAuthRequest(
capabilities: String,
homeserverPublicKey: String? = null,
signupToken: String? = null,
authorizationUrl: String? = rawUrl,
): Result<PubkyAuthRequest> = parseBitkitClaim(rawUrl, capabilities).map { bitkitClaim ->
val permissions = parseCapabilities(capabilities)
PubkyAuthRequest(
Expand All @@ -160,7 +157,6 @@ data class PubkyAuthRequest(
bitkitClaim = bitkitClaim,
homeserverPublicKey = homeserverPublicKey,
signupToken = signupToken,
authorizationUrl = authorizationUrl,
)
}

Expand Down Expand Up @@ -208,25 +204,16 @@ data class PubkyAuthRequest(
val uri = URI(rawUrl)
require(uri.isSignupRequest() && !isGrantSignupUrl(rawUrl)) { "Unsupported Pubky signup URL" }
val query = parseQuery(uri)
require(!uri.authorizesApp(query)) { "Unsupported legacy Pubky signup authorization" }
val homeserver = query.requiredSingle("hs")
val authorizesApp = uri.authorizesApp(query)
val relay = if (authorizesApp) query.requiredSingle("relay") else ""
val secret = if (authorizesApp) query.requiredSingle("secret") else ""
val capabilities = if (authorizesApp) query.requiredSingle("caps") else ""
val authorizationUrl = if (authorizesApp) {
ringAuthorizationUrl(relay, secret, capabilities)
} else {
null
}

parse(
rawUrl = rawUrl,
clientId = "",
relay = relay,
capabilities = capabilities,
relay = "",
capabilities = "",
homeserverPublicKey = homeserver,
signupToken = query.optionalSingle("st"),
authorizationUrl = authorizationUrl,
).getOrThrow().also {
require(it.bitkitClaim == null) { "Pubky signup does not support Bitkit companion claims" }
}
Expand Down Expand Up @@ -314,13 +301,6 @@ data class PubkyAuthRequest(

private fun decodeQueryComponent(value: String) = URLDecoder.decode(value, StandardCharsets.UTF_8.name())

private fun ringAuthorizationUrl(relay: String, secret: String, capabilities: String): String =
"pubkyauth:///?relay=${encodeQueryComponent(relay)}" +
"&secret=${encodeQueryComponent(secret)}&caps=${encodeQueryComponent(capabilities)}"

private fun encodeQueryComponent(value: String) =
URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20")

private fun parseQuery(uri: URI): Map<String, List<String>> = uri.rawQuery.orEmpty()
.split("&")
.filter { it.isNotEmpty() }
Expand Down
13 changes: 3 additions & 10 deletions app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2069,10 +2069,7 @@ class PubkyRepo @Inject constructor(
withContext(ioDispatcher) {
if (PubkyAuthRequest.isSignupUrl(authUrl) && !PubkyAuthRequest.isGrantSignupUrl(authUrl)) {
val request = PubkyAuthRequest.parseSignup(authUrl).getOrThrow()
pubkyService.validateSignupRequest(
authorizationUrl = request.authorizationUrl,
homeserverPublicKey = requireNotNull(request.homeserverPublicKey),
)
pubkyService.validateSignupRequest(requireNotNull(request.homeserverPublicKey))
return@withContext request
}

Expand Down Expand Up @@ -2112,12 +2109,8 @@ class PubkyRepo @Inject constructor(
homeserverZ32 = requireNotNull(request.homeserverPublicKey),
signupCode = request.signupToken,
)
request.authorizationUrl?.let {
if (request.isGrantSignup) {
pubkyService.approveAuth(it, request.capabilities, request.clientId, secretKeyHex)
} else {
pubkyService.approveRingAuth(it, secretKeyHex)
}
if (request.isGrantSignup) {
pubkyService.approveAuth(request.rawUrl, request.capabilities, request.clientId, secretKeyHex)
}
var activated = false
try {
Expand Down
26 changes: 4 additions & 22 deletions app/src/main/java/to/bitkit/services/PubkyService.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package to.bitkit.services

import com.synonym.bitkitcore.approvePubkyAuth
import com.synonym.paykit.ContactRecord
import com.synonym.paykit.ContactUpdate
import com.synonym.paykit.PaykitProfile
Expand All @@ -13,15 +12,13 @@ import com.synonym.paykit.PubkyProfileUpdate
import com.synonym.paykit.PubkySessionBootstrapResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.withTimeoutOrNull
import to.bitkit.async.ServiceQueue
import to.bitkit.ext.runSuspendCatching
import to.bitkit.utils.AppError
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import com.synonym.bitkitcore.parsePubkyAuthUrl as parseLegacyPubkyAuthUrl

@Singleton
class PubkyService @Inject constructor(
Expand Down Expand Up @@ -130,12 +127,10 @@ class PubkyService @Inject constructor(
PaykitSdkService.parseAuthUrl(url)
}

suspend fun validateSignupRequest(authorizationUrl: String?, homeserverPublicKey: String): Unit =
ServiceQueue.PUBKY.background {
authorizationUrl?.let { parseLegacyPubkyAuthUrl(it) }
PaykitPublicKeys.normalize(homeserverPublicKey)
Unit
}
suspend fun validateSignupRequest(homeserverPublicKey: String): Unit = ServiceQueue.PUBKY.background {
PaykitPublicKeys.normalize(homeserverPublicKey)
Unit
}

suspend fun approveAuth(
authUrl: String,
Expand All @@ -147,17 +142,6 @@ class PubkyService @Inject constructor(
paykitSdkService.approveAuth(authUrl, expectedCapabilities, approvedClientId, secretKeyHex)
}

suspend fun approveRingAuth(
authUrl: String,
secretKeyHex: String,
timeout: Duration = AUTHORIZATION_TIMEOUT,
) = ServiceQueue.PUBKY.background {
paykitSdkService.republishIdentityIfNeeded(publicKeyFromSecret(secretKeyHex))
withTimeoutOrNull(timeout) {
approvePubkyAuth(authUrl, secretKeyHex)
} ?: throw PubkyRingAuthTimeoutError()
}

suspend fun approveAuthWithCompanionClaim(
authUrl: String,
expectedCapabilities: String,
Expand Down Expand Up @@ -294,5 +278,3 @@ class PubkyService @Inject constructor(
private suspend fun <T> cancellablePublicRead(block: suspend CoroutineScope.() -> T): T =
ServiceQueue.PUBKY.background(ServiceQueue.PUBKY.queueContext.minusKey(Job), block)
}

class PubkyRingAuthTimeoutError : AppError("Ring authorization timed out")
2 changes: 0 additions & 2 deletions app/src/main/java/to/bitkit/ui/utils/PubkyAuthErrorMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import to.bitkit.R
import to.bitkit.ext.isPaykitSignupCodeRejected
import to.bitkit.models.PubkyAuthRequestError
import to.bitkit.repositories.WatchOnlyAccountError
import to.bitkit.services.PubkyRingAuthTimeoutError

fun Throwable.localizedPubkyAuthMessage(context: Context): String? {
var current: Throwable? = this
Expand All @@ -21,7 +20,6 @@ fun Throwable.localizedPubkyAuthMessage(context: Context): String? {
}

private fun Throwable.pubkyAuthMessageResource() = when (this) {
is PubkyRingAuthTimeoutError -> R.string.profile__auth_error_timeout
is PubkyAuthRequestError.InvalidUrl -> R.string.profile__auth_error_invalid_url
PubkyAuthRequestError.InvalidPaymentDetails -> R.string.profile__auth_approval_usdt_unavailable
PubkyAuthRequestError.RequesterChanged -> R.string.profile__auth_error_invalid_url
Expand Down
2 changes: 1 addition & 1 deletion app/src/test/java/to/bitkit/build/PubkyAuthManifestTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ class PubkyAuthManifestTest {
val authAlias = ComponentName(application.packageName, "to.bitkit.ui.MainActivityPubkyAuth")
val signupAlias = ComponentName(application.packageName, "to.bitkit.ui.MainActivityPubkySignup")
val signupUrls = listOf(
"pubkyring://signup?hs=homeserver",
"pubkyauth://signup?hs=homeserver&relay=relay&secret=secret&caps=rw",
"pubkyauth://signup?hs=homeserver",
"pubkyauth://direct_signup?hs=homeserver",
)
val authUrls = listOf("pubkyauth://signin_grant?caps=rw", "pubkyauth://signup_grant?caps=rw")
val unrelatedUrls = listOf(
"pubkyring://signup?hs=homeserver&relay=relay&secret=secret&caps=rw",
"pubkyring://auth",
"pubkyring://direct_signup",
"pubkyauth://?caps=rw",
Expand Down
33 changes: 14 additions & 19 deletions app/src/test/java/to/bitkit/models/PubkyAuthRequestTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,17 @@ class PubkyAuthRequestTest {
}

@Test
fun `parse authorized signup preserves registration and authorization details`() {
listOf("pubkyring", "pubkyauth").forEach { scheme ->
val request = PubkyAuthRequest.parseSignup(ringSignupUrl("invite code", scheme)).getOrThrow()
fun `parse signup rejects legacy Ring app authorization`() {
val legacyUrls = listOf(
ringSignupUrl("invite code"),
ringSignupUrl("invite code", "pubkyauth"),
ringSignupUrl().replace("&secret=secret", ""),
"pubkyring://signup?hs=homeserver",
directSignupUrl("signup") + "&relay=https%3A%2F%2Frelay.example",
)

assertTrue(request.isSignup)
assertEquals("homeserver", request.homeserverPublicKey)
assertEquals("invite code", request.signupToken)
assertEquals("https://relay.example/inbox/", request.relay)
assertEquals("/pub/example.app/:rw", request.capabilities)
assertEquals(
"pubkyauth:///?relay=https%3A%2F%2Frelay.example%2Finbox%2F" +
"&secret=secret&caps=%2Fpub%2Fexample.app%2F%3Arw",
request.authorizationUrl,
)
legacyUrls.forEach { url ->
assertIs<PubkyAuthRequestError.InvalidUrl>(PubkyAuthRequest.parseSignup(url).exceptionOrNull(), url)
}
}

Expand All @@ -140,7 +137,6 @@ class PubkyAuthRequestTest {
assertEquals("invite code", request.signupToken)
assertEquals("", request.relay)
assertEquals("", request.capabilities)
assertNull(request.authorizationUrl)
}
}

Expand All @@ -162,7 +158,6 @@ class PubkyAuthRequestTest {
assertEquals("homeserver", request.homeserverPublicKey)
assertEquals("invite code", request.signupToken)
assertEquals("shop.pubky.app", request.clientId)
assertEquals(url, request.authorizationUrl)
assertEquals(listOf(PubkyAuthPermission("/pub/pubky.app/", "rw")), request.permissions)
}

Expand All @@ -182,11 +177,11 @@ class PubkyAuthRequestTest {
}

@Test
fun `parse Ring signup rejects missing and duplicate required values`() {
fun `parse signup rejects missing and duplicate required values`() {
val invalidUrls = listOf(
ringSignupUrl().replace("&secret=secret", ""),
"${ringSignupUrl()}&hs=other",
directSignupUrl("signup") + "&relay=https%3A%2F%2Frelay.example",
"pubkyauth://direct_signup",
"${directSignupUrl("direct_signup")}&hs=other",
"${directSignupUrl("signup", "one")}&st=two",
)

invalidUrls.forEach { url ->
Expand Down
Loading
Loading