mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Initial test implementation of SVR3.
This commit is contained in:
committed by
Nicholas Tinsley
parent
68ced18ea1
commit
f570f1f2c4
@@ -138,7 +138,7 @@ fun SvrPlaygroundScreenDarkTheme() {
|
||||
Surface {
|
||||
SvrPlaygroundScreen(
|
||||
state = InternalSvrPlaygroundState(
|
||||
options = persistentListOf(SvrImplementation.SVR2)
|
||||
options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ data class InternalSvrPlaygroundState(
|
||||
enum class SvrImplementation(
|
||||
val title: String
|
||||
) {
|
||||
SVR2("SVR2")
|
||||
SVR2("SVR2"),
|
||||
SVR3("SVR3")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class SecureValueRecoveryV2(
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun authorization(): AuthCredentials {
|
||||
return pushServiceSocket.svr2Authorization
|
||||
return pushServiceSocket.svrAuthorization
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user