Add skip SMS flow.

main
Cody Henthorne 2023-02-22 11:03:10 -05:00 zatwierdzone przez Greyson Parrelli
rodzic a47e3900c1
commit 4f458a022f
19 zmienionych plików z 657 dodań i 131 usunięć

Wyświetl plik

@ -24,7 +24,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.multidex.MultiDexApplication;
import com.google.android.gms.security.ProviderInstaller;
@ -63,6 +62,7 @@ import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshKbsCredentialsJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
@ -75,7 +75,6 @@ import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -104,13 +103,10 @@ import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWra
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.Security;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.core.CompletableObserver;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
@ -202,6 +198,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(this::initializeTrimThreadsByDateManager)
.addPostRender(RefreshKbsCredentialsJob::enqueueIfNecessary)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))

Wyświetl plik

@ -162,6 +162,7 @@ public final class JobManagerFactories {
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
put(RefreshKbsCredentialsJob.KEY, new RefreshKbsCredentialsJob.Factory());
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory());

Wyświetl plik

@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -83,15 +84,15 @@ public class RefreshAttributesJob extends BaseJob {
return;
}
int registrationId = SignalStore.account().getRegistrationId();
boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced();
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
String registrationLockV1 = null;
String registrationLockV2 = null;
KbsValues kbsValues = SignalStore.kbsValues();
int pniRegistrationId = new RegistrationRepository(ApplicationDependencies.getApplication()).getPniRegistrationId();
String registrationRecoveryPassword = kbsValues.getRegistrationRecoveryPassword();
int registrationId = SignalStore.account().getRegistrationId();
boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced();
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
String registrationLockV1 = null;
String registrationLockV2 = null;
KbsValues kbsValues = SignalStore.kbsValues();
int pniRegistrationId = new RegistrationRepository(ApplicationDependencies.getApplication()).getPniRegistrationId();
String recoveryPassword = kbsValues.getRecoveryPassword();
if (kbsValues.isV2RegistrationLockEnabled()) {
registrationLockV2 = kbsValues.getRegistrationLockToken();
@ -107,23 +108,27 @@ public class RefreshAttributesJob extends BaseJob {
AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut());
Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() +
"\n Recovery password? " + !TextUtils.isEmpty(recoveryPassword) +
"\n Phone number discoverable : " + phoneNumberDiscoverable +
"\n Device Name : " + (encryptedDeviceName != null) +
"\n Capabilities: " + capabilities);
SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
signalAccountManager.setAccountAttributes(null,
registrationId,
fetchesMessages,
registrationLockV1,
registrationLockV2,
unidentifiedAccessKey,
universalUnidentifiedAccess,
capabilities,
phoneNumberDiscoverable,
encryptedDeviceName,
pniRegistrationId,
registrationRecoveryPassword);
AccountAttributes accountAttributes = new AccountAttributes(
null,
registrationId,
fetchesMessages,
registrationLockV1,
registrationLockV2,
unidentifiedAccessKey,
universalUnidentifiedAccess,
capabilities,
phoneNumberDiscoverable,
(encryptedDeviceName == null) ? null : Base64.encodeBytes(encryptedDeviceName),
pniRegistrationId,
recoveryPassword
);
ApplicationDependencies.getSignalServiceAccountManager().setAccountAttributes(accountAttributes);
hasRefreshedThisAppCycle = true;
}

Wyświetl plik

@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import java.io.IOException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
/**
* Refresh KBS authentication credentials for talking to KBS during re-registration.
*/
class RefreshKbsCredentialsJob private constructor(parameters: Parameters) : BaseJob(parameters) {
companion object {
const val KEY = "RefreshKbsCredentialsJob"
private val TAG = Log.tag(RefreshKbsCredentialsJob::class.java)
private val FREQUENCY: Duration = 15.days
@JvmStatic
fun enqueueIfNecessary() {
val lastTimestamp = SignalStore.kbsValues().lastRefreshAuthTimestamp
if (lastTimestamp + FREQUENCY.inWholeMilliseconds < System.currentTimeMillis() || lastTimestamp > System.currentTimeMillis()) {
ApplicationDependencies.getJobManager().add(RefreshKbsCredentialsJob())
} else {
Log.d(TAG, "Do not need to refresh credentials. Last refresh: $lastTimestamp")
}
}
}
private constructor() : this(
parameters = Parameters.Builder()
.setQueue("RefreshKbsCredentials")
.addConstraint(NetworkConstraint.KEY)
.setMaxInstancesForQueue(2)
.setLifespan(1.days.inWholeMilliseconds)
.build()
)
override fun serialize(): Data = Data.Builder().build()
override fun getFactoryKey(): String = KEY
override fun onRun() {
KbsRepository().refreshAuthorization()
}
override fun onShouldRetry(e: Exception): Boolean {
return e is IOException && e !is NonSuccessfulResponseCodeException
}
override fun onFailure() = Unit
class Factory : Job.Factory<RefreshKbsCredentialsJob> {
override fun create(parameters: Parameters, data: Data): RefreshKbsCredentialsJob {
return RefreshKbsCredentialsJob(parameters)
}
}
}

