diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt index 6b195fa557..569beda01b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt @@ -138,7 +138,7 @@ fun SvrPlaygroundScreenDarkTheme() { Surface { SvrPlaygroundScreen( state = InternalSvrPlaygroundState( - options = persistentListOf(SvrImplementation.SVR2) + options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3) ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt index 0f49cd8de1..943f4c7ca5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt @@ -13,5 +13,6 @@ data class InternalSvrPlaygroundState( enum class SvrImplementation( val title: String ) { - SVR2("SVR2") + SVR2("SVR2"), + SVR3("SVR3") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt index 191ad7deff..b9bbb77957 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt @@ -19,12 +19,13 @@ import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.whispersystems.signalservice.api.svr.SecureValueRecovery +import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3 class InternalSvrPlaygroundViewModel : ViewModel() { private val _state: MutableState = mutableStateOf( InternalSvrPlaygroundState( - options = persistentListOf(SvrImplementation.SVR2) + options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3) ) ) val state: State = _state @@ -104,6 +105,22 @@ class InternalSvrPlaygroundViewModel : ViewModel() { private fun SvrImplementation.toImplementation(): SecureValueRecovery { return when (this) { SvrImplementation.SVR2 -> ApplicationDependencies.getSignalServiceAccountManager().getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE) + SvrImplementation.SVR3 -> ApplicationDependencies.getSignalServiceAccountManager().getSecureValueRecoveryV3(ApplicationDependencies.getLibsignalNetwork().network, TestShareSetStorage()) + } + } + + /** + * Temporary implementation of share set storage. Only useful for testing. + */ + private class TestShareSetStorage : SecureValueRecoveryV3.ShareSetStorage { + private var shareSet: ByteArray? = null + + override fun write(data: ByteArray) { + shareSet = data + } + + override fun read(): ByteArray? { + return shareSet } } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 7c271cbc89..2106095746 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -56,6 +56,7 @@ import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.storage.StorageManifestKey; import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2; +import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -183,6 +184,10 @@ public class SignalServiceAccountManager { return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket); } + public SecureValueRecoveryV3 getSecureValueRecoveryV3(Network network, SecureValueRecoveryV3.ShareSetStorage storage) { + return new SecureValueRecoveryV3(network, pushServiceSocket, storage); + } + public WhoAmIResponse getWhoAmI() throws IOException { return this.pushServiceSocket.getWhoAmI(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt index 3e58ca7314..93f188a1c4 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt @@ -61,15 +61,21 @@ object PinHashUtil { * Takes a user-input PIN string and normalizes it to a standard character set. */ @JvmStatic - fun normalize(pin: String): ByteArray { + fun normalizeToString(pin: String): String { var normalizedPin = pin.trim() if (PinString.allNumeric(normalizedPin)) { normalizedPin = PinString.toArabic(normalizedPin) } - normalizedPin = Normalizer.normalize(normalizedPin, Normalizer.Form.NFKD) + return Normalizer.normalize(normalizedPin, Normalizer.Form.NFKD) + } - return normalizedPin.toByteArray(StandardCharsets.UTF_8) + /** + * Takes a user-input PIN string and normalizes it to a standard character set. + */ + @JvmStatic + fun normalize(pin: String): ByteArray { + return normalizeToString(pin).toByteArray(StandardCharsets.UTF_8) } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt index 7f1371dcfa..011dfd47a0 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt @@ -90,7 +90,7 @@ class SecureValueRecoveryV2( @Throws(IOException::class) override fun authorization(): AuthCredentials { - return pushServiceSocket.svr2Authorization + return pushServiceSocket.svrAuthorization } override fun toString(): String { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt new file mode 100644 index 0000000000..78b12d4ed9 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.svr + +import org.signal.core.util.logging.Log +import org.signal.libsignal.attest.AttestationFailedException +import org.signal.libsignal.net.EnclaveAuth +import org.signal.libsignal.net.Network +import org.signal.libsignal.net.NetworkException +import org.signal.libsignal.sgxsession.SgxCommunicationFailureException +import org.signal.libsignal.svr.DataMissingException +import org.signal.libsignal.svr.RestoreFailedException +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.kbs.PinHashUtil +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteResponse +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse +import org.whispersystems.signalservice.internal.push.AuthCredentials +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import java.io.IOException +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException + +/** + * An interface for working with V3 of the Secure Value Recovery service. + */ +class SecureValueRecoveryV3( + private val network: Network, + private val pushServiceSocket: PushServiceSocket, + private val shareSetStorage: ShareSetStorage +) : SecureValueRecovery { + + companion object { + val TAG = Log.tag(SecureValueRecoveryV3::class) + } + + override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession { + return Svr3PinChangeSession(userPin, masterKey) + } + + /** + * Note: Unlike SVR2, there is no concept of "resuming", so this is equivalent to starting a new session. + */ + override fun resumePinChangeSession(userPin: String, masterKey: MasterKey, serializedChangeSession: String): PinChangeSession { + return Svr3PinChangeSession(userPin, masterKey) + } + + override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse { + val normalizedPin: String = PinHashUtil.normalizeToString(userPin) + val shareSet = shareSetStorage.read() ?: return RestoreResponse.ApplicationError(IllegalStateException("No share set found!")) + val enclaveAuth = EnclaveAuth(authorization.username(), authorization.password()) + + return try { + val result = network.svr3().restore(normalizedPin, shareSet, enclaveAuth).get() + val masterKey = MasterKey(result) + RestoreResponse.Success(masterKey, authorization) + } catch (e: ExecutionException) { + when (val cause = e.cause) { + is NetworkException -> RestoreResponse.NetworkError(IOException(cause)) // TODO [svr3] Update when we get to IOException + is DataMissingException -> RestoreResponse.Missing + is RestoreFailedException -> RestoreResponse.PinMismatch(1) // TODO [svr3] Get proper API for this + is AttestationFailedException -> RestoreResponse.ApplicationError(cause) + is SgxCommunicationFailureException -> RestoreResponse.ApplicationError(cause) + is IOException -> RestoreResponse.NetworkError(cause) + else -> RestoreResponse.ApplicationError(cause ?: RuntimeException("Unknown!")) + } + } catch (e: InterruptedException) { + return RestoreResponse.ApplicationError(e) + } catch (e: CancellationException) { + return RestoreResponse.ApplicationError(e) + } + } + + override fun restoreDataPostRegistration(userPin: String): RestoreResponse { + val authorization: AuthCredentials = try { + pushServiceSocket.svrAuthorization + } catch (e: NonSuccessfulResponseCodeException) { + return RestoreResponse.ApplicationError(e) + } catch (e: IOException) { + return RestoreResponse.NetworkError(e) + } catch (e: Exception) { + return RestoreResponse.ApplicationError(e) + } + + return restoreDataPreRegistration(authorization, userPin) + } + + /** + * There's no concept of "deleting" data with SVR3. + */ + override fun deleteData(): DeleteResponse { + return DeleteResponse.Success + } + + @Throws(IOException::class) + override fun authorization(): AuthCredentials { + return pushServiceSocket.svrAuthorization + } + + inner class Svr3PinChangeSession( + private val userPin: String, + private val masterKey: MasterKey + ) : PinChangeSession { + override fun execute(): BackupResponse { + val normalizedPin: String = PinHashUtil.normalizeToString(userPin) + val rawAuth = try { + pushServiceSocket.svrAuthorization + } catch (e: NonSuccessfulResponseCodeException) { + return BackupResponse.ApplicationError(e) + } catch (e: IOException) { + return BackupResponse.NetworkError(e) + } catch (e: Exception) { + return BackupResponse.ApplicationError(e) + } + + val enclaveAuth = EnclaveAuth(rawAuth.username(), rawAuth.password()) + + return try { + val result = network.svr3().backup(masterKey.serialize(), normalizedPin, 10, enclaveAuth).get() + shareSetStorage.write(result) + BackupResponse.Success(masterKey, rawAuth) + } catch (e: ExecutionException) { + when (val cause = e.cause) { + is NetworkException -> BackupResponse.NetworkError(IOException(cause)) // TODO [svr] Update when we move to IOException + is AttestationFailedException -> BackupResponse.ApplicationError(cause) + is SgxCommunicationFailureException -> BackupResponse.ApplicationError(cause) + is IOException -> BackupResponse.NetworkError(cause) + else -> BackupResponse.ApplicationError(cause ?: RuntimeException("Unknown!")) + } + } catch (e: InterruptedException) { + BackupResponse.ApplicationError(e) + } catch (e: CancellationException) { + BackupResponse.ApplicationError(e) + } + } + + override fun serialize(): String { + // There is no "resuming" SVR3, so we don't need to serialize anything + return "" + } + } + + /** + * An interface to allow reading and writing the "share set" to persistent storage. + */ + interface ShareSetStorage { + fun write(data: ByteArray) + fun read(): ByteArray? + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index af6602b5d3..598402df91 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -304,7 +304,7 @@ public class PushServiceSocket { private static final String REGISTRATION_PATH = "/v1/registration"; private static final String CDSI_AUTH = "/v2/directory/auth"; - private static final String SVR2_AUTH = "/v2/backup/auth"; + private static final String SVR_AUTH = "/v2/backup/auth"; private static final String REPORT_SPAM = "/v1/messages/report/%s/%s"; @@ -485,8 +485,8 @@ public class PushServiceSocket { return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class); } - public AuthCredentials getSvr2Authorization() throws IOException { - String body = makeServiceRequest(SVR2_AUTH, "GET", null); + public AuthCredentials getSvrAuthorization() throws IOException { + String body = makeServiceRequest(SVR_AUTH, "GET", null); AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class); return credentials; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt index 6d2134ea3c..f36be5c9be 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt @@ -21,7 +21,7 @@ import java.util.function.Consumer /** * Makes Network API more ergonomic to use with Android client types */ -class LibSignalNetwork(private val inner: Network, config: SignalServiceConfiguration) { +class LibSignalNetwork(val network: Network, config: SignalServiceConfiguration) { init { resetSettings(config) } @@ -31,7 +31,7 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur ): ChatService { val username = credentialsProvider?.username ?: "" val password = credentialsProvider?.password ?: "" - return inner.createChatService(username, password) + return network.createChatService(username, password) } fun resetSettings(config: SignalServiceConfiguration) { @@ -40,9 +40,9 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur private fun resetProxy(proxy: SignalProxy?) { if (proxy == null) { - inner.clearProxy() + network.clearProxy() } else { - inner.setProxy(proxy.host, proxy.port) + network.setProxy(proxy.host, proxy.port) } } @@ -54,6 +54,6 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur request: CdsiLookupRequest?, tokenConsumer: Consumer ): CompletableFuture? { - return inner.cdsiLookup(username, password, request, tokenConsumer) + return network.cdsiLookup(username, password, request, tokenConsumer) } }