Initial test implementation of SVR3.

This commit is contained in:
Greyson Parrelli
2024-05-14 09:47:21 -04:00
committed by Nicholas Tinsley
parent 68ced18ea1
commit f570f1f2c4
9 changed files with 199 additions and 15 deletions

View File

@@ -138,7 +138,7 @@ fun SvrPlaygroundScreenDarkTheme() {
Surface {
SvrPlaygroundScreen(
state = InternalSvrPlaygroundState(
options = persistentListOf(SvrImplementation.SVR2)
options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3)
)
)
}

View File

@@ -13,5 +13,6 @@ data class InternalSvrPlaygroundState(
enum class SvrImplementation(
val title: String
) {
SVR2("SVR2")
SVR2("SVR2"),
SVR3("SVR3")
}

View File

@@ -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<InternalSvrPlaygroundState> = mutableStateOf(
InternalSvrPlaygroundState(
options = persistentListOf(SvrImplementation.SVR2)
options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3)
)
)
val state: State<InternalSvrPlaygroundState> = _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
}
}
}

View File

@@ -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();
}

View File

@@ -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)
}
}

View File

@@ -90,7 +90,7 @@ class SecureValueRecoveryV2(
@Throws(IOException::class)
override fun authorization(): AuthCredentials {
return pushServiceSocket.svr2Authorization
return pushServiceSocket.svrAuthorization
}
override fun toString(): String {

View File

@@ -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?
}
}

View File

@@ -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;

View File

@@ -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<ByteArray?>
): CompletableFuture<CdsiLookupResponse?>? {
return inner.cdsiLookup(username, password, request, tokenConsumer)
return network.cdsiLookup(username, password, request, tokenConsumer)
}
}