Wyświetl plik

@ -12,6 +12,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -19,15 +20,16 @@ import java.util.stream.Stream;
public final class KbsValues extends SignalStoreValues {
public static final String V2_LOCK_ENABLED = "kbs.v2_lock_enabled";
private static final String MASTER_KEY = "kbs.registration_lock_master_key";
private static final String TOKEN_RESPONSE = "kbs.token_response";
private static final String PIN = "kbs.pin";
private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash";
private static final String LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp";
public static final String OPTED_OUT = "kbs.opted_out";
private static final String PIN_FORGOTTEN_OR_SKIPPED = "kbs.pin.forgotten.or.skipped";
private static final String KBS_AUTH_TOKENS = "kbs.kbs_auth_tokens";
public static final String V2_LOCK_ENABLED = "kbs.v2_lock_enabled";
private static final String MASTER_KEY = "kbs.registration_lock_master_key";
private static final String TOKEN_RESPONSE = "kbs.token_response";
private static final String PIN = "kbs.pin";
private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash";
private static final String LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp";
public static final String OPTED_OUT = "kbs.opted_out";
private static final String PIN_FORGOTTEN_OR_SKIPPED = "kbs.pin.forgotten.or.skipped";
private static final String KBS_AUTH_TOKENS = "kbs.kbs_auth_tokens";
private static final String KBS_LAST_AUTH_REFRESH_TIMESTAMP = "kbs.kbs_auth_tokens.last_refresh_timestamp";
KbsValues(KeyValueStore store) {
super(store);
@ -55,6 +57,8 @@ public final class KbsValues extends SignalStoreValues {
.remove(PIN)
.remove(LAST_CREATE_FAILED_TIMESTAMP)
.remove(OPTED_OUT)
.remove(KBS_AUTH_TOKENS)
.remove(KBS_LAST_AUTH_REFRESH_TIMESTAMP)
.commit();
}
@ -149,12 +153,12 @@ public final class KbsValues extends SignalStoreValues {
}
}
public synchronized @Nullable String getRegistrationRecoveryPassword() {
MasterKey masterKey = getPinBackedMasterKey();
if (masterKey == null) {
return null;
} else {
public synchronized @Nullable String getRecoveryPassword() {
MasterKey masterKey = getMasterKey();
if (masterKey != null && hasPin()) {
return masterKey.deriveRegistrationRecoveryPassword();
} else {
return null;
}
}
@ -180,6 +184,7 @@ public final class KbsValues extends SignalStoreValues {
public synchronized void putAuthTokenList(List<String> tokens) {
putList(KBS_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE);
setLastRefreshAuthTimestamp(System.currentTimeMillis());
}
public synchronized List<String> getKbsAuthTokenList() {
@ -202,6 +207,16 @@ public final class KbsValues extends SignalStoreValues {
}
}
public boolean removeAuthTokens(@NonNull List<String> invalid) {
List<String> tokens = new ArrayList<>(getKbsAuthTokenList());
if (tokens.removeAll(invalid)) {
putAuthTokenList(tokens);
return true;
}
return false;
}
/** Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}. */
public synchronized void optOut() {
getStore().beginWrite()
@ -229,4 +244,12 @@ public final class KbsValues extends SignalStoreValues {
throw new AssertionError(e);
}
}
private void setLastRefreshAuthTimestamp(long timestamp) {
putLong(KBS_LAST_AUTH_REFRESH_TIMESTAMP, timestamp);
}
public long getLastRefreshAuthTimestamp() {
return getLong(KBS_LAST_AUTH_REFRESH_TIMESTAMP, 0L);
}
}

Wyświetl plik

@ -63,6 +63,26 @@ public class KbsRepository {
}).subscribeOn(Schedulers.io());
}
/**
* Fetch and store a new KBS authorization.
*/
public void refreshAuthorization() throws IOException {
for (KbsEnclave enclave : KbsEnclaves.all()) {
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
try {
String authorization = kbs.getAuthorization();
backupAuthToken(authorization);
} catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 404) {
Log.i(TAG, "Enclave decommissioned, skipping", e);
} else {
throw e;
}
}
}
}
private @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException {
TokenData firstKnownTokenData = null;
@ -101,7 +121,7 @@ public class KbsRepository {
private static void backupAuthToken(String token) {
final boolean tokenIsNew = SignalStore.kbsValues().appendAuthTokenToList(token);
if (tokenIsNew) {
if (tokenIsNew && SignalStore.kbsValues().hasPin()) {
new BackupManager(ApplicationDependencies.getApplication()).dataChanged();
}
}

Wyświetl plik

@ -29,7 +29,6 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@ -44,7 +43,8 @@ public final class PinState {
public static synchronized void onRegistration(@NonNull Context context,
@Nullable KbsPinData kbsData,
@Nullable String pin,
boolean hasPinToRestore)
boolean hasPinToRestore,
boolean setRegistrationLockEnabled)
{
Log.i(TAG, "onRegistration()");
@ -57,9 +57,13 @@ public final class PinState {
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
} else if (kbsData != null && pin != null) {
Log.i(TAG, "Registration Lock V2");
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
if (setRegistrationLockEnabled) {
Log.i(TAG, "Registration Lock V2");
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
} else {
Log.i(TAG, "ReRegistration Skip SMS");
}
SignalStore.kbsValues().setKbsMasterKey(kbsData, pin);
SignalStore.pinValues().resetPinReminders();
resetPinRetryCount(context, pin);
@ -130,6 +134,7 @@ public final class PinState {
bestEffortRefreshAttributes();
} else {
Log.i(TAG, "Not the first time setting a PIN. Enclave: " + kbsEnclave.getEnclaveName());
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
}
}

Wyświetl plik

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.registration;
import android.app.Application;
import android.app.backup.BackupManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -41,6 +42,7 @@ import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.BackupAuthCheckProcessor;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException;
@ -95,12 +97,13 @@ public final class RegistrationRepository {
}
public Single<ServiceResponse<VerifyResponse>> registerAccount(@NonNull RegistrationData registrationData,
@NonNull VerifyResponse response)
@NonNull VerifyResponse response,
boolean setRegistrationLockEnabled)
{
return Single.<ServiceResponse<VerifyResponse>>fromCallable(() -> {
try {
String pin = response.getPin();
registerAccountInternal(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData());
registerAccountInternal(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData(), setRegistrationLockEnabled);
if (pin != null && !pin.isEmpty()) {
PinState.onPinChangedOrCreated(context, pin, SignalStore.pinValues().getKeyboardType());
@ -124,7 +127,8 @@ public final class RegistrationRepository {
private void registerAccountInternal(@NonNull RegistrationData registrationData,
@NonNull VerifyAccountResponse response,
@Nullable String pin,
@Nullable KbsPinData kbsData)
@Nullable KbsPinData kbsData,
boolean setRegistrationLockEnabled)
throws IOException
{
ACI aci = ACI.parseOrThrow(response.getUuid());
@ -172,7 +176,10 @@ public final class RegistrationRepository {
TextSecurePreferences.setPromptedPushRegistration(context, true);
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID);
PinState.onRegistration(context, kbsData, pin, hasPin);
PinState.onRegistration(context, kbsData, pin, hasPin, setRegistrationLockEnabled);
ApplicationDependencies.closeConnections();
ApplicationDependencies.getIncomingMessageObserver();
}
private void generateAndRegisterPreKeys(@NonNull ServiceIdType serviceIdType,
@ -210,8 +217,15 @@ public final class RegistrationRepository {
return null;
}
@Nullable
public String getRecoveryPassword() {
return SignalStore.kbsValues().getRegistrationRecoveryPassword();
public Single<BackupAuthCheckProcessor> getKbsAuthCredential(@NonNull RegistrationData registrationData) {
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword());
return accountManager.checkBackupAuthCredentials(registrationData.getE164(), SignalStore.kbsValues().getKbsAuthTokenList())
.map(BackupAuthCheckProcessor::new)
.doOnSuccess(processor -> {
if (SignalStore.kbsValues().removeAuthTokens(processor.getInvalid())) {
new BackupManager(context).dataChanged();
}
});
}
}

Wyświetl plik

@ -147,7 +147,7 @@ class VerifyAccountRepository(private val context: Application) {
}.subscribeOn(Schedulers.io())
}
fun registerAccount(sessionId: String, registrationData: RegistrationData, pin: String? = null, kbsPinDataProducer: KbsPinDataProducer? = null): Single<ServiceResponse<VerifyResponse>> {
fun registerAccount(sessionId: String?, registrationData: RegistrationData, pin: String? = null, kbsPinDataProducer: KbsPinDataProducer? = null): Single<ServiceResponse<VerifyResponse>> {
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)

Wyświetl plik

@ -44,7 +44,7 @@ public abstract class BaseRegistrationLockFragment extends LoggingFragment {
/**
* Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1.
*/
private static final int MINIMUM_PIN_LENGTH = 4;
public static final int MINIMUM_PIN_LENGTH = 4;
private EditText pinEntry;
private View forgotPin;

Wyświetl plik

@ -180,7 +180,7 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context);
if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
confirmNumberPrompt(context, e164number, () -> handleRequestVerification(context, true));
confirmNumberPrompt(context, e164number, () -> onE164EnteredSuccessfully(context, true));
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) {
confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context));
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) {
@ -192,10 +192,26 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
}
}
private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) {
private void onE164EnteredSuccessfully(@NonNull Context context, boolean fcmSupported) {
register.setSpinning();
disableAllEntries();
Disposable disposable = viewModel.canEnterSkipSmsFlow()
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturnItem(false)
.subscribe(canEnter -> {
if (canEnter) {
Log.i(TAG, "Enter skip flow");
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterPhoneNumberFragmentDirections.actionReRegisterWithPinFragment());
} else {
Log.i(TAG, "Unable to collect necessary data to enter skip flow, returning to normal");
handleRequestVerification(context, fcmSupported);
}
});
disposables.add(disposable);
}
private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) {
if (fcmSupported) {
SmsRetrieverClient client = SmsRetriever.getClient(context);
Task<Void> task = client.startSmsRetriever();
@ -378,7 +394,7 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.RegistrationActivity_missing_google_play_services)
.setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services)
.setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(context, false))
.setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> onE164EnteredSuccessfully(context, false))
.setNegativeButton(android.R.string.cancel, null)
.show();
}

