mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +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 {
|
Surface {
|
||||||
SvrPlaygroundScreen(
|
SvrPlaygroundScreen(
|
||||||
state = InternalSvrPlaygroundState(
|
state = InternalSvrPlaygroundState(
|
||||||
options = persistentListOf(SvrImplementation.SVR2)
|
options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ data class InternalSvrPlaygroundState(
|
|||||||
enum class SvrImplementation(
|
enum class SvrImplementation(
|
||||||
val title: String
|
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.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery
|
import org.whispersystems.signalservice.api.svr.SecureValueRecovery
|
||||||
|
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3
|
||||||
|
|
||||||
class InternalSvrPlaygroundViewModel : ViewModel() {
|
class InternalSvrPlaygroundViewModel : ViewModel() {
|
||||||
|
|
||||||
private val _state: MutableState<InternalSvrPlaygroundState> = mutableStateOf(
|
private val _state: MutableState<InternalSvrPlaygroundState> = mutableStateOf(
|
||||||
InternalSvrPlaygroundState(
|
InternalSvrPlaygroundState(
|
||||||
options = persistentListOf(SvrImplementation.SVR2)
|
options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val state: State<InternalSvrPlaygroundState> = _state
|
val state: State<InternalSvrPlaygroundState> = _state
|
||||||
@@ -104,6 +105,22 @@ class InternalSvrPlaygroundViewModel : ViewModel() {
|
|||||||
private fun SvrImplementation.toImplementation(): SecureValueRecovery {
|
private fun SvrImplementation.toImplementation(): SecureValueRecovery {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
SvrImplementation.SVR2 -> ApplicationDependencies.getSignalServiceAccountManager().getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE)
|
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.StorageKey;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
|
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
|
||||||
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2;
|
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.CredentialsProvider;
|
||||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||||
@@ -183,6 +184,10 @@ public class SignalServiceAccountManager {
|
|||||||
return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket);
|
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 {
|
public WhoAmIResponse getWhoAmI() throws IOException {
|
||||||
return this.pushServiceSocket.getWhoAmI();
|
return this.pushServiceSocket.getWhoAmI();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,15 +61,21 @@ object PinHashUtil {
|
|||||||
* Takes a user-input PIN string and normalizes it to a standard character set.
|
* Takes a user-input PIN string and normalizes it to a standard character set.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun normalize(pin: String): ByteArray {
|
fun normalizeToString(pin: String): String {
|
||||||
var normalizedPin = pin.trim()
|
var normalizedPin = pin.trim()
|
||||||
|
|
||||||
if (PinString.allNumeric(normalizedPin)) {
|
if (PinString.allNumeric(normalizedPin)) {
|
||||||
normalizedPin = PinString.toArabic(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)
|
@Throws(IOException::class)
|
||||||
override fun authorization(): AuthCredentials {
|
override fun authorization(): AuthCredentials {
|
||||||
return pushServiceSocket.svr2Authorization
|
return pushServiceSocket.svrAuthorization
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
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 REGISTRATION_PATH = "/v1/registration";
|
||||||
|
|
||||||
private static final String CDSI_AUTH = "/v2/directory/auth";
|
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";
|
private static final String REPORT_SPAM = "/v1/messages/report/%s/%s";
|
||||||
|
|
||||||
@@ -485,8 +485,8 @@ public class PushServiceSocket {
|
|||||||
return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class);
|
return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthCredentials getSvr2Authorization() throws IOException {
|
public AuthCredentials getSvrAuthorization() throws IOException {
|
||||||
String body = makeServiceRequest(SVR2_AUTH, "GET", null);
|
String body = makeServiceRequest(SVR_AUTH, "GET", null);
|
||||||
AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class);
|
AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class);
|
||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.function.Consumer
|
|||||||
/**
|
/**
|
||||||
* Makes Network API more ergonomic to use with Android client types
|
* 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 {
|
init {
|
||||||
resetSettings(config)
|
resetSettings(config)
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur
|
|||||||
): ChatService {
|
): ChatService {
|
||||||
val username = credentialsProvider?.username ?: ""
|
val username = credentialsProvider?.username ?: ""
|
||||||
val password = credentialsProvider?.password ?: ""
|
val password = credentialsProvider?.password ?: ""
|
||||||
return inner.createChatService(username, password)
|
return network.createChatService(username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetSettings(config: SignalServiceConfiguration) {
|
fun resetSettings(config: SignalServiceConfiguration) {
|
||||||
@@ -40,9 +40,9 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur
|
|||||||
|
|
||||||
private fun resetProxy(proxy: SignalProxy?) {
|
private fun resetProxy(proxy: SignalProxy?) {
|
||||||
if (proxy == null) {
|
if (proxy == null) {
|
||||||
inner.clearProxy()
|
network.clearProxy()
|
||||||
} else {
|
} 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?,
|
request: CdsiLookupRequest?,
|
||||||
tokenConsumer: Consumer<ByteArray?>
|
tokenConsumer: Consumer<ByteArray?>
|
||||||
): CompletableFuture<CdsiLookupResponse?>? {
|
): CompletableFuture<CdsiLookupResponse?>? {
|
||||||
return inner.cdsiLookup(username, password, request, tokenConsumer)
|
return network.cdsiLookup(username, password, request, tokenConsumer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user