Add key transparency backend support.

This commit is contained in:
Michelle Tang
2026-01-30 13:17:26 -05:00
committed by Greyson Parrelli
parent 26739491a5
commit 423b8c942c
18 changed files with 355 additions and 9 deletions

View File

@@ -369,6 +369,9 @@ object AppDependencies {
val donationsApi: DonationsApi
get() = networkModule.donationsApi
val keyTransparencyApi: KeyTransparencyApi
get() = networkModule.keyTransparencyApi
@JvmStatic
val okHttpClient: OkHttpClient
get() = networkModule.okHttpClient
@@ -463,5 +466,6 @@ object AppDependencies {
fun provideRemoteConfigApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): RemoteConfigApi
fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi
fun provideSvrBApi(libSignalNetwork: Network): SvrBApi
fun provideKeyTransparencyApi(unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): KeyTransparencyApi
}
}

View File

@@ -579,6 +579,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
return new SvrBApi(libSignalNetwork);
}
@Override
public @NonNull KeyTransparencyApi provideKeyTransparencyApi(@NonNull SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket) {
return new KeyTransparencyApi(unauthWebSocket);
}
@VisibleForTesting
static class DynamicCredentialsProvider implements CredentialsProvider {

View File

@@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.dependencies
import org.signal.libsignal.internal.mapWithCancellation
import org.signal.libsignal.keytrans.KeyTransparencyException
import org.signal.libsignal.keytrans.VerificationFailedException
import org.signal.libsignal.net.AppExpiredException
import org.signal.libsignal.net.BadRequestError
import org.signal.libsignal.net.KeyTransparency
import org.signal.libsignal.net.NetworkException
import org.signal.libsignal.net.NetworkProtocolException
import org.signal.libsignal.net.RequestResult
import org.signal.libsignal.net.RetryLaterException
import org.signal.libsignal.net.ServerSideErrorException
import org.signal.libsignal.net.TimeoutException
import org.signal.libsignal.net.getOrError
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.ServiceId
import org.thoughtcrime.securesms.database.model.KeyTransparencyStore
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
/**
* Operations used when interacting with [org.signal.libsignal.net.KeyTransparencyClient]
*/
class KeyTransparencyApi(private val unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket) {
/**
* Uses KT to verify recipient. This is an unauthenticated and should only be called the first time KT is being requested for this recipient.
*/
suspend fun search(aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String, unidentifiedAccessKey: ByteArray, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
return unauthWebSocket.runCatchingWithUnauthChatConnection { chatConnection ->
chatConnection.keyTransparencyClient().search(aci, aciIdentityKey, e164, unidentifiedAccessKey, null, keyTransparencyStore)
.mapWithCancellation(
onSuccess = { RequestResult.Success(Unit) },
onError = { throwable ->
when (throwable) {
is TimeoutException,
is ServerSideErrorException,
is NetworkException,
is NetworkProtocolException -> {
RequestResult.RetryableNetworkError(throwable, null)
}
is RetryLaterException -> {
RequestResult.RetryableNetworkError(throwable, throwable.duration)
}
is VerificationFailedException,
is KeyTransparencyException,
is AppExpiredException,
is IllegalArgumentException -> {
RequestResult.NonSuccess(KeyTransparencyError(throwable))
}
else -> {
RequestResult.ApplicationError(throwable)
}
}
}
)
}.getOrError()
}
/**
* Monitors KT to verify recipient. This is an unauthenticated and should only be called following a successful [search].
*/
suspend fun monitor(monitorMode: KeyTransparency.MonitorMode, aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String, unidentifiedAccessKey: ByteArray, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
return unauthWebSocket.runCatchingWithUnauthChatConnection { chatConnection ->
chatConnection.keyTransparencyClient().monitor(monitorMode, aci, aciIdentityKey, e164, unidentifiedAccessKey, null, keyTransparencyStore)
.mapWithCancellation(
onSuccess = { RequestResult.Success(Unit) },
onError = { throwable ->
when (throwable) {
is TimeoutException,
is ServerSideErrorException,
is NetworkException,
is NetworkProtocolException -> {
RequestResult.RetryableNetworkError(throwable, null)
}
is RetryLaterException -> {
RequestResult.RetryableNetworkError(throwable, throwable.duration)
}
is VerificationFailedException,
is KeyTransparencyException,
is AppExpiredException,
is IllegalArgumentException -> {
RequestResult.NonSuccess(KeyTransparencyError(throwable))
}
else -> {
RequestResult.ApplicationError(throwable)
}
}
}
)
}.getOrError()
}
}
data class KeyTransparencyError(val exception: Throwable) : BadRequestError

View File

@@ -225,6 +225,10 @@ class NetworkDependenciesModule(
provider.provideSvrBApi(libsignalNetwork)
}
val keyTransparencyApi: KeyTransparencyApi by lazy {
provider.provideKeyTransparencyApi(unauthWebSocket)
}
val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(StandardUserAgentInterceptor())