Implement blind donation permits system.

This commit is contained in:
Alex Hart
2026-06-25 13:10:11 -03:00
committed by GitHub
parent 0fab27fee9
commit ed4c298c33
26 changed files with 1086 additions and 18 deletions
@@ -174,7 +174,7 @@ object RecurringInAppPaymentRepository {
InAppPaymentsRepository.getSubscriber(subscriberType)?.subscriberId ?: SubscriberId.generate()
}
donationsService.putSubscription(subscriberId).resultOrThrow
donationsService.createSubscriber(subscriberId).resultOrThrow
Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true)
@@ -0,0 +1,48 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.subscription.permits
import androidx.annotation.WorkerThread
import org.signal.core.util.Base64
import org.signal.donations.permits.DonationPermitError
import org.signal.libsignal.net.RequestResult
import org.signal.network.rest.RestStatusCodeError
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.whispersystems.signalservice.api.donations.DonationPermitProvider
import java.io.IOException
/**
* App-side [DonationPermitProvider]: spends a permit and base64-encodes it for the `Donation-Permit` header,
* translating an acquisition failure into a [RequestResult] the donations service can surface.
*/
object DonationPermits : DonationPermitProvider {
@WorkerThread
override fun getDonationPermit(): RequestResult<String, RestStatusCodeError> {
return AppDependencies.donationPermitsRepository
.spendOrAcquirePermit()
.fold(
ifLeft = { it.toRequestResult() },
ifRight = { RequestResult.Success(Base64.encodeWithPadding(it.serialize())) }
)
}
private fun DonationPermitError.toRequestResult(): RequestResult<String, RestStatusCodeError> {
return when (this) {
is DonationPermitError.IssuerUnavailable -> {
val statusCode = statusCode
val cause = cause
when {
statusCode != null -> RequestResult.NonSuccess(RestStatusCodeError(statusCode, emptyMap(), null))
cause is IOException -> RequestResult.RetryableNetworkError(cause)
else -> RequestResult.ApplicationError(cause ?: IllegalStateException("Donation permit issuer unavailable"))
}
}
DonationPermitError.VerificationFailed -> RequestResult.ApplicationError(IllegalStateException("Donation permit verification failed"))
DonationPermitError.MalformedResponse -> RequestResult.ApplicationError(IllegalStateException("Malformed donation permit response"))
}
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.subscription.permits
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import org.signal.donations.permits.DonationPermitError
import org.signal.donations.permits.DonationPermitIssuer
import org.signal.libsignal.net.RequestResult
import org.thoughtcrime.securesms.dependencies.AppDependencies
/**
* [DonationPermitIssuer] backed by [org.whispersystems.signalservice.api.donations.DonationsApi], translating a
* non-success [RequestResult] into a [DonationPermitError].
*/
object NetworkDonationPermitIssuer : DonationPermitIssuer {
override fun issue(requestBytes: ByteArray): Either<DonationPermitError, ByteArray> {
return when (val result = AppDependencies.donationsApi.createDonationPermits(requestBytes)) {
is RequestResult.Success -> result.result.right()
is RequestResult.NonSuccess -> DonationPermitError.IssuerUnavailable(statusCode = result.error.statusCode).left()
is RequestResult.RetryableNetworkError -> DonationPermitError.IssuerUnavailable(cause = result.networkError).left()
is RequestResult.ApplicationError -> DonationPermitError.IssuerUnavailable(cause = result.cause).left()
}
}
}
@@ -14,6 +14,7 @@ import org.signal.core.util.concurrent.LatestValueObservable
import org.signal.core.util.contentproviders.BlobProvider
import org.signal.core.util.orNull
import org.signal.core.util.resettableLazy
import org.signal.donations.permits.DonationPermitsRepository
import org.signal.glide.SignalGlideDependencies
import org.signal.libsignal.net.Network
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
@@ -415,6 +416,11 @@ object AppDependencies {
val donationsApi: DonationsApi
get() = networkModule.donationsApi
@JvmStatic
val donationPermitsRepository: DonationPermitsRepository by lazy {
provider.provideDonationPermitsRepository(signalServiceNetworkAccess.getConfiguration().zkGroupServerPublicParams)
}
val keyTransparencyApi: KeyTransparencyApi
get() = networkModule.keyTransparencyApi
@@ -488,6 +494,7 @@ object AppDependencies {
fun provideExoPlayerPool(): ExoPlayerPool<ExoPlayer>
fun provideAndroidCallAudioManager(): AudioManagerCompat
fun provideDonationsService(donationsApi: DonationsApi): DonationsService
fun provideDonationPermitsRepository(zkGroupServerPublicParams: ByteArray): DonationPermitsRepository
fun provideProfileService(profileOperations: ClientZkProfileOperations, authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): ProfileService
fun provideDeadlockDetector(): DeadlockDetector
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
@@ -507,6 +514,7 @@ object AppDependencies {
fun provideUsernameApi(unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): UsernameApi
fun provideCallingApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, pushServiceSocket: PushServiceSocket): CallingApi
fun providePaymentsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): PaymentsApi
fun provideCdsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): CdsApi
fun provideRateLimitChallengeApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): RateLimitChallengeApi
fun provideMessageApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): MessageApi
@@ -22,10 +22,12 @@ import org.signal.core.util.billing.BillingApi;
import org.signal.core.util.concurrent.DeadlockDetector;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.contentproviders.BlobProvider;
import org.signal.donations.permits.DonationPermitsRepository;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.zkgroup.GenericServerPublicParams;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
import org.signal.network.api.ArchiveApi;
@@ -44,9 +46,12 @@ import org.signal.network.api.SvrBApi;
import org.signal.network.api.UsernameApi;
import org.signal.network.rest.SignalRestClient;
import org.signal.network.service.MessageService;
import org.signal.video.exo.ExoPlayerPool;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.components.settings.app.subscription.permits.DonationPermits;
import org.thoughtcrime.securesms.components.settings.app.subscription.permits.NetworkDonationPermitIssuer;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.storage.SignalBaseIdentityKeyStore;
@@ -108,9 +113,8 @@ import org.thoughtcrime.securesms.util.FrameRateTracker;
import org.thoughtcrime.securesms.util.PreKeyBatcher;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
import org.signal.video.exo.ExoPlayerPool;
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat;
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
@@ -505,7 +509,16 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
@Override
public @NonNull DonationsService provideDonationsService(@NonNull DonationsApi donationsApi) {
return new DonationsService(donationsApi);
return new DonationsService(donationsApi, DonationPermits.INSTANCE);
}
@Override
public @NonNull DonationPermitsRepository provideDonationPermitsRepository(@NonNull byte[] zkGroupServerPublicParams) {
try {
return new DonationPermitsRepository(NetworkDonationPermitIssuer.INSTANCE, new ServerPublicParams(zkGroupServerPublicParams));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
@Override
@@ -463,6 +463,10 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
clearLocalCredentials()
}
if ((previous && !registered) || isAciChanged) {
AppDependencies.donationPermitsRepository.clearPermits()
}
if (registered && (!previous || isAciChanged)) {
registeredAtTimestamp = System.currentTimeMillis()
} else if (!registered) {