mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-27 20:24:32 +01:00
App ability to regV5 in the main app, behind compile flag.
This commit is contained in:
@@ -110,6 +110,7 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Environment;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
@@ -403,6 +404,20 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
AppDependencies.init(this, new ApplicationDependencyProvider(this));
|
||||
}
|
||||
AppForegroundObserver.begin();
|
||||
|
||||
if (Environment.USE_NEW_REGISTRATION) {
|
||||
initializeRegistrationDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeRegistrationDependencies() {
|
||||
org.signal.registration.RegistrationDependencies.Companion.provide(
|
||||
new org.signal.registration.RegistrationDependencies(
|
||||
new org.thoughtcrime.securesms.registration.v2.AppRegistrationNetworkController(this, AppDependencies.getPushServiceSocket()),
|
||||
new org.thoughtcrime.securesms.registration.v2.AppRegistrationStorageController(this),
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void initializeFirstEverAppLaunch() {
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
|
||||
import org.thoughtcrime.securesms.util.Environment;
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
@@ -134,8 +135,12 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
Intent intent = getIntentForState(applicationState);
|
||||
if (intent != null) {
|
||||
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
if (applicationState == STATE_WELCOME_PUSH_SCREEN && Environment.USE_NEW_REGISTRATION) {
|
||||
startActivity(intent);
|
||||
} else {
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +226,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
if (Environment.USE_NEW_REGISTRATION) {
|
||||
return org.signal.registration.RegistrationActivity.createIntent(this);
|
||||
} else {
|
||||
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getEnterSignalPinIntent() {
|
||||
|
||||
@@ -344,6 +344,10 @@ object AppDependencies {
|
||||
val linkDeviceApi: LinkDeviceApi
|
||||
get() = networkModule.linkDeviceApi
|
||||
|
||||
@JvmStatic
|
||||
val pushServiceSocket: PushServiceSocket
|
||||
get() = networkModule.pushServiceSocket
|
||||
|
||||
@JvmStatic
|
||||
val registrationApi: RegistrationApi
|
||||
get() = networkModule.registrationApi
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.signal.registration.NetworkController
|
||||
import org.signal.registration.NetworkController.AccountAttributes
|
||||
import org.signal.registration.NetworkController.BackupMasterKeyError
|
||||
import org.signal.registration.NetworkController.CheckSvrCredentialsError
|
||||
import org.signal.registration.NetworkController.CheckSvrCredentialsResponse
|
||||
import org.signal.registration.NetworkController.CreateSessionError
|
||||
import org.signal.registration.NetworkController.GetSessionStatusError
|
||||
import org.signal.registration.NetworkController.GetSvrCredentialsError
|
||||
import org.signal.registration.NetworkController.PreKeyCollection
|
||||
import org.signal.registration.NetworkController.ProvisioningEvent
|
||||
import org.signal.registration.NetworkController.ProvisioningMessage
|
||||
import org.signal.registration.NetworkController.RegisterAccountError
|
||||
import org.signal.registration.NetworkController.RegisterAccountResponse
|
||||
import org.signal.registration.NetworkController.RegistrationLockResponse
|
||||
import org.signal.registration.NetworkController.RegistrationNetworkResult
|
||||
import org.signal.registration.NetworkController.RequestVerificationCodeError
|
||||
import org.signal.registration.NetworkController.RestoreMasterKeyError
|
||||
import org.signal.registration.NetworkController.SessionMetadata
|
||||
import org.signal.registration.NetworkController.SetAccountAttributesError
|
||||
import org.signal.registration.NetworkController.SetRegistrationLockError
|
||||
import org.signal.registration.NetworkController.SubmitVerificationCodeError
|
||||
import org.signal.registration.NetworkController.SvrCredentials
|
||||
import org.signal.registration.NetworkController.ThirdPartyServiceErrorResponse
|
||||
import org.signal.registration.NetworkController.UpdateSessionError
|
||||
import org.signal.registration.NetworkController.VerificationCodeTransport
|
||||
import org.signal.registration.proto.RegistrationProvisionMessage
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.gcm.FcmUtil
|
||||
import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.fcm.PushChallengeRequest
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse
|
||||
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes as ServiceAccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection as ServicePreKeyCollection
|
||||
|
||||
/**
|
||||
* Implementation of [NetworkController] that bridges to the app's existing network infrastructure.
|
||||
*/
|
||||
class AppRegistrationNetworkController(
|
||||
private val context: Context,
|
||||
private val pushServiceSocket: PushServiceSocket
|
||||
) : NetworkController {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(AppRegistrationNetworkController::class)
|
||||
private val PUSH_REQUEST_TIMEOUT = 5.seconds.inWholeMilliseconds
|
||||
}
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
override suspend fun createSession(
|
||||
e164: String,
|
||||
fcmToken: String?,
|
||||
mcc: String?,
|
||||
mnc: String?
|
||||
): RegistrationNetworkResult<SessionMetadata, CreateSessionError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pushServiceSocket.createVerificationSessionV2(e164, fcmToken, mcc, mnc).use { response ->
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Success(session)
|
||||
}
|
||||
422 -> {
|
||||
RegistrationNetworkResult.Failure(CreateSessionError.InvalidRequest(response.body.string()))
|
||||
}
|
||||
429 -> {
|
||||
RegistrationNetworkResult.Failure(CreateSessionError.RateLimited(response.retryAfter()))
|
||||
}
|
||||
else -> {
|
||||
RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSession(sessionId: String): RegistrationNetworkResult<SessionMetadata, GetSessionStatusError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pushServiceSocket.getSessionStatusV2(sessionId).use { response ->
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Success(session)
|
||||
}
|
||||
400 -> {
|
||||
RegistrationNetworkResult.Failure(GetSessionStatusError.InvalidRequest(response.body.string()))
|
||||
}
|
||||
404 -> {
|
||||
RegistrationNetworkResult.Failure(GetSessionStatusError.SessionNotFound(response.body.string()))
|
||||
}
|
||||
422 -> {
|
||||
RegistrationNetworkResult.Failure(GetSessionStatusError.InvalidSessionId(response.body.string()))
|
||||
}
|
||||
else -> {
|
||||
RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateSession(
|
||||
sessionId: String?,
|
||||
pushChallengeToken: String?,
|
||||
captchaToken: String?
|
||||
): RegistrationNetworkResult<SessionMetadata, UpdateSessionError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pushServiceSocket.patchVerificationSessionV2(
|
||||
sessionId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
captchaToken,
|
||||
pushChallengeToken
|
||||
).use { response ->
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Success(session)
|
||||
}
|
||||
400 -> {
|
||||
RegistrationNetworkResult.Failure(UpdateSessionError.InvalidRequest(response.body.string()))
|
||||
}
|
||||
409 -> {
|
||||
RegistrationNetworkResult.Failure(UpdateSessionError.RejectedUpdate(response.body.string()))
|
||||
}
|
||||
429 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(UpdateSessionError.RateLimited(response.retryAfter(), session))
|
||||
}
|
||||
else -> {
|
||||
RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun requestVerificationCode(
|
||||
sessionId: String,
|
||||
locale: Locale?,
|
||||
androidSmsRetrieverSupported: Boolean,
|
||||
transport: VerificationCodeTransport
|
||||
): RegistrationNetworkResult<SessionMetadata, RequestVerificationCodeError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val socketTransport = when (transport) {
|
||||
VerificationCodeTransport.SMS -> PushServiceSocket.VerificationCodeTransport.SMS
|
||||
VerificationCodeTransport.VOICE -> PushServiceSocket.VerificationCodeTransport.VOICE
|
||||
}
|
||||
|
||||
pushServiceSocket.requestVerificationCodeV2(
|
||||
sessionId,
|
||||
locale,
|
||||
androidSmsRetrieverSupported,
|
||||
socketTransport
|
||||
).use { response ->
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Success(session)
|
||||
}
|
||||
400 -> {
|
||||
RegistrationNetworkResult.Failure(RequestVerificationCodeError.InvalidSessionId(response.body.string()))
|
||||
}
|
||||
404 -> {
|
||||
RegistrationNetworkResult.Failure(RequestVerificationCodeError.SessionNotFound(response.body.string()))
|
||||
}
|
||||
409 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(RequestVerificationCodeError.MissingRequestInformationOrAlreadyVerified(session))
|
||||
}
|
||||
418 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(RequestVerificationCodeError.CouldNotFulfillWithRequestedTransport(session))
|
||||
}
|
||||
429 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(RequestVerificationCodeError.RateLimited(response.retryAfter(), session))
|
||||
}
|
||||
440 -> {
|
||||
val errorBody = json.decodeFromString<ThirdPartyServiceErrorResponse>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(RequestVerificationCodeError.ThirdPartyServiceError(errorBody))
|
||||
}
|
||||
else -> {
|
||||
RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun submitVerificationCode(
|
||||
sessionId: String,
|
||||
verificationCode: String
|
||||
): RegistrationNetworkResult<SessionMetadata, SubmitVerificationCodeError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pushServiceSocket.submitVerificationCodeV2(sessionId, verificationCode).use { response ->
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Success(session)
|
||||
}
|
||||
400 -> {
|
||||
RegistrationNetworkResult.Failure(SubmitVerificationCodeError.InvalidSessionIdOrVerificationCode(response.body.string()))
|
||||
}
|
||||
404 -> {
|
||||
RegistrationNetworkResult.Failure(SubmitVerificationCodeError.SessionNotFound(response.body.string()))
|
||||
}
|
||||
409 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(SubmitVerificationCodeError.SessionAlreadyVerifiedOrNoCodeRequested(session))
|
||||
}
|
||||
429 -> {
|
||||
val session = json.decodeFromString<SessionMetadata>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(SubmitVerificationCodeError.RateLimited(response.retryAfter(), session))
|
||||
}
|
||||
else -> {
|
||||
RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun registerAccount(
|
||||
e164: String,
|
||||
password: String,
|
||||
sessionId: String?,
|
||||
recoveryPassword: String?,
|
||||
attributes: AccountAttributes,
|
||||
aciPreKeys: PreKeyCollection,
|
||||
pniPreKeys: PreKeyCollection,
|
||||
fcmToken: String?,
|
||||
skipDeviceTransfer: Boolean
|
||||
): RegistrationNetworkResult<RegisterAccountResponse, RegisterAccountError> = withContext(Dispatchers.IO) {
|
||||
check(sessionId != null || recoveryPassword != null) { "Either sessionId or recoveryPassword must be provided" }
|
||||
check(sessionId == null || recoveryPassword == null) { "Either sessionId or recoveryPassword must be provided, but not both" }
|
||||
|
||||
try {
|
||||
pushServiceSocket.submitRegistrationRequestV2(
|
||||
e164,
|
||||
password,
|
||||
sessionId,
|
||||
recoveryPassword,
|
||||
attributes.toServiceAccountAttributes(),
|
||||
aciPreKeys.toServicePreKeyCollection(),
|
||||
pniPreKeys.toServicePreKeyCollection(),
|
||||
fcmToken,
|
||||
skipDeviceTransfer
|
||||
).use { response ->
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val result = json.decodeFromString<RegisterAccountResponse>(response.body.string())
|
||||
RegistrationNetworkResult.Success(result)
|
||||
}
|
||||
401 -> {
|
||||
RegistrationNetworkResult.Failure(RegisterAccountError.SessionNotFoundOrNotVerified(response.body.string()))
|
||||
}
|
||||
403 -> {
|
||||
RegistrationNetworkResult.Failure(RegisterAccountError.RegistrationRecoveryPasswordIncorrect(response.body.string()))
|
||||
}
|
||||
409 -> {
|
||||
RegistrationNetworkResult.Failure(RegisterAccountError.DeviceTransferPossible)
|
||||
}
|
||||
422 -> {
|
||||
RegistrationNetworkResult.Failure(RegisterAccountError.InvalidRequest(response.body.string()))
|
||||
}
|
||||
423 -> {
|
||||
val lockResponse = json.decodeFromString<RegistrationLockResponse>(response.body.string())
|
||||
RegistrationNetworkResult.Failure(RegisterAccountError.RegistrationLock(lockResponse))
|
||||
}
|
||||
429 -> {
|
||||
RegistrationNetworkResult.Failure(RegisterAccountError.RateLimited(response.retryAfter()))
|
||||
}
|
||||
else -> {
|
||||
RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFcmToken(): String? {
|
||||
return try {
|
||||
FcmUtil.getToken(context).orElse(null)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to get FCM token", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun awaitPushChallengeToken(): String? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val latch = java.util.concurrent.CountDownLatch(1)
|
||||
val challenge = java.util.concurrent.atomic.AtomicReference<String>()
|
||||
|
||||
val subscriber = object {
|
||||
@org.greenrobot.eventbus.Subscribe(threadMode = org.greenrobot.eventbus.ThreadMode.POSTING)
|
||||
fun onChallengeEvent(event: PushChallengeRequest.PushChallengeEvent) {
|
||||
challenge.set(event.challenge)
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
val eventBus = org.greenrobot.eventbus.EventBus.getDefault()
|
||||
eventBus.register(subscriber)
|
||||
try {
|
||||
latch.await(PUSH_REQUEST_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS)
|
||||
challenge.get()
|
||||
} finally {
|
||||
eventBus.unregister(subscriber)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to await push challenge token", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCaptchaUrl(): String {
|
||||
return BuildConfig.SIGNAL_CAPTCHA_URL
|
||||
}
|
||||
|
||||
override suspend fun restoreMasterKeyFromSvr(
|
||||
svrCredentials: SvrCredentials,
|
||||
pin: String
|
||||
): RegistrationNetworkResult<NetworkController.MasterKeyResponse, RestoreMasterKeyError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val authCredentials = AuthCredentials.create(svrCredentials.username, svrCredentials.password)
|
||||
val credentialSet = SvrAuthCredentialSet(svr2Credentials = authCredentials, svr3Credentials = null)
|
||||
|
||||
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(credentialSet, pin)
|
||||
RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
|
||||
} catch (e: SvrWrongPinException) {
|
||||
RegistrationNetworkResult.Failure(RestoreMasterKeyError.WrongPin(e.triesRemaining))
|
||||
} catch (e: SvrNoDataException) {
|
||||
RegistrationNetworkResult.Failure(RestoreMasterKeyError.NoDataFound)
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setPinAndMasterKeyOnSvr(
|
||||
pin: String,
|
||||
masterKey: MasterKey
|
||||
): RegistrationNetworkResult<SvrCredentials?, BackupMasterKeyError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val svr2 = AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE)
|
||||
val session = svr2.setPin(pin, masterKey)
|
||||
when (val response = session.execute()) {
|
||||
is BackupResponse.Success -> {
|
||||
RegistrationNetworkResult.Success(SvrCredentials(response.authorization.username(), response.authorization.password()))
|
||||
}
|
||||
is BackupResponse.EnclaveNotFound -> {
|
||||
RegistrationNetworkResult.Failure(BackupMasterKeyError.EnclaveNotFound)
|
||||
}
|
||||
is BackupResponse.ExposeFailure -> {
|
||||
RegistrationNetworkResult.Success(null)
|
||||
}
|
||||
is BackupResponse.NetworkError -> {
|
||||
RegistrationNetworkResult.NetworkError(response.exception)
|
||||
}
|
||||
is BackupResponse.ApplicationError -> {
|
||||
RegistrationNetworkResult.ApplicationError(response.exception)
|
||||
}
|
||||
is BackupResponse.ServerRejected -> {
|
||||
RegistrationNetworkResult.NetworkError(IOException("Server rejected backup request"))
|
||||
}
|
||||
is BackupResponse.RateLimited -> {
|
||||
RegistrationNetworkResult.NetworkError(IOException("Rate limited"))
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun enqueueSvrGuessResetJob() {
|
||||
AppDependencies.jobManager.add(ResetSvrGuessCountJob())
|
||||
}
|
||||
|
||||
override suspend fun enableRegistrationLock(): RegistrationNetworkResult<Unit, SetRegistrationLockError> = withContext(Dispatchers.IO) {
|
||||
val masterKey = SignalStore.svr.masterKey
|
||||
if (masterKey == null) {
|
||||
return@withContext RegistrationNetworkResult.Failure(SetRegistrationLockError.NoPinSet)
|
||||
}
|
||||
|
||||
when (val result = SignalNetwork.account.enableRegistrationLock(masterKey.deriveRegistrationLock())) {
|
||||
is NetworkResult.Success -> RegistrationNetworkResult.Success(Unit)
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (result.code) {
|
||||
401 -> RegistrationNetworkResult.Failure(SetRegistrationLockError.Unauthorized)
|
||||
422 -> RegistrationNetworkResult.Failure(SetRegistrationLockError.InvalidRequest(result.toString()))
|
||||
else -> RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${result.code}"))
|
||||
}
|
||||
}
|
||||
is NetworkResult.NetworkError -> RegistrationNetworkResult.NetworkError(result.exception)
|
||||
is NetworkResult.ApplicationError -> RegistrationNetworkResult.ApplicationError(result.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun disableRegistrationLock(): RegistrationNetworkResult<Unit, SetRegistrationLockError> = withContext(Dispatchers.IO) {
|
||||
when (val result = SignalNetwork.account.disableRegistrationLock()) {
|
||||
is NetworkResult.Success -> RegistrationNetworkResult.Success(Unit)
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (result.code) {
|
||||
401 -> RegistrationNetworkResult.Failure(SetRegistrationLockError.Unauthorized)
|
||||
else -> RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${result.code}"))
|
||||
}
|
||||
}
|
||||
is NetworkResult.NetworkError -> RegistrationNetworkResult.NetworkError(result.exception)
|
||||
is NetworkResult.ApplicationError -> RegistrationNetworkResult.ApplicationError(result.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSvrCredentials(): RegistrationNetworkResult<SvrCredentials, GetSvrCredentialsError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val svr2 = AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE)
|
||||
val auth = svr2.authorization()
|
||||
RegistrationNetworkResult.Success(SvrCredentials(auth.username(), auth.password()))
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkSvrCredentials(
|
||||
e164: String,
|
||||
credentials: List<SvrCredentials>
|
||||
): RegistrationNetworkResult<CheckSvrCredentialsResponse, CheckSvrCredentialsError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val tokens = credentials.map { "${it.username}:${it.password}" }
|
||||
pushServiceSocket.checkSvr2AuthCredentialsV2(e164, tokens).use { response ->
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val result = json.decodeFromString<CheckSvrCredentialsResponse>(response.body.string())
|
||||
RegistrationNetworkResult.Success(result)
|
||||
}
|
||||
400, 422 -> {
|
||||
RegistrationNetworkResult.Failure(CheckSvrCredentialsError.InvalidRequest(response.body.string()))
|
||||
}
|
||||
401 -> {
|
||||
RegistrationNetworkResult.Failure(CheckSvrCredentialsError.Unauthorized)
|
||||
}
|
||||
else -> {
|
||||
RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RegistrationNetworkResult.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RegistrationNetworkResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setAccountAttributes(
|
||||
attributes: AccountAttributes
|
||||
): RegistrationNetworkResult<Unit, SetAccountAttributesError> = withContext(Dispatchers.IO) {
|
||||
when (val result = SignalNetwork.account.setAccountAttributes(attributes.toServiceAccountAttributes())) {
|
||||
is NetworkResult.Success -> RegistrationNetworkResult.Success(Unit)
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (result.code) {
|
||||
401 -> RegistrationNetworkResult.Failure(SetAccountAttributesError.Unauthorized)
|
||||
422 -> RegistrationNetworkResult.Failure(SetAccountAttributesError.InvalidRequest(result.toString()))
|
||||
else -> RegistrationNetworkResult.ApplicationError(IllegalStateException("Unexpected response code: ${result.code}"))
|
||||
}
|
||||
}
|
||||
is NetworkResult.NetworkError -> RegistrationNetworkResult.NetworkError(result.exception)
|
||||
is NetworkResult.ApplicationError -> RegistrationNetworkResult.ApplicationError(result.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startProvisioning(): Flow<ProvisioningEvent> = callbackFlow {
|
||||
val socketHandles = mutableListOf<java.io.Closeable>()
|
||||
val configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration()
|
||||
|
||||
fun startSocket() {
|
||||
val handle = ProvisioningSocket.start<RegistrationProvisionMessage>(
|
||||
mode = ProvisioningSocket.Mode.REREG,
|
||||
identityKeyPair = IdentityKeyPair.generate(),
|
||||
configuration = configuration,
|
||||
handler = { id, t ->
|
||||
Log.w(TAG, "[startProvisioning] Socket [$id] failed", t)
|
||||
trySend(ProvisioningEvent.Error(t))
|
||||
}
|
||||
) { socket ->
|
||||
val url = socket.getProvisioningUrl()
|
||||
trySend(ProvisioningEvent.QrCodeReady(url))
|
||||
|
||||
val result = socket.getProvisioningMessageDecryptResult()
|
||||
|
||||
if (result is SecondaryProvisioningCipher.ProvisioningDecryptResult.Success) {
|
||||
val msg = result.message
|
||||
trySend(
|
||||
ProvisioningEvent.MessageReceived(
|
||||
ProvisioningMessage(
|
||||
accountEntropyPool = msg.accountEntropyPool,
|
||||
e164 = msg.e164,
|
||||
pin = msg.pin,
|
||||
aciIdentityKeyPair = IdentityKeyPair(IdentityKey(msg.aciIdentityKeyPublic.toByteArray()), ECPrivateKey(msg.aciIdentityKeyPrivate.toByteArray())),
|
||||
pniIdentityKeyPair = IdentityKeyPair(IdentityKey(msg.pniIdentityKeyPublic.toByteArray()), ECPrivateKey(msg.pniIdentityKeyPrivate.toByteArray())),
|
||||
platform = when (msg.platform) {
|
||||
RegistrationProvisionMessage.Platform.ANDROID -> ProvisioningMessage.Platform.ANDROID
|
||||
RegistrationProvisionMessage.Platform.IOS -> ProvisioningMessage.Platform.IOS
|
||||
},
|
||||
tier = when (msg.tier) {
|
||||
RegistrationProvisionMessage.Tier.FREE -> ProvisioningMessage.Tier.FREE
|
||||
RegistrationProvisionMessage.Tier.PAID -> ProvisioningMessage.Tier.PAID
|
||||
null -> null
|
||||
},
|
||||
backupTimestampMs = msg.backupTimestampMs,
|
||||
backupSizeBytes = msg.backupSizeBytes,
|
||||
restoreMethodToken = msg.restoreMethodToken,
|
||||
backupVersion = msg.backupVersion
|
||||
)
|
||||
)
|
||||
)
|
||||
channel.close()
|
||||
} else {
|
||||
Log.w(TAG, "[startProvisioning] Failed to decrypt provisioning message")
|
||||
trySend(ProvisioningEvent.Error(IOException("Failed to decrypt provisioning message")))
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(socketHandles) {
|
||||
socketHandles += handle
|
||||
if (socketHandles.size > 2) {
|
||||
socketHandles.removeAt(0).close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startSocket()
|
||||
|
||||
val rotationJob = launch {
|
||||
var count = 0
|
||||
while (count < 5 && isActive) {
|
||||
kotlinx.coroutines.delay(ProvisioningSocket.LIFESPAN / 2)
|
||||
if (isActive) {
|
||||
startSocket()
|
||||
count++
|
||||
Log.d(TAG, "[startProvisioning] Rotated socket, count: $count")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
rotationJob.cancel()
|
||||
synchronized(socketHandles) {
|
||||
socketHandles.forEach { it.close() }
|
||||
socketHandles.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AccountAttributes.toServiceAccountAttributes(): ServiceAccountAttributes {
|
||||
return ServiceAccountAttributes(
|
||||
signalingKey,
|
||||
registrationId,
|
||||
fetchesMessages,
|
||||
registrationLock,
|
||||
unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess,
|
||||
capabilities?.toServiceCapabilities(),
|
||||
discoverableByPhoneNumber,
|
||||
name,
|
||||
pniRegistrationId,
|
||||
recoveryPassword
|
||||
)
|
||||
}
|
||||
|
||||
private fun AccountAttributes.Capabilities.toServiceCapabilities(): ServiceAccountAttributes.Capabilities {
|
||||
return ServiceAccountAttributes.Capabilities(
|
||||
storage,
|
||||
versionedExpirationTimer,
|
||||
attachmentBackfill,
|
||||
spqr
|
||||
)
|
||||
}
|
||||
|
||||
private fun PreKeyCollection.toServicePreKeyCollection(): ServicePreKeyCollection {
|
||||
return ServicePreKeyCollection(
|
||||
identityKey = identityKey,
|
||||
signedPreKey = signedPreKey,
|
||||
lastResortKyberPreKey = lastResortKyberPreKey
|
||||
)
|
||||
}
|
||||
|
||||
private fun okhttp3.Response.retryAfter(): Duration {
|
||||
return this.header("Retry-After")?.toLongOrNull()?.seconds ?: 0.seconds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.archive.LocalBackupRestoreProgress
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.registration.PreExistingRegistrationData
|
||||
import org.signal.registration.StorageController
|
||||
import org.signal.registration.proto.RegistrationData
|
||||
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
|
||||
import org.signal.registration.screens.restoreselection.ArchiveRestoreOption
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver
|
||||
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistrationMetadata
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Implementation of [StorageController] that bridges to the app's existing storage infrastructure.
|
||||
*/
|
||||
class AppRegistrationStorageController(private val context: Context) : StorageController {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(AppRegistrationStorageController::class)
|
||||
private const val TEMP_PROTO_FILENAME = "registration-in-progress.proto"
|
||||
private val TEMP_PROTO_TIMEOUT = 15.minutes
|
||||
private val MODERN_BACKUP_PATTERN = Regex("^signal-backup-(\\d{4})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})$")
|
||||
private val LEGACY_BACKUP_PATTERN = Regex("^signal-(\\d{4})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})\\.backup$")
|
||||
}
|
||||
|
||||
override suspend fun getPreExistingRegistrationData(): PreExistingRegistrationData? = withContext(Dispatchers.IO) {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val aci = SignalStore.account.aci ?: return@withContext null
|
||||
val pni = SignalStore.account.pni ?: return@withContext null
|
||||
val e164 = SignalStore.account.e164 ?: return@withContext null
|
||||
val servicePassword = SignalStore.account.servicePassword ?: return@withContext null
|
||||
val aep = SignalStore.account.accountEntropyPool ?: return@withContext null
|
||||
|
||||
val aciIdentityKeyPair = SignalStore.account.aciIdentityKey
|
||||
val pniIdentityKeyPair = SignalStore.account.pniIdentityKey
|
||||
|
||||
PreExistingRegistrationData(
|
||||
e164 = e164,
|
||||
aci = aci,
|
||||
pni = pni,
|
||||
servicePassword = servicePassword,
|
||||
aep = aep,
|
||||
registrationLockEnabled = SignalStore.svr.isRegistrationLockEnabled,
|
||||
aciIdentityKeyPair = aciIdentityKeyPair,
|
||||
pniIdentityKeyPair = pniIdentityKeyPair
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearAllData() = withContext(Dispatchers.IO) {
|
||||
File(context.cacheDir, TEMP_PROTO_FILENAME).takeIf { it.exists() }?.delete()
|
||||
Unit
|
||||
}
|
||||
|
||||
override suspend fun readInProgressRegistrationData(): RegistrationData = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, TEMP_PROTO_FILENAME)
|
||||
if (file.exists()) {
|
||||
val age = System.currentTimeMillis() - file.lastModified()
|
||||
if (age > TEMP_PROTO_TIMEOUT.inWholeMilliseconds) {
|
||||
Log.w(TAG, "In-progress registration data is stale (${age}ms old), discarding.")
|
||||
file.delete()
|
||||
return@withContext RegistrationData()
|
||||
}
|
||||
|
||||
try {
|
||||
RegistrationData.ADAPTER.decode(file.readBytes())
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to decode registration data, returning empty.", e)
|
||||
RegistrationData()
|
||||
}
|
||||
} else {
|
||||
RegistrationData()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateInProgressRegistrationData(updater: RegistrationData.Builder.() -> Unit) = withContext(Dispatchers.IO) {
|
||||
val current = readInProgressRegistrationData()
|
||||
val updated = current.newBuilder().apply(updater).build()
|
||||
writeRegistrationData(updated)
|
||||
}
|
||||
|
||||
override suspend fun commitRegistrationData() = withContext(Dispatchers.IO) {
|
||||
val data = readInProgressRegistrationData()
|
||||
|
||||
// Build LocalRegistrationMetadata if we have enough data for account setup
|
||||
if (data.e164.isNotEmpty() && data.aci.isNotEmpty() && data.pni.isNotEmpty() && data.servicePassword.isNotEmpty()) {
|
||||
val profileKey = RegistrationRepository.getProfileKey(data.e164)
|
||||
|
||||
val metadata = LocalRegistrationMetadata.Builder().apply {
|
||||
if (data.aciIdentityKeyPair.size > 0) {
|
||||
aciIdentityKeyPair = data.aciIdentityKeyPair
|
||||
}
|
||||
if (data.pniIdentityKeyPair.size > 0) {
|
||||
pniIdentityKeyPair = data.pniIdentityKeyPair
|
||||
}
|
||||
if (data.aciSignedPreKey.size > 0) {
|
||||
aciSignedPreKey = data.aciSignedPreKey
|
||||
}
|
||||
if (data.pniSignedPreKey.size > 0) {
|
||||
pniSignedPreKey = data.pniSignedPreKey
|
||||
}
|
||||
if (data.aciLastResortKyberPreKey.size > 0) {
|
||||
aciLastRestoreKyberPreKey = data.aciLastResortKyberPreKey
|
||||
}
|
||||
if (data.pniLastResortKyberPreKey.size > 0) {
|
||||
pniLastRestoreKyberPreKey = data.pniLastResortKyberPreKey
|
||||
}
|
||||
|
||||
aci = data.aci
|
||||
pni = data.pni
|
||||
e164 = data.e164
|
||||
this.servicePassword = data.servicePassword
|
||||
this.profileKey = profileKey.serialize().toByteString()
|
||||
hasPin = data.pin.isNotEmpty()
|
||||
if (data.pin.isNotEmpty()) {
|
||||
pin = data.pin
|
||||
}
|
||||
if (data.temporaryMasterKey.size > 0) {
|
||||
masterKey = data.temporaryMasterKey
|
||||
}
|
||||
fcmEnabled = SignalStore.account.fcmEnabled
|
||||
fcmToken = SignalStore.account.fcmToken ?: ""
|
||||
reglockEnabled = data.registrationLockEnabled
|
||||
}.build()
|
||||
|
||||
// TODO [greyson] Should probably move this stuff into this file as we get closer to being done
|
||||
RegistrationRepository.registerAccountLocally(context, metadata)
|
||||
SignalStore.registration.localRegistrationMetadata = metadata
|
||||
|
||||
if (data.accountEntropyPool.isNotEmpty()) {
|
||||
SignalStore.account.restoreAccountEntropyPool(AccountEntropyPool(data.accountEntropyPool))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle PIN/master key
|
||||
if (data.pin.isNotEmpty() && data.temporaryMasterKey.size > 0) {
|
||||
val masterKey = MasterKey(data.temporaryMasterKey.toByteArray())
|
||||
SvrRepository.onRegistrationComplete(
|
||||
masterKey,
|
||||
data.pin,
|
||||
true,
|
||||
data.registrationLockEnabled,
|
||||
data.accountEntropyPool.isNotEmpty()
|
||||
)
|
||||
}
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
override suspend fun getAvailableRestoreOptions(): Set<ArchiveRestoreOption> = withContext(Dispatchers.IO) {
|
||||
// TODO [greyson] Real options
|
||||
val options = mutableSetOf<ArchiveRestoreOption>()
|
||||
|
||||
options.add(ArchiveRestoreOption.LocalBackup)
|
||||
options.add(ArchiveRestoreOption.DeviceTransfer)
|
||||
|
||||
options
|
||||
}
|
||||
|
||||
override fun restoreLocalBackupV1(uri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> = flow {
|
||||
// TODO [greyson] better progress
|
||||
Log.d(TAG, "Starting V1 local backup restore from: $uri")
|
||||
|
||||
emit(LocalBackupRestoreProgress.Preparing)
|
||||
|
||||
try {
|
||||
if (!FullBackupImporter.validatePassphrase(context, uri, passphrase)) {
|
||||
emit(LocalBackupRestoreProgress.Error(IllegalArgumentException("Invalid passphrase")))
|
||||
return@flow
|
||||
}
|
||||
|
||||
val database = SignalDatabase.backupDatabase
|
||||
FullBackupImporter.importFile(
|
||||
context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
database,
|
||||
uri,
|
||||
passphrase,
|
||||
SignalStore.registration.localRegistrationMetadata != null
|
||||
)
|
||||
|
||||
SignalDatabase.runPostBackupRestoreTasks(database)
|
||||
|
||||
emit(LocalBackupRestoreProgress.Complete)
|
||||
Log.d(TAG, "V1 restore complete.")
|
||||
} catch (e: FullBackupImporter.DatabaseDowngradeException) {
|
||||
Log.w(TAG, "V1 restore failed: database downgrade", e)
|
||||
emit(LocalBackupRestoreProgress.Error(e))
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "V1 restore failed", e)
|
||||
emit(LocalBackupRestoreProgress.Error(e))
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
override fun restoreLocalBackupV2(rootUri: Uri, backupUri: Uri, aep: AccountEntropyPool): Flow<LocalBackupRestoreProgress> = flow {
|
||||
// TODO [greyson] better progress
|
||||
Log.d(TAG, "Starting V2 local backup restore from backup=$backupUri, root=$rootUri")
|
||||
|
||||
emit(LocalBackupRestoreProgress.Preparing)
|
||||
|
||||
try {
|
||||
val backupDir = DocumentFile.fromTreeUri(context, backupUri)
|
||||
if (backupDir == null || !backupDir.canRead()) {
|
||||
emit(LocalBackupRestoreProgress.Error(IllegalStateException("Could not open backup directory")))
|
||||
return@flow
|
||||
}
|
||||
|
||||
val selfAci = SignalStore.account.aci
|
||||
val selfPni = SignalStore.account.pni
|
||||
val selfE164 = SignalStore.account.e164
|
||||
|
||||
if (selfAci == null || selfPni == null || selfE164 == null) {
|
||||
emit(LocalBackupRestoreProgress.Error(IllegalStateException("Account not registered, cannot restore V2 backup")))
|
||||
return@flow
|
||||
}
|
||||
|
||||
val selfData = BackupRepository.SelfData(selfAci, selfPni, selfE164, ProfileKeyUtil.getSelfProfileKey())
|
||||
val messageBackupKey = aep.deriveMessageBackupKey()
|
||||
val snapshotFileSystem = SnapshotFileSystem(context, backupDir)
|
||||
|
||||
when (val result = LocalArchiver.import(snapshotFileSystem, selfData, messageBackupKey)) {
|
||||
is org.signal.core.util.Result.Success -> {
|
||||
emit(LocalBackupRestoreProgress.Complete)
|
||||
Log.d(TAG, "V2 restore complete.")
|
||||
}
|
||||
is org.signal.core.util.Result.Failure -> {
|
||||
Log.w(TAG, "V2 restore failed: ${result.failure}")
|
||||
emit(LocalBackupRestoreProgress.Error(IOException("V2 restore failed: ${result.failure}")))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "V2 restore failed", e)
|
||||
emit(LocalBackupRestoreProgress.Error(e))
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
override suspend fun scanLocalBackupFolder(folderUri: Uri): List<LocalBackupInfo> = withContext(Dispatchers.IO) {
|
||||
val folder = DocumentFile.fromTreeUri(context, folderUri) ?: return@withContext emptyList()
|
||||
val children = folder.listFiles()
|
||||
|
||||
// If the selected folder contains a SignalBackups directory, use that instead
|
||||
val signalBackupsDir = children.firstOrNull { it.isDirectory && it.name == "SignalBackups" }
|
||||
val effectiveChildren = if (signalBackupsDir != null) {
|
||||
Log.d(TAG, "Found SignalBackups directory, using it as the effective folder")
|
||||
signalBackupsDir.listFiles()
|
||||
} else {
|
||||
children
|
||||
}
|
||||
|
||||
val backups = mutableListOf<LocalBackupInfo>()
|
||||
|
||||
// Check for modern backups: requires a 'files' directory and signal-backup-* directories
|
||||
val hasFilesDir = effectiveChildren.any { it.isDirectory && it.name == "files" }
|
||||
if (hasFilesDir) {
|
||||
for (child in effectiveChildren) {
|
||||
if (!child.isDirectory) continue
|
||||
val name = child.name ?: continue
|
||||
val match = MODERN_BACKUP_PATTERN.matchEntire(name) ?: continue
|
||||
val (year, month, day, hour, minute, second) = match.destructured
|
||||
try {
|
||||
val date = LocalDateTime.of(year.toInt(), month.toInt(), day.toInt(), hour.toInt(), minute.toInt(), second.toInt())
|
||||
backups.add(
|
||||
LocalBackupInfo(
|
||||
type = LocalBackupInfo.BackupType.V2,
|
||||
date = date,
|
||||
name = name,
|
||||
uri = child.uri
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to parse date from modern backup name: $name", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for legacy backups: signal-yyyy-MM-dd-HH-mm-ss.backup files
|
||||
for (child in effectiveChildren) {
|
||||
if (!child.isFile) continue
|
||||
val name = child.name ?: continue
|
||||
val match = LEGACY_BACKUP_PATTERN.matchEntire(name) ?: continue
|
||||
val (year, month, day, hour, minute, second) = match.destructured
|
||||
try {
|
||||
val date = LocalDateTime.of(year.toInt(), month.toInt(), day.toInt(), hour.toInt(), minute.toInt(), second.toInt())
|
||||
backups.add(
|
||||
LocalBackupInfo(
|
||||
type = LocalBackupInfo.BackupType.V1,
|
||||
date = date,
|
||||
name = name,
|
||||
uri = child.uri,
|
||||
sizeBytes = child.length()
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to parse date from legacy backup name: $name", e)
|
||||
}
|
||||
}
|
||||
|
||||
backups.sortedByDescending { it.date }
|
||||
}
|
||||
|
||||
private suspend fun writeRegistrationData(data: RegistrationData) = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, TEMP_PROTO_FILENAME)
|
||||
file.writeBytes(RegistrationData.ADAPTER.encode(data))
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ object Environment {
|
||||
return !IS_INSTRUMENTATION && (BuildConfig.DEBUG || IS_NIGHTLY || IS_PERF || IS_STAGING)
|
||||
}
|
||||
|
||||
const val USE_NEW_REGISTRATION: Boolean = false
|
||||
|
||||
object Backups {
|
||||
@JvmStatic
|
||||
fun supportsGooglePlayBilling(): Boolean {
|
||||
|
||||
Reference in New Issue
Block a user