Wyświetl plik

@ -0,0 +1,135 @@
package org.thoughtcrime.securesms.registration.fragments
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Using a recovery password or restored KBS token attempt to register in the skip flow.
*/
class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_lock) {
companion object {
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
}
private var _binding: FragmentRegistrationLockBinding? = null
private val binding: FragmentRegistrationLockBinding
get() = _binding!!
private val viewModel: RegistrationViewModel by activityViewModels()
private val disposables = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentRegistrationLockBinding.bind(view)
disposables.bindTo(viewLifecycleOwner.lifecycle)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.kbsLockPinTitle)
binding.kbsLockForgotPin.visibility = View.GONE
binding.kbsLockPinInput.imeOptions = EditorInfo.IME_ACTION_DONE
binding.kbsLockPinInput.setOnEditorActionListener { v, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v!!)
handlePinEntry()
return@setOnEditorActionListener true
}
false
}
enableAndFocusPinEntry()
binding.kbsLockPinConfirm.setOnClickListener {
ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput)
handlePinEntry()
}
binding.kbsLockKeyboardToggle.setOnClickListener { v: View? ->
val keyboardType: PinKeyboardType = getPinEntryKeyboardType()
updateKeyboard(keyboardType.other)
binding.kbsLockKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType))
}
val keyboardType: PinKeyboardType = getPinEntryKeyboardType().other
binding.kbsLockKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType))
}
private fun handlePinEntry() {
binding.kbsLockPinInput.isEnabled = false
val pin: String? = binding.kbsLockPinInput.text?.toString()
val trimmedLength = pin?.replace(" ", "")?.length ?: 0
if (trimmedLength == 0) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
if (trimmedLength < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) {
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
binding.kbsLockPinConfirm.setSpinning()
disposables += viewModel.verifyReRegisterWithPin(pin!!)
.subscribe { p ->
if (p.hasResult()) {
Log.i(TAG, "Successfully re-registered via skip flow")
findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment)
} else {
Log.w(TAG, "Unable to continue skip flow, resuming normal flow", p.error)
// todo handle the various error conditions
Toast.makeText(requireContext(), "retry or nav TODO ERROR See log", Toast.LENGTH_SHORT).show()
binding.kbsLockPinInput.isEnabled = true
}
}
}
private fun enableAndFocusPinEntry() {
binding.kbsLockPinInput.isEnabled = true
binding.kbsLockPinInput.isFocusable = true
if (binding.kbsLockPinInput.requestFocus()) {
ServiceUtil.getInputMethodManager(binding.kbsLockPinInput.context).showSoftInput(binding.kbsLockPinInput, 0)
}
}
private fun getPinEntryKeyboardType(): PinKeyboardType {
val isNumeric = binding.kbsLockPinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER
return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC
}
private fun updateKeyboard(keyboard: PinKeyboardType) {
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
binding.kbsLockPinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
binding.kbsLockPinInput.text.clear()
}
@StringRes
private fun resolveKeyboardToggleText(keyboard: PinKeyboardType): Int {
return if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
R.string.RegistrationLockFragment__enter_alphanumeric_pin
} else {
R.string.RegistrationLockFragment__enter_numeric_pin
}
}
}

