Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ val bcp47Locales = listOf(
)
val e2eEnv = envFlag("E2E", default = false)
val e2eBackendEnv = providers.environmentVariable("E2E_BACKEND").orElse("local")
val e2eHomegateUrlEnv = providers.environmentVariable("E2E_HOMEGATE_URL").orElse("http://127.0.0.1:6288")
val e2eHomegateUrlEnv = providers.environmentVariable("E2E_HOMEGATE_URL").orElse("http://10.0.2.2:6288")
Comment thread
ben-kaufman marked this conversation as resolved.
Outdated
val geoEnv = envFlag("GEO", default = true)
val paykitUiDisabledEnv = envFlag("PAYKIT_UI_DISABLED", default = false)
val trezorBridgeEnv = localProp("TREZOR_BRIDGE").map { it.toBoolean().toString() }.orElse("false")
Expand Down
12 changes: 9 additions & 3 deletions app/src/main/java/to/bitkit/models/PubkyAuthRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ enum class PubkyAuthClaim(val wireValue: String) {
const val QUERY_PARAMETER = "x-bitkit-claim"

/** Capabilities required by the watch-only Paykit Server setup flow. */
const val WATCH_ONLY_ACCOUNT_CAPABILITIES = "/pub/paykit/v0/bitkit/server/:rw"
const val WATCH_ONLY_ACCOUNT_CAPABILITIES =
"/pub/paykit/v0/bitkit/server/:rw,/pub/paykit/v0/private/bitkit/server/:rw"
Comment thread
ben-kaufman marked this conversation as resolved.

private val watchOnlyAccountCapabilitySet = WATCH_ONLY_ACCOUNT_CAPABILITIES.split(",").toSet()

fun matchesWatchOnlyAccountCapabilities(capabilities: String) =
capabilities.split(",").toSet() == watchOnlyAccountCapabilitySet
Comment thread
ben-kaufman marked this conversation as resolved.
Outdated

fun fromWireValue(value: String) = entries.firstOrNull { it.wireValue == value }
}
Expand Down Expand Up @@ -95,7 +101,7 @@ data class PubkyAuthRequest(
capabilities: String,
): Result<PubkyAuthClaim?> = when {
claimValues.size > 1 -> Result.failure(PubkyAuthRequestError.DuplicateBitkitClaim)
claimValues.isEmpty() && capabilities == PubkyAuthClaim.WATCH_ONLY_ACCOUNT_CAPABILITIES ->
claimValues.isEmpty() && PubkyAuthClaim.matchesWatchOnlyAccountCapabilities(capabilities) ->
Result.failure(PubkyAuthRequestError.MissingBitkitClaim)
claimValues.isEmpty() -> Result.success(null)
else -> validateBitkitClaimValue(claimValues.first(), capabilities)
Expand All @@ -108,7 +114,7 @@ data class PubkyAuthRequest(
val claim = PubkyAuthClaim.fromWireValue(claimValue)
?: return Result.failure(PubkyAuthRequestError.UnsupportedBitkitClaim(claimValue))

return if (capabilities == PubkyAuthClaim.WATCH_ONLY_ACCOUNT_CAPABILITIES) {
return if (PubkyAuthClaim.matchesWatchOnlyAccountCapabilities(capabilities)) {
Result.success(claim)
} else {
Result.failure(PubkyAuthRequestError.InvalidBitkitClaimCapabilities)
Expand Down
31 changes: 24 additions & 7 deletions app/src/main/java/to/bitkit/services/PaykitSdkService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.synonym.paykit.PrivateReceivingDetailReservationResponseKind
import com.synonym.paykit.PrivateStreamCounterpartyIntakeReport
import com.synonym.paykit.PubkyAuthCompanionClaim
import com.synonym.paykit.PubkyAuthRequest
import com.synonym.paykit.PubkyClientConfig
import com.synonym.paykit.PubkyLocalSecretKey
import com.synonym.paykit.PubkyProfile
import com.synonym.paykit.PubkySessionAccess
Expand All @@ -53,6 +54,7 @@ import com.synonym.paykit.SdkStateBlobSnapshot
import com.synonym.paykit.SdkStateBlobStore
import com.synonym.paykit.decodeSdkStateBlobSnapshot
import com.synonym.paykit.defaultConfig
import com.synonym.paykit.defaultPubkyClientConfig
import com.synonym.paykit.encodeSdkStateBlobSnapshot
import com.synonym.paykit.parsePubkyAuthUrl
import com.synonym.paykit.pubkyPublicKeyFromSecret
Expand Down Expand Up @@ -186,7 +188,7 @@ class PaykitSdkService @Inject constructor(
): PubkySessionBootstrapResult {
isSetup.await()
val previousPublicKey = operationMutex.withLock { currentSdkStatePublicKeyLocked() }
val result = PubkySessionBootstrap().importSession(
val result = bootstrap().importSession(
sessionSecret = secret,
localSecretKey = if (includeLocalSecret) sessionProvider.loadLocalSecretKey() else null,
receiverNoiseSecretKey = sessionProvider.loadOrDeriveReceiverNoiseSecretKey(),
Expand All @@ -210,7 +212,7 @@ class PaykitSdkService @Inject constructor(
): PubkySessionBootstrapResult {
isSetup.await()
val previousPublicKey = operationMutex.withLock { currentSdkStatePublicKeyLocked() }
val result = PubkySessionBootstrap().signUp(
val result = bootstrap().signUp(
localSecretKey = localSecretKey(secretKeyHex),
receiverNoiseSecretKey = sessionProvider.loadOrDeriveReceiverNoiseSecretKey(),
homeserverPublicKey = homeserverPublicKey,
Expand All @@ -231,7 +233,7 @@ class PaykitSdkService @Inject constructor(
suspend fun signIn(secretKeyHex: String): PubkySessionBootstrapResult {
isSetup.await()
val previousPublicKey = operationMutex.withLock { currentSdkStatePublicKeyLocked() }
val result = PubkySessionBootstrap().signIn(
val result = bootstrap().signIn(
localSecretKey = localSecretKey(secretKeyHex),
receiverNoiseSecretKey = sessionProvider.loadOrDeriveReceiverNoiseSecretKey(),
requiredCapabilities = requiredCapabilities(),
Expand All @@ -250,7 +252,7 @@ class PaykitSdkService @Inject constructor(
suspend fun startAuth(): String {
isSetup.await()
return operationMutex.withLock {
val request = PubkySessionBootstrap().startSignInAuth(requiredCapabilities())
val request = bootstrap().startSignInAuth(requiredCapabilities())
activeAuthRequest = request
request.authorizationUrl()
}
Expand Down Expand Up @@ -292,7 +294,7 @@ class PaykitSdkService @Inject constructor(

suspend fun approveAuth(authUrl: String, expectedCapabilities: String, secretKeyHex: String) {
isSetup.await()
PubkySessionBootstrap().approveAuth(
bootstrap().approveAuth(
authUrl = authUrl,
expectedCapabilities = expectedCapabilities,
localSecretKey = localSecretKey(secretKeyHex),
Expand All @@ -306,7 +308,7 @@ class PaykitSdkService @Inject constructor(
claim: PubkyAuthCompanionClaim,
) {
isSetup.await()
PubkySessionBootstrap().approveAuthWithCompanionClaim(
bootstrap().approveAuthWithCompanionClaim(
authUrl = authUrl,
expectedCapabilities = expectedCapabilities,
localSecretKey = localSecretKey(secretKeyHex),
Expand Down Expand Up @@ -786,14 +788,17 @@ class PaykitSdkService @Inject constructor(

private suspend fun handle(): PaykitSdk = handleMutex.withLock {
sdk?.let { return@withLock it }
PaykitSdk.withPaymentAdapter(
PaykitSdk.withPaymentAdapterAndPubkyClientConfig(
stateStore = stateStore,
sessionProvider = sessionProvider,
paymentAdapter = paymentAdapter,
config = paykitSdkConfig(),
pubkyClient = paykitPubkyClientConfig(),
).also { sdk = it }
}

private fun bootstrap() = PubkySessionBootstrap.withPubkyClientConfig(paykitPubkyClientConfig())
Comment thread
ben-kaufman marked this conversation as resolved.
Outdated

private fun resetRuntime() {
sdk = null
}
Expand Down Expand Up @@ -845,6 +850,18 @@ internal fun paykitSdkConfig() = defaultConfig(PaykitReceiverPaths.WALLET).copy(
publicContactSharing = BitkitPaykitSdkConfig.publicContactSharing,
)

internal fun paykitPubkyClientConfig(
isLocalE2eBackend: Boolean = Env.isLocalE2eBackend,
baseConfig: PubkyClientConfig = defaultPubkyClientConfig(),
) =
if (isLocalE2eBackend) {
baseConfig.copy(
localTestnetHost = "10.0.2.2",
Comment thread
ben-kaufman marked this conversation as resolved.
Outdated
)
} else {
baseConfig
}

private class PaykitSdkStateBlobStore(
private val keychain: Keychain,
) : SdkStateBlobStore {
Expand Down
24 changes: 24 additions & 0 deletions app/src/test/java/to/bitkit/models/PubkyAuthRequestTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ class PubkyAuthRequestTest {
assertEquals(PubkyAuthClaim.WATCH_ONLY_ACCOUNT_V1, request.bitkitClaim)
}

@Test
fun `parse recognizes watch-only account claim with reordered capabilities`() {
val capabilities = PubkyAuthClaim.WATCH_ONLY_ACCOUNT_CAPABILITIES.split(",").reversed().joinToString(",")
Comment thread
ben-kaufman marked this conversation as resolved.
val request = PubkyAuthRequest.parse(
rawUrl = authUrl(capabilities, PubkyAuthClaim.WATCH_ONLY_ACCOUNT_V1.wireValue),
relay = "https://httprelay.pubky.app/inbox/",
capabilities = capabilities,
).getOrThrow()

assertEquals(PubkyAuthClaim.WATCH_ONLY_ACCOUNT_V1, request.bitkitClaim)
}

@Test
fun `parse preserves normal auth without Bitkit claim`() {
val request = PubkyAuthRequest.parse(
Expand Down Expand Up @@ -84,6 +96,18 @@ class PubkyAuthRequestTest {
assertIs<PubkyAuthRequestError.InvalidBitkitClaimCapabilities>(result.exceptionOrNull())
}

@Test
fun `parse rejects watch-only claim without private capability`() {
val capabilities = "/pub/paykit/v0/bitkit/server/:rw"
val result = PubkyAuthRequest.parse(
rawUrl = authUrl(capabilities, PubkyAuthClaim.WATCH_ONLY_ACCOUNT_V1.wireValue),
relay = "https://httprelay.pubky.app/inbox/",
capabilities = capabilities,
)

assertIs<PubkyAuthRequestError.InvalidBitkitClaimCapabilities>(result.exceptionOrNull())
}

@Test
fun `parseCapabilities parses single permission`() {
val permissions = PubkyAuthRequest.parseCapabilities("/pub/bitkit.to/:rw")
Expand Down
27 changes: 27 additions & 0 deletions app/src/test/java/to/bitkit/services/PaykitSdkServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package to.bitkit.services

import com.synonym.paykit.EncryptedLinkRecoveryMarkerPolicy
import com.synonym.paykit.EndpointManagementScope
import com.synonym.paykit.PubkyClientConfig
import com.synonym.paykit.PublicContactSharingPolicy
import org.junit.Test
import org.mockito.kotlin.mock
Expand All @@ -17,13 +18,39 @@ import kotlin.test.assertNull
import kotlin.test.assertTrue

class PaykitSdkServiceTest {
private val basePubkyClientConfig = PubkyClientConfig(
requestTimeoutSecs = 30uL,
localTestnetHost = null,
)

@Test
fun `config scopes public endpoint sync to Bitkit managed endpoints`() {
assertEquals(EndpointManagementScope.MANAGED_ONLY, BitkitPaykitSdkConfig.endpointManagementScope)
assertEquals(PublicContactSharingPolicy.LOCAL_ONLY, BitkitPaykitSdkConfig.publicContactSharing)
assertEquals(EncryptedLinkRecoveryMarkerPolicy.ENABLED, BitkitPaykitSdkConfig.encryptedLinkRecoveryMarkers)
}

@Test
fun `production preserves Pubky client config`() {
val config = paykitPubkyClientConfig(
isLocalE2eBackend = false,
baseConfig = basePubkyClientConfig,
)

assertEquals(basePubkyClientConfig, config)
}

@Test
fun `local E2E uses emulator host for Pubky testnet`() {
val config = paykitPubkyClientConfig(
isLocalE2eBackend = true,
baseConfig = basePubkyClientConfig,
)

assertEquals("10.0.2.2", config.localTestnetHost)
assertEquals(basePubkyClientConfig.requestTimeoutSecs, config.requestTimeoutSecs)
}

@Test
fun `receiver noise derivation matches cross platform vector`() {
val seed = (
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1131.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed Paykit Server authorization to require both public and private payment capabilities.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" }
biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" }
bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.5.5" }
paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc39" }
paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc42" }
bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" }
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
Expand Down
Loading