Wyświetl plik

@ -42,6 +42,7 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
private static final String STATE_TIME_REMAINING = "TIME_REMAINING";
private static final String STATE_CAN_CALL_AT_TIME = "CAN_CALL_AT_TIME";
private static final String STATE_CAN_SMS_AT_TIME = "CAN_SMS_AT_TIME";
private static final String STATE_RECOVERY_PASSWORD = "RECOVERY_PASSWORD";
protected final SavedStateHandle savedState;
protected final VerifyAccountRepository verifyAccountRepository;
@ -62,9 +63,10 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
setInitialDefaultValue(STATE_VERIFICATION_CODE, "");
setInitialDefaultValue(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
setInitialDefaultValue(STATE_RECOVERY_PASSWORD, SignalStore.kbsValues().getRecoveryPassword());
}
protected <T> void setInitialDefaultValue(@NonNull String key, @NonNull T initialValue) {
protected <T> void setInitialDefaultValue(@NonNull String key, @Nullable T initialValue) {
if (!savedState.contains(key) || savedState.get(key) == null) {
savedState.set(key, initialValue);
}
@ -164,6 +166,14 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
savedState.set(STATE_KBS_TOKEN, tokenData);
}
public void setRecoveryPassword(@Nullable String recoveryPassword) {
savedState.set(STATE_RECOVERY_PASSWORD, recoveryPassword);
}
public @Nullable String getRecoveryPassword() {
return savedState.get(STATE_RECOVERY_PASSWORD);
}
public LiveData<Long> getLockedTimeRemaining() {
return savedState.getLiveData(STATE_TIME_REMAINING, 0L);
}

Wyświetl plik

@ -3,14 +3,21 @@ package org.thoughtcrime.securesms.registration.viewmodel;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.AbstractSavedStateViewModelFactory;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import androidx.savedstate.SavedStateRegistryOwner;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.PinHashing;
import org.thoughtcrime.securesms.pin.KbsRepository;
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException;
import org.thoughtcrime.securesms.pin.TokenData;
import org.thoughtcrime.securesms.registration.RegistrationData;
import org.thoughtcrime.securesms.registration.RegistrationRepository;
@ -19,13 +26,18 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
import org.thoughtcrime.securesms.registration.VerifyResponse;
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor;
import org.thoughtcrime.securesms.registration.VerifyResponseWithSuccessfulKbs;
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
@ -33,6 +45,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
public final class RegistrationViewModel extends BaseRegistrationViewModel {
private static final String TAG = Log.tag(RegistrationViewModel.class);
private static final String STATE_FCM_TOKEN = "FCM_TOKEN";
private static final String STATE_RESTORE_FLOW_SHOWN = "RESTORE_FLOW_SHOWN";
private static final String STATE_IS_REREGISTER = "IS_REREGISTER";
@ -113,7 +127,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
})
.observeOn(Schedulers.io())
.flatMap( processor -> {
.flatMap(processor -> {
if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) {
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), null, null);
} else {
@ -155,7 +169,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
}
})
.flatMap( processor -> {
.flatMap(processor -> {
if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) {
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> Objects.requireNonNull(KbsRepository.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse())));
} else {
@ -166,13 +180,13 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
@Override
protected Single<VerifyResponseProcessor> onVerifySuccess(@NonNull VerifyResponseProcessor processor) {
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult())
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), false)
.map(VerifyResponseWithoutKbs::new);
}
@Override
protected Single<VerifyResponseWithRegistrationLockProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin) {
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult())
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), true)
.map(processor::updatedIfRegistrationFailed);
}
@ -184,7 +198,169 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
registrationRepository.getProfileKey(getNumber().getE164Number()),
getFcmToken(),
registrationRepository.getPniRegistrationId(),
registrationRepository.getRecoveryPassword());
getRecoveryPassword());
}
public @NonNull Single<VerifyResponseProcessor> verifyReRegisterWithPin(@NonNull String pin) {
return Single.fromCallable(() -> verifyReRegisterWithPinInternal(pin))
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.flatMap(data -> {
if (data.canProceed) {
return verifyReRegisterWithRecoveryPassword(pin, data.pinData);
} else {
throw new IllegalStateException("Unable to get token or master key");
}
})
.onErrorReturn(t -> new VerifyResponseWithoutKbs(ServiceResponse.forUnknownError(t)))
.doOnSuccess(p -> {
if (p.hasResult()) {
restoreFromStorageService();
}
})
.observeOn(AndroidSchedulers.mainThread());
}
@WorkerThread
private @NonNull ReRegistrationData verifyReRegisterWithPinInternal(@NonNull String pin)
throws KeyBackupSystemWrongPinException, IOException, KeyBackupSystemNoDataException
{
String localPinHash = SignalStore.kbsValues().getLocalPinHash();
if (hasRecoveryPassword() && localPinHash != null && PinHashing.verifyLocalPinHash(localPinHash, pin)) {
Log.i(TAG, "Local pin matches input, attempting registration");
return ReRegistrationData.canProceed(new KbsPinData(SignalStore.kbsValues().getOrCreateMasterKey(), SignalStore.kbsValues().getRegistrationLockTokenResponse()));
} else {
TokenData data = getKeyBackupCurrentToken();
if (data == null) {
Log.w(TAG, "No token data, abort skip flow");
return ReRegistrationData.cannotProceed();
}
KbsPinData kbsPinData = KbsRepository.restoreMasterKey(pin, data.getEnclave(), data.getBasicAuth(), data.getTokenResponse());
if (kbsPinData == null || kbsPinData.getMasterKey() == null) {
Log.w(TAG, "No kbs data, abort skip flow");
return ReRegistrationData.cannotProceed();
}
setRecoveryPassword(kbsPinData.getMasterKey().deriveRegistrationRecoveryPassword());
return ReRegistrationData.canProceed(kbsPinData);
}
}
private Single<VerifyResponseProcessor> verifyReRegisterWithRecoveryPassword(@NonNull String pin, @NonNull KbsPinData pinData) {
RegistrationData registrationData = getRegistrationData();
if (registrationData.getRecoveryPassword() == null) {
throw new IllegalStateException("No valid recovery password");
}
return verifyAccountRepository.registerAccount(null, registrationData, null, null)
.onErrorReturn(ServiceResponse::forUnknownError)
.map(VerifyResponseWithoutKbs::new)
.flatMap(processor -> {
if (processor.registrationLock()) {
return verifyAccountRepository.registerAccount(null, registrationData, pin, () -> pinData)
.onErrorReturn(ServiceResponse::forUnknownError)
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, getKeyBackupCurrentToken()));
} else {
return Single.just(processor);
}
})
.<VerifyResponseProcessor>flatMap(processor -> {
if (processor.hasResult()) {
VerifyResponse verifyResponse = processor.getResult();
boolean setRegistrationLockEnabled = verifyResponse.getKbsData() != null;
if (!setRegistrationLockEnabled) {
verifyResponse = new VerifyResponse(processor.getResult().getVerifyAccountResponse(), pinData, pin);
}
return registrationRepository.registerAccount(registrationData, verifyResponse, setRegistrationLockEnabled)
.map(r -> {
return setRegistrationLockEnabled ? new VerifyResponseWithRegistrationLockProcessor(r, getKeyBackupCurrentToken())
: new VerifyResponseWithoutKbs(r);
});
} else {
return Single.just(processor);
}
})
.observeOn(AndroidSchedulers.mainThread());
}
public @NonNull Single<Boolean> canEnterSkipSmsFlow() {
return Single.just(hasRecoveryPassword())
.flatMap(hasRecoveryPassword -> {
if (hasRecoveryPassword) {
Log.d(TAG, "Have valid recovery password but still checking kbs credentials as a backup");
return checkForValidKbsAuthCredentials().map(unused -> true);
} else {
return checkForValidKbsAuthCredentials();
}
});
}
private Single<Boolean> checkForValidKbsAuthCredentials() {
return registrationRepository.getKbsAuthCredential(getRegistrationData())
.flatMap(p -> {
if (p.getValid() != null) {
return kbsRepository.getToken(p.getValid())
.flatMap(r -> {
if (r.getResult().isPresent()) {
setKeyBackupTokenData(r.getResult().get());
return Single.just(true);
} else {
return Single.just(false);
}
});
} else {
return Single.just(false);
}
})
.onErrorReturnItem(false)
.observeOn(AndroidSchedulers.mainThread());
}
private void restoreFromStorageService() {
SignalStore.onboarding().clearAll();
Stopwatch stopwatch = new Stopwatch("ReRegisterRestore");
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
stopwatch.split("AccountRestore");
ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJob(), TimeUnit.SECONDS.toMillis(10));
stopwatch.split("ContactRestore");
try {
FeatureFlags.refreshSync();
} catch (IOException e) {
Log.w(TAG, "Failed to refresh flags.", e);
}
stopwatch.split("FeatureFlags");
stopwatch.stop(TAG);
}
private boolean hasRecoveryPassword() {
return getRecoveryPassword() != null && Objects.equals(getRegistrationData().getE164(), SignalStore.account().getE164());
}
private static class ReRegistrationData {
public boolean canProceed;
public KbsPinData pinData;
private ReRegistrationData(boolean canProceed, @Nullable KbsPinData pinData) {
this.canProceed = canProceed;
this.pinData = pinData;
}
public static ReRegistrationData cannotProceed() {
return new ReRegistrationData(false, null);
}
public static ReRegistrationData canProceed(@NonNull KbsPinData pinData) {
return new ReRegistrationData(true, pinData);
}
}
public static final class Factory extends AbstractSavedStateViewModelFactory {

Wyświetl plik

@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:viewBindingIgnore="true">
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

Wyświetl plik

@ -110,13 +110,21 @@
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_reRegisterWithPinFragment"
app:destination="@id/reRegisterWithPinFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/countryPickerFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
android:label="fragment_country_picker"
tools:layout="@layout/fragment_registration_country_picker"/>
tools:layout="@layout/fragment_registration_country_picker" />
<fragment
android:id="@+id/enterCodeFragment"
@ -191,6 +199,31 @@
</fragment>
<fragment
android:id="@+id/reRegisterWithPinFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment"
tools:layout="@layout/fragment_registration_lock">
<action
android:id="@+id/action_reRegisterWithPinFragment_to_enterCodeFragment"
app:destination="@id/enterCodeFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/enterPhoneNumberFragment"/>
<action
android:id="@+id/action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment"
app:destination="@id/registrationCompletePlaceHolderFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/accountLockedFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
@ -201,7 +234,7 @@
android:id="@+id/captchaFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
android:label="fragment_captcha"
tools:layout="@layout/fragment_registration_captcha"/>
tools:layout="@layout/fragment_registration_captcha" />
<fragment
android:id="@+id/restoreBackupFragment"

Wyświetl plik

@ -56,6 +56,8 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher;
import org.whispersystems.signalservice.internal.push.AuthCredentials;
import org.whispersystems.signalservice.internal.push.BackupAuthCheckRequest;
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse;
import org.whispersystems.signalservice.internal.push.CdsiAuthResponse;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
@ -73,9 +75,11 @@ import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -93,6 +97,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -206,6 +211,22 @@ public class SignalServiceAccountManager {
}
}
public Single<ServiceResponse<BackupAuthCheckResponse>> checkBackupAuthCredentials(@Nonnull String e164, @Nonnull List<String> basicAuthTokens) {
List<String> usernamePasswords = basicAuthTokens
.stream()
.limit(10)
.map(t -> {
try {
return new String(Base64.decode(t.replace("Basic ", "").trim()), StandardCharsets.ISO_8859_1);
} catch (IOException e) {
return null;
}
})
.collect(Collectors.toList());
return pushServiceSocket.checkBackupAuthCredentials(new BackupAuthCheckRequest(e164, usernamePasswords), DefaultResponseMapper.getDefault(BackupAuthCheckResponse.class));
}
/**
* Request a push challenge. A number will be pushed to the GCM (FCM) id. This can then be used
* during SMS/call requests to bypass the CAPTCHA.
@ -326,44 +347,12 @@ public class SignalServiceAccountManager {
/**
* Refresh account attributes with server.
*
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, concatenated.
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
* This value should remain consistent across registrations for the same
* install, but probabilistically differ across registrations for
* separate installs.
* @param pin Only supply if pin has not yet been migrated to KBS.
* @param registrationLock Only supply if found on KBS.
*
* @throws IOException
*/
public void setAccountAttributes(String signalingKey,
int signalProtocolRegistrationId,
boolean fetchesMessages,
String pin,
String registrationLock,
byte[] unidentifiedAccessKey,
boolean unrestrictedUnidentifiedAccess,
AccountAttributes.Capabilities capabilities,
boolean discoverableByPhoneNumber,
byte[] encryptedDeviceName,
int pniRegistrationId,
String recoveryPassword)
public void setAccountAttributes(@Nonnull AccountAttributes accountAttributes)
throws IOException
{
this.pushServiceSocket.setAccountAttributes(
signalingKey,
signalProtocolRegistrationId,
fetchesMessages,
pin,
registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess,
capabilities,
discoverableByPhoneNumber,
encryptedDeviceName,
pniRegistrationId,
recoveryPassword
);
this.pushServiceSocket.setAccountAttributes(accountAttributes);
}
/**

Wyświetl plik

@ -0,0 +1,46 @@
package org.whispersystems.signalservice.internal.push
import com.fasterxml.jackson.annotation.JsonCreator
import okio.ByteString.Companion.encode
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import java.nio.charset.StandardCharsets
/**
* Request body JSON for verifying stored KBS auth credentials.
*/
@Suppress("unused")
class BackupAuthCheckRequest @JsonCreator constructor(
val number: String?,
val passwords: List<String>
)
/**
* Verify KBS auth credentials JSON response.
*/
data class BackupAuthCheckResponse @JsonCreator constructor(
private val matches: Map<String, Map<String, Any>>
) {
private val actualMatches = matches["matches"] ?: emptyMap()
val match: String? = actualMatches.entries.firstOrNull { it.value.toString() == "match" }?.key?.toBasic()
val invalid: List<String> = actualMatches.filterValues { it.toString() == "invalid" }.keys.map { it.toBasic() }
/** Server expects and returns values as <username>:<password> but we prefer the full encoded Basic auth header format */
private fun String.toBasic(): String {
return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}"
}
}
/**
* Processes a response from the verify stored KBS auth credentials request.
*/
class BackupAuthCheckProcessor(response: ServiceResponse<BackupAuthCheckResponse>) : ServiceResponseProcessor<BackupAuthCheckResponse>(response) {
fun getInvalid(): List<String> {
return response.result.map { it.invalid }.orElse(emptyList())
}
fun getValid(): String? {
return response.result.map { it.match }.orElse(null)
}
}

Wyświetl plik

@ -286,6 +286,8 @@ public class PushServiceSocket {
private static final String REPORT_SPAM = "/v1/messages/report/%s/%s";
private static final String BACKUP_AUTH_CHECK = "/v1/backup/auth/check";
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
@ -428,39 +430,13 @@ public class PushServiceSocket {
return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
}
public void setAccountAttributes(String signalingKey,
int registrationId,
boolean fetchesMessages,
String pin,
String registrationLock,
byte[] unidentifiedAccessKey,
boolean unrestrictedUnidentifiedAccess,
AccountAttributes.Capabilities capabilities,
boolean discoverableByPhoneNumber,
byte[] encryptedDeviceName,
int pniRegistrationId,
String recoveryPassword)
public void setAccountAttributes(@Nonnull AccountAttributes accountAttributes)
throws IOException
{
if (registrationLock != null && pin != null) {
if (accountAttributes.getRegistrationLock() != null && accountAttributes.getPin() != null) {
throw new AssertionError("Pin should be null if registrationLock is set.");
}
String name = (encryptedDeviceName == null) ? null : Base64.encodeBytes(encryptedDeviceName);
AccountAttributes accountAttributes = new AccountAttributes(signalingKey,
registrationId,
fetchesMessages,
pin,
registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess,
capabilities,
discoverableByPhoneNumber,
name,
pniRegistrationId,
recoveryPassword);
makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes));
}
@ -929,6 +905,22 @@ public class PushServiceSocket {
.onErrorReturn(ServiceResponse::forUnknownError);
}
public Single<ServiceResponse<BackupAuthCheckResponse>> checkBackupAuthCredentials(@Nonnull BackupAuthCheckRequest request,
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
{
Single<ServiceResponse<BackupAuthCheckResponse>> requestSingle = Single.fromCallable(() -> {
try (Response response = getServiceConnection(BACKUP_AUTH_CHECK, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) {
String body = response.body() != null ? readBodyString(response.body()): "";
return responseMapper.map(response.code(), body, response::header, false);
}
});
return requestSingle
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.onErrorReturn(ServiceResponse::forUnknownError);
}
/**
* GET /v1/accounts/username_hash/{usernameHash}
*