Update registration to allow PIN entry.

master
Greyson Parrelli 2020-04-07 13:19:53 -04:00
parent 6b37675a81
commit acbfff89d3
46 changed files with 1206 additions and 161 deletions

View File

@ -475,7 +475,12 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<activity android:name=".pin.PinRestoreActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".service.IncomingMessageObserver$ForegroundService"/>

View File

@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
@ -32,15 +33,17 @@ import java.util.Locale;
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
public static final String LOCALE_EXTRA = "locale_extra";
public static final String LOCALE_EXTRA = "locale_extra";
public static final String NEXT_INTENT_EXTRA = "next_intent";
private static final int STATE_NORMAL = 0;
private static final int STATE_CREATE_PASSPHRASE = 1;
private static final int STATE_PROMPT_PASSPHRASE = 2;
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
private static final int STATE_WELCOME_PUSH_SCREEN = 4;
private static final int STATE_CREATE_PROFILE_NAME = 5;
private static final int STATE_CREATE_KBS_PIN = 6;
private static final int STATE_ENTER_SIGNAL_PIN = 5;
private static final int STATE_CREATE_PROFILE_NAME = 6;
private static final int STATE_CREATE_SIGNAL_PIN = 7;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@ -155,7 +158,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
case STATE_CREATE_KBS_PIN: return getCreateKbsPinIntent();
case STATE_ENTER_SIGNAL_PIN: return getEnterSignalPinIntent();
case STATE_CREATE_SIGNAL_PIN: return getCreateSignalPinIntent();
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
default: return null;
}
@ -170,21 +174,23 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (userMustSetKbsPin()) {
return STATE_CREATE_KBS_PIN;
} else if (userMustCreateSignalPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else {
return STATE_NORMAL;
}
}
private boolean userMustSetKbsPin() {
private boolean userMustCreateSignalPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin();
}
private boolean userMustSetProfileName() {
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName() == ProfileName.EMPTY;
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
}
private Intent getCreatePassphraseIntent() {
@ -206,7 +212,11 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
}
private Intent getCreateKbsPinIntent() {
private Intent getEnterSignalPinIntent() {
return getRoutedIntent(PinRestoreActivity.class, getIntent());
}
private Intent getCreateSignalPinIntent() {
final Intent intent;
if (userMustSetProfileName()) {
@ -252,4 +262,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
clearKeyReceiver = null;
}
}
/**
* Puts an extra in {@code intent} so that {@code nextIntent} will be shown after it.
*/
public static @NonNull Intent chainIntent(@NonNull Intent intent, @NonNull Intent nextIntent) {
intent.putExtra(NEXT_INTENT_EXTRA, nextIntent);
return intent;
}
}

View File

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
@ -148,19 +149,10 @@ public class HelpFragment extends Fragment {
.map(view -> Feeling.getByViewId(view.getId()))
.findFirst().orElse(null);
Spanned body = getEmailBody(debugLog, feeling);
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:"));
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{getString(R.string.RegistrationActivity_support_email)});
intent.putExtra(Intent.EXTRA_SUBJECT, getEmailSubject());
intent.putExtra(Intent.EXTRA_TEXT, body.toString());
if (IntentUtils.isResolvable(requireContext(), intent)) {
startActivity(intent);
} else {
Toast.makeText(requireContext(), R.string.HelpFragment__no_email_app_found, Toast.LENGTH_LONG).show();
}
CommunicationActions.openEmail(requireContext(),
getString(R.string.RegistrationActivity_support_email),
getEmailSubject(),
getEmailBody(debugLog, feeling).toString());
}
private String getEmailSubject() {

View File

@ -49,6 +49,11 @@ public class RefreshAttributesJob extends BaseJob {
@Override
public void onRun() throws IOException {
if (!TextSecurePreferences.isPushRegistered(context)) {
Log.w(TAG, "Not yet registered. Skipping.");
return;
}
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context);
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());

View File

@ -6,7 +6,9 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.util.Map;
@ -14,6 +16,8 @@ import java.util.concurrent.TimeUnit;
public class RemoteConfigRefreshJob extends BaseJob {
private static final String TAG = Log.tag(RemoteConfigRefreshJob.class);
public static final String KEY = "RemoteConfigRefreshJob";
public RemoteConfigRefreshJob() {
@ -41,6 +45,11 @@ public class RemoteConfigRefreshJob extends BaseJob {
@Override
protected void onRun() throws Exception {
if (!TextSecurePreferences.isPushRegistered(context)) {
Log.w(TAG, "Not registered. Skipping.");
return;
}
Map<String, Boolean> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
FeatureFlags.update(config);
}

View File

@ -51,6 +51,11 @@ public class RotateCertificateJob extends BaseJob {
@Override
public void onRun() throws IOException {
if (!TextSecurePreferences.isPushRegistered(context)) {
Log.w(TAG, "Not yet registered. Ignoring.");
return;
}
synchronized (RotateCertificateJob.class) {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
byte[] certificate = accountManager.getSenderCertificate();

View File

@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
@ -33,7 +34,7 @@ public class StorageAccountRestoreJob extends BaseJob {
public static String KEY = "StorageAccountRestoreJob";
public static long LIFESPAN = TimeUnit.SECONDS.toMillis(10);
public static long LIFESPAN = TimeUnit.SECONDS.toMillis(20);
private static final String TAG = Log.tag(StorageAccountRestoreJob.class);
@ -69,7 +70,8 @@ public class StorageAccountRestoreJob extends BaseJob {
Optional<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
if (!manifest.isPresent()) {
Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring.");
Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring. Force-pushing.");
ApplicationDependencies.getJobManager().add(new StorageForcePushJob());
return;
}
@ -97,16 +99,13 @@ public class StorageAccountRestoreJob extends BaseJob {
StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId());
StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord);
JobManager jobManager = ApplicationDependencies.getJobManager();
if (accountRecord.getAvatarUrlPath().isPresent()) {
RetrieveProfileAvatarJob avatarJob = new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get());
try {
avatarJob.setContext(context);
avatarJob.onRun();
} catch (IOException e) {
Log.w(TAG, "Failed to download avatar. Scheduling for later.");
ApplicationDependencies.getJobManager().add(avatarJob);
}
jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()), LIFESPAN/2);
}
jobManager.runSynchronously(new RefreshAttributesJob(), LIFESPAN/2);
}
@Override

View File

@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidKeyException;
@ -78,20 +78,26 @@ public class StorageForcePushJob extends BaseJob {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
long currentVersion = accountManager.getStorageManifestVersion();
Map<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap();
long currentVersion = accountManager.getStorageManifestVersion();
Map<RecipientId, StorageId> oldContactStorageIds = recipientDatabase.getContactStorageSyncIdsMap();
long newVersion = currentVersion + 1;
Map<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
List<SignalStorageRecord> inserts = Stream.of(oldStorageKeys.keySet())
.map(recipientDatabase::getRecipientSettings)
.withoutNulls()
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw(), archivedRecipients))
.toList();
inserts.add(StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId())));
long newVersion = currentVersion + 1;
Map<RecipientId, StorageId> newContactStorageIds = generateContactStorageIds(oldContactStorageIds);
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
List<SignalStorageRecord> inserts = Stream.of(oldContactStorageIds.keySet())
.map(recipientDatabase::getRecipientSettings)
.withoutNulls()
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw(), archivedRecipients))
.toList();
SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newStorageKeys.values()));
SignalStorageRecord accountRecord = StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId()));
List<StorageId> allNewStorageIds = new ArrayList<>(newContactStorageIds.values());
inserts.add(accountRecord);
allNewStorageIds.add(accountRecord.getId());
SignalStorageManifest manifest = new SignalStorageManifest(newVersion, allNewStorageIds);
StorageSyncValidations.validateForcePush(manifest, inserts);
try {
if (newVersion > 1) {
@ -114,7 +120,8 @@ public class StorageForcePushJob extends BaseJob {
Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion);
TextSecurePreferences.setStorageManifestVersion(context, newVersion);
recipientDatabase.applyStorageIdUpdates(newStorageKeys);
recipientDatabase.applyStorageIdUpdates(newContactStorageIds);
recipientDatabase.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().getId(), accountRecord.getId()));
storageKeyDatabase.deleteAll();
}
@ -127,7 +134,7 @@ public class StorageForcePushJob extends BaseJob {
public void onFailure() {
}
private static @NonNull Map<RecipientId, StorageId> generateNewKeys(@NonNull Map<RecipientId, StorageId> oldKeys) {
private static @NonNull Map<RecipientId, StorageId> generateContactStorageIds(@NonNull Map<RecipientId, StorageId> oldKeys) {
Map<RecipientId, StorageId> out = new HashMap<>();
for (Map.Entry<RecipientId, StorageId> entry : oldKeys.entrySet()) {

View File

@ -28,6 +28,8 @@ public final class KbsValues {
/**
* Deliberately does not clear the {@link #MASTER_KEY}.
*
* Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}
*/
public void clearRegistrationLockAndPin() {
store.beginWrite()
@ -37,6 +39,7 @@ public final class KbsValues {
.commit();
}
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */
public synchronized void setKbsMasterKey(@NonNull KbsPinData pinData, @NonNull String localPinHash) {
MasterKey masterKey = pinData.getMasterKey();
String tokenResponse;
@ -53,6 +56,7 @@ public final class KbsValues {
.commit();
}
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */
public synchronized void setV2RegistrationLockEnabled(boolean enabled) {
store.beginWrite().putBoolean(V2_LOCK_ENABLED, enabled).apply();
}

View File

@ -85,6 +85,7 @@ public final class PinValues {
return PinKeyboardType.fromCode(store.getString(KEYBOARD_TYPE, null));
}
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState} */
public void setPinState(@NonNull String pinState) {
store.beginWrite().putString(PIN_STATE, pinState).commit();
}

View File

@ -10,7 +10,8 @@ import java.security.SecureRandom;
public class StorageServiceValues {
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
private static final String NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore";
private final KeyValueStore store;
@ -29,4 +30,12 @@ public class StorageServiceValues {
public void onSyncCompleted() {
store.beginWrite().putLong(LAST_SYNC_TIME, System.currentTimeMillis()).apply();
}
public boolean needsAccountRestore() {
return store.getBoolean(NEEDS_ACCOUNT_RESTORE, false);
}
public void setNeedsAccountRestore(boolean value) {
store.beginWrite().putBoolean(NEEDS_ACCOUNT_RESTORE, value).apply();
}
}

View File

@ -180,7 +180,7 @@ public final class RegistrationLockV1Dialog {
protected Boolean doInBackground(Void... voids) {
try {
Log.i(TAG, "Setting pin on KBS - dialog");
PinState.onCompleteRegistrationLockV1Reminder(context, pinValue);
PinState.onEnableLegacyRegistrationLockPreference(context, pinValue);
Log.i(TAG, "Pin set on KBS");
return true;
} catch (IOException | UnauthenticatedResponseException e) {
@ -235,7 +235,7 @@ public final class RegistrationLockV1Dialog {
@Override
protected Boolean doInBackground(Void... voids) {
try {
PinState.onDisableRegistrationLockV1(context);
PinState.onDisableLegacyRegistrationLockPreference(context);
return true;
} catch (IOException | UnauthenticatedResponseException e) {
Log.w(TAG, e);

View File

@ -20,6 +20,8 @@ import org.thoughtcrime.securesms.animation.AnimationRepeatListener;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.SpanUtil;
import java.util.Objects;
@ -109,7 +111,8 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
public void onAnimationEnd(Animator animation) {
requireActivity().setResult(Activity.RESULT_OK);
closeNavGraphBranch();
SignalStore.registrationValues().setRegistrationComplete();
RegistrationUtil.markRegistrationPossiblyComplete();
StorageSyncHelper.scheduleSyncForDataChange();
}
});
break;
@ -117,7 +120,7 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
SignalStore.registrationValues().setRegistrationComplete();
RegistrationUtil.markRegistrationPossiblyComplete();
displayFailedDialog();
}
});

View File

@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.logsubmit;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class LogSectionPin implements LogSection {
@Override
public @NonNull String getTitle() {
return "PIN STATE";
}
@Override
public @NonNull CharSequence getContent(@NonNull Context context) {
return new StringBuilder().append("State: ").append(SignalStore.pinValues().getPinState()).append("\n")
.append("Last Successful Reminder Entry: ").append(SignalStore.pinValues().getLastSuccessfulEntryTime()).append("\n")
.append("Next Reminder Interval: ").append(SignalStore.pinValues().getCurrentInterval()).append("\n")
.append("ReglockV1: ").append(TextSecurePreferences.isV1RegistrationLockEnabled(context)).append("\n")
.append("ReglockV2: ").append(SignalStore.kbsValues().isV2RegistrationLockEnabled()).append("\n")
.append("Signal PIN: ").append(SignalStore.kbsValues().hasPin()).append("\n")
.append("Needs Account Restore: ").append(SignalStore.storageServiceValues().needsAccountRestore()).append("\n")
.append("PIN Required at Registration: ").append(SignalStore.registrationValues().pinWasRequiredAtRegistration()).append("\n")
.append("Registration Complete: ").append(SignalStore.registrationValues().isRegistrationComplete());
}
}

View File

@ -58,6 +58,7 @@ public class SubmitDebugLogRepository {
if (Build.VERSION.SDK_INT >= 28) {
add(new LogSectionPower());
}
add(new LogSectionPin());
add(new LogSectionThreads());
add(new LogSectionFeatureFlags());
add(new LogSectionPermissions());

View File

@ -42,7 +42,7 @@ class PinsForAllSchedule implements MegaphoneSchedule {
}
private static boolean isEnabled() {
if (SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
if (SignalStore.kbsValues().hasPin()) {
return false;
}
@ -54,7 +54,7 @@ class PinsForAllSchedule implements MegaphoneSchedule {
return true;
}
if (newlyRegisteredV1PinUser()) {
if (newlyRegisteredRegistrationLockV1User()) {
return true;
}
@ -67,11 +67,11 @@ class PinsForAllSchedule implements MegaphoneSchedule {
private static boolean pinCreationFailedDuringRegistration() {
return SignalStore.registrationValues().pinWasRequiredAtRegistration() &&
!SignalStore.kbsValues().isV2RegistrationLockEnabled() &&
!SignalStore.kbsValues().hasPin() &&
!TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication());
}
private static boolean newlyRegisteredV1PinUser() {
private static boolean newlyRegisteredRegistrationLockV1User() {
return SignalStore.registrationValues().pinWasRequiredAtRegistration() && TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication());
}
}

View File

@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.keyvalue.KbsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.PinHashing;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.pin.PinState;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
@ -59,7 +60,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
}
@Override
protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException, KeyBackupSystemNoDataException {
protected void onRun() throws IOException, UnauthenticatedResponseException {
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context)) {
Log.i(TAG, "Registration lock disabled");
return;
@ -74,19 +75,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
}
Log.i(TAG, "Migrating pin to Key Backup Service");
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
pinChangeSession.enableRegistrationLock(masterKey);
kbsValues.setKbsMasterKey(kbsData, PinHashing.localPinHash(pinValue));
TextSecurePreferences.clearRegistrationLockV1(context);
PinState.onMigrateToRegistrationLockV2(context, pinValue);
Log.i(TAG, "Pin migrated to Key Backup Service");
}

View File

@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.pin;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
public final class PinRestoreActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.pin_restore_activity);
}
void navigateToPinCreation() {
final Intent main = new Intent(this, MainActivity.class);
final Intent createPin = CreateKbsPinActivity.getIntentForPinCreate(this);
final Intent chained = PassphraseRequiredActionBarActivity.chainIntent(createPin, main);
startActivity(chained);
}
}

View File

@ -0,0 +1,290 @@
package org.thoughtcrime.securesms.pin;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Locale;
public class PinRestoreEntryFragment extends Fragment {
private static final String TAG = Log.tag(PinRestoreActivity.class);
private static final int MINIMUM_PIN_LENGTH = 4;
private EditText pinEntry;
private View helpButton;
private View skipButton;
private CircularProgressButton pinButton;
private TextView errorLabel;
private TextView keyboardToggle;
private PinRestoreViewModel viewModel;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.pin_restore_entry_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initViews(view);
initViewModel();
}
private void initViews(@NonNull View root) {
pinEntry = root.findViewById(R.id.pin_restore_pin_input);
pinButton = root.findViewById(R.id.pin_restore_pin_confirm);
errorLabel = root.findViewById(R.id.pin_restore_pin_input_label);
keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle);
helpButton = root.findViewById(R.id.pin_restore_forgot_pin);
skipButton = root.findViewById(R.id.pin_restore_skip_button);
helpButton.setVisibility(View.GONE);
helpButton.setOnClickListener(v -> onNeedHelpClicked());
skipButton.setOnClickListener(v -> onSkipClicked());
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v);
onPinSubmitted();
return true;
}
return false;
});
enableAndFocusPinEntry();
pinButton.setOnClickListener((v) -> {
ViewUtil.hideKeyboard(requireContext(), pinEntry);
onPinSubmitted();
});
keyboardToggle.setOnClickListener((v) -> {
PinKeyboardType keyboardType = getPinEntryKeyboardType();
updateKeyboard(keyboardType.getOther());
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
});
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
}
private void initViewModel() {
viewModel = ViewModelProviders.of(this).get(PinRestoreViewModel.class);
viewModel.getTriesRemaining().observe(this, this::presentTriesRemaining);
viewModel.getEvent().observe(this, this::presentEvent);
}
private void presentTriesRemaining(PinRestoreViewModel.TriesRemaining triesRemaining) {
if (triesRemaining.hasIncorrectGuess()) {
if (triesRemaining.getCount() == 1) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
.setMessage(getResources().getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining.getCount(), triesRemaining.getCount()))
.setPositiveButton(android.R.string.ok, null)
.show();
}
errorLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin);
helpButton.setVisibility(View.VISIBLE);
skipButton.setVisibility(View.VISIBLE);
} else {
if (triesRemaining.getCount() == 1) {
helpButton.setVisibility(View.VISIBLE);
new AlertDialog.Builder(requireContext())
.setMessage(getResources().getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining.getCount(), triesRemaining.getCount()))
.setPositiveButton(android.R.string.ok, null)
.show();
}
}
if (triesRemaining.getCount() == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.");
onAccountLocked();
}
}
private void presentEvent(@NonNull PinRestoreViewModel.Event event) {
switch (event) {
case SUCCESS:
handleSuccess();
break;
case EMPTY_PIN:
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
cancelSpinning(pinButton);
pinEntry.getText().clear();
enableAndFocusPinEntry();
break;
case PIN_TOO_SHORT:
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
cancelSpinning(pinButton);
pinEntry.getText().clear();
enableAndFocusPinEntry();
break;
case PIN_INCORRECT:
cancelSpinning(pinButton);
pinEntry.getText().clear();
enableAndFocusPinEntry();
break;
case PIN_LOCKED:
onAccountLocked();
break;
case NETWORK_ERROR:
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
cancelSpinning(pinButton);
pinEntry.setEnabled(true);
enableAndFocusPinEntry();
break;
}
}
private PinKeyboardType getPinEntryKeyboardType() {
boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC;
}
private void onPinSubmitted() {
pinEntry.setEnabled(false);
viewModel.onPinSubmitted(pinEntry.getText().toString(), getPinEntryKeyboardType());
setSpinning(pinButton);
}
private void onNeedHelpClicked() {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_need_help)
.setMessage(getString(R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code, KbsConstants.MINIMUM_PIN_LENGTH))
.setPositiveButton(R.string.PinRestoreEntryFragment_create_new_pin, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> {
CommunicationActions.openEmail(requireContext(),
getString(R.string.PinRestoreEntryFragment_support_email),
getString(R.string.PinRestoreEntryFragment_signal_registration_need_help_with_pin),
getString(R.string.PinRestoreEntryFragment_subject_signal_registration,
getDevice(),
getAndroidVersion(),
BuildConfig.VERSION_NAME,
Locale.getDefault()));
})
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show();
}
private void onSkipClicked() {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry)
.setMessage(R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin)
.setPositiveButton(R.string.PinRestoreEntryFragment_create_new_pin, (dialog, which) -> {
PinState.onPinRestoreForgottenOrSkipped();
((PinRestoreActivity) requireActivity()).navigateToPinCreation();
})
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show();
}
private void onAccountLocked() {
Navigation.findNavController(requireView()).navigate(PinRestoreEntryFragmentDirections.actionAccountLocked());
}
private void handleSuccess() {
cancelSpinning(pinButton);
Activity activity = requireActivity();
if (Recipient.self().getProfileName().isEmpty() || !AvatarHelper.hasAvatar(activity, Recipient.self().getId())) {
final Intent main = new Intent(activity, MainActivity.class);
final Intent profile = EditProfileActivity.getIntent(activity, false);
profile.putExtra("next_intent", main);
startActivity(profile);
} else {
startActivity(new Intent(activity, MainActivity.class));
}
activity.finish();
}
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
pinEntry.getText().clear();
}
private @StringRes static int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) {
if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
return R.string.PinRestoreEntryFragment_enter_alphanumeric_pin;
} else {
return R.string.PinRestoreEntryFragment_enter_numeric_pin;
}
}
private void enableAndFocusPinEntry() {
pinEntry.setEnabled(true);
pinEntry.setFocusable(true);
if (pinEntry.requestFocus()) {
ServiceUtil.getInputMethodManager(pinEntry.getContext()).showSoftInput(pinEntry, 0);
}
}
private static void setSpinning(@Nullable CircularProgressButton button) {
if (button != null) {
button.setClickable(false);
button.setIndeterminateProgressMode(true);
button.setProgress(50);
}
}
private static void cancelSpinning(@Nullable CircularProgressButton button) {
if (button != null) {
button.setProgress(0);
button.setIndeterminateProgressMode(false);
button.setClickable(true);
}
}
private static String getDevice() {
return String.format("%s %s (%s)", Build.MANUFACTURER, Build.MODEL, Build.PRODUCT);
}
private static String getAndroidVersion() {
return String.format("%s (%s, %s)", Build.VERSION.RELEASE, Build.VERSION.INCREMENTAL, Build.DISPLAY);
}
}

View File

@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.pin;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.CommunicationActions;
public class PinRestoreLockedFragment extends Fragment {
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.pin_restore_locked_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
View createPinButton = view.findViewById(R.id.pin_locked_next);
View learnMoreButton = view.findViewById(R.id.pin_locked_learn_more);
createPinButton.setOnClickListener(v -> {
PinState.onPinRestoreForgottenOrSkipped();
((PinRestoreActivity) requireActivity()).navigateToPinCreation();
});
learnMoreButton.setOnClickListener(v -> {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url));
});
}
}

View File

@ -0,0 +1,106 @@
package org.thoughtcrime.securesms.pin;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Executor;
class PinRestoreRepository {
private static final String TAG = Log.tag(PinRestoreRepository.class);
private final Executor executor = SignalExecutors.UNBOUNDED;
private final KeyBackupService kbs = ApplicationDependencies.getKeyBackupService();
void getToken(@NonNull Callback<Optional<TokenData>> callback) {
executor.execute(() -> {
try {
String authorization = kbs.getAuthorization();
TokenResponse token = kbs.getToken(authorization);
TokenData tokenData = new TokenData(authorization, token);
callback.onComplete(Optional.of(tokenData));
} catch (IOException e) {
callback.onComplete(Optional.absent());
}
});
}
void submitPin(@NonNull String pin, @NonNull TokenData tokenData, @NonNull Callback<PinResultData> callback) {
executor.execute(() -> {
try {
Stopwatch stopwatch = new Stopwatch("PinSubmission");
KbsPinData kbsData = PinState.restoreMasterKey(pin, tokenData.basicAuth, tokenData.tokenResponse);
PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin);
stopwatch.split("MasterKey");
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
stopwatch.split("AccountRestore");
stopwatch.stop(TAG);
callback.onComplete(new PinResultData(PinResult.SUCCESS, tokenData));
} catch (IOException e) {
callback.onComplete(new PinResultData(PinResult.NETWORK_ERROR, tokenData));
} catch (KeyBackupSystemNoDataException e) {
callback.onComplete(new PinResultData(PinResult.LOCKED, tokenData));
} catch (KeyBackupSystemWrongPinException e) {
callback.onComplete(new PinResultData(PinResult.INCORRECT, new TokenData(tokenData.basicAuth, e.getTokenResponse())));
}
});
}
interface Callback<T> {
void onComplete(@NonNull T value);
}
static class TokenData {
private final String basicAuth;
private final TokenResponse tokenResponse;
TokenData(@NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
this.basicAuth = basicAuth;
this.tokenResponse = tokenResponse;
}
int getTriesRemaining() {
return tokenResponse.getTries();
}
}
static class PinResultData {
private final PinResult result;
private final TokenData tokenData;
PinResultData(@NonNull PinResult result, @NonNull TokenData tokenData) {
this.result = result;
this.tokenData = tokenData;
}
public @NonNull PinResult getResult() {
return result;
}
public @NonNull TokenData getTokenData() {
return tokenData;
}
}
enum PinResult {
SUCCESS, INCORRECT, LOCKED, NETWORK_ERROR
}
}

View File

@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.pin;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
public class PinRestoreViewModel extends ViewModel {
private final PinRestoreRepository repo;
private final DefaultValueLiveData<TriesRemaining> triesRemaining;
private final SingleLiveEvent<Event> event;
private volatile PinRestoreRepository.TokenData tokenData;
public PinRestoreViewModel() {
this.repo = new PinRestoreRepository();
this.triesRemaining = new DefaultValueLiveData<>(new TriesRemaining(10, false));
this.event = new SingleLiveEvent<>();
repo.getToken(token -> {
if (token.isPresent()) {
updateTokenData(token.get(), false);
} else {
event.postValue(Event.NETWORK_ERROR);
}
});
}
void onPinSubmitted(@NonNull String pin, @NonNull PinKeyboardType pinKeyboardType) {
int trimmedLength = pin.replace(" ", "").length();
if (trimmedLength == 0) {
event.postValue(Event.EMPTY_PIN);
return;
}
if (trimmedLength < KbsConstants.MINIMUM_PIN_LENGTH) {
event.postValue(Event.PIN_TOO_SHORT);
return;
}
if (tokenData != null) {
repo.submitPin(pin, tokenData, result -> {
switch (result.getResult()) {
case SUCCESS:
SignalStore.pinValues().setKeyboardType(pinKeyboardType);
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
event.postValue(Event.SUCCESS);
break;
case LOCKED:
event.postValue(Event.PIN_LOCKED);
break;
case INCORRECT:
event.postValue(Event.PIN_INCORRECT);
updateTokenData(result.getTokenData(), true);
break;
case NETWORK_ERROR:
event.postValue(Event.NETWORK_ERROR);
break;
}
});
} else {
repo.getToken(token -> {
if (token.isPresent()) {
updateTokenData(token.get(), false);
onPinSubmitted(pin, pinKeyboardType);
} else {
event.postValue(Event.NETWORK_ERROR);
}
});
}
}
@NonNull DefaultValueLiveData<TriesRemaining> getTriesRemaining() {
return triesRemaining;
}
@NonNull LiveData<Event> getEvent() {
return event;
}
private void updateTokenData(@NonNull PinRestoreRepository.TokenData tokenData, boolean incorrectGuess) {
this.tokenData = tokenData;
triesRemaining.postValue(new TriesRemaining(tokenData.getTriesRemaining(), incorrectGuess));
}
enum Event {
SUCCESS, EMPTY_PIN, PIN_TOO_SHORT, PIN_INCORRECT, PIN_LOCKED, NETWORK_ERROR
}
static class TriesRemaining {
private final int triesRemaining;
private final boolean hasIncorrectGuess;
TriesRemaining(int triesRemaining, boolean hasIncorrectGuess) {
this.triesRemaining = triesRemaining;
this.hasIncorrectGuess = hasIncorrectGuess;
}
public int getCount() {
return triesRemaining;
}
public boolean hasIncorrectGuess() {
return hasIncorrectGuess;
}
}
}

View File

@ -29,6 +29,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@ -84,35 +85,63 @@ public final class PinState {
*/
public static synchronized void onRegistration(@NonNull Context context,
@Nullable KbsPinData kbsData,
@Nullable String pin)
@Nullable String pin,
boolean hasPinToRestore)
{
Log.i(TAG, "onNewRegistration()");
if (kbsData == null) {
Log.i(TAG, "No KBS PIN. Clearing any PIN state.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
//noinspection deprecation Only acceptable place to write the old pin.
TextSecurePreferences.setV1RegistrationLockPin(context, pin);
//noinspection deprecation Only acceptable place to write the old pin enabled state.
TextSecurePreferences.setV1RegistrationLockEnabled(context, pin != null);
} else {
Log.i(TAG, "Had a KBS PIN. Saving data.");
SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
// TODO [greyson] [pins] Not always true -- when this flow is reworked, you can have a PIN but no reglock
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
resetPinRetryCount(context, pin, kbsData);
}
TextSecurePreferences.setV1RegistrationLockPin(context, pin);
if (pin != null) {
if (kbsData == null && pin != null) {
Log.i(TAG, "Registration Lock V1");
SignalStore.kbsValues().clearRegistrationLockAndPin();
TextSecurePreferences.setV1RegistrationLockEnabled(context, true);
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);
SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
SignalStore.pinValues().resetPinReminders();
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
resetPinRetryCount(context, pin, kbsData);
} else if (hasPinToRestore) {
Log.i(TAG, "Has a PIN to restore.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
SignalStore.storageServiceValues().setNeedsAccountRestore(true);
} else {
Log.i(TAG, "No registration lock or PIN at all.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
}
updateState(buildInferredStateFromOtherFields());
}
/**
* Invoked when the user is going through the PIN restoration flow (which is separate from reglock).
*/
public static synchronized void onSignalPinRestore(@NonNull Context context, @NonNull KbsPinData kbsData, @NonNull String pin) {
SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
SignalStore.pinValues().resetPinReminders();
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
resetPinRetryCount(context, pin, kbsData);
updateState(buildInferredStateFromOtherFields());
}
/**
* Invoked when the user skips out on PIN restoration or otherwise fails to remember their PIN.
*/
public static synchronized void onPinRestoreForgottenOrSkipped() {
SignalStore.kbsValues().clearRegistrationLockAndPin();
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
updateState(buildInferredStateFromOtherFields());
}
/**
* Invoked whenever the Signal PIN is changed or created.
*/
@ -181,10 +210,11 @@ public final class PinState {
}
/**
* Invoked whenever registration lock is disabled for a user without a Signal PIN.
* Called when registration lock is disabled in the settings using the old UI (i.e. no mention of
* Signal PINs).
*/
@WorkerThread
public static synchronized void onDisableRegistrationLockV1(@NonNull Context context)
public static synchronized void onDisableLegacyRegistrationLockPreference(@NonNull Context context)
throws IOException, UnauthenticatedResponseException
{
Log.i(TAG, "onDisableRegistrationLockV1()");
@ -197,12 +227,16 @@ public final class PinState {
updateState(State.NO_REGISTRATION_LOCK);
}
/**
* Called when registration lock is enabled in the settings using the old UI (i.e. no mention of
* Signal PINs).
*/
@WorkerThread
public static synchronized void onCompleteRegistrationLockV1Reminder(@NonNull Context context, @NonNull String pin)
public static synchronized void onEnableLegacyRegistrationLockPreference(@NonNull Context context, @NonNull String pin)
throws IOException, UnauthenticatedResponseException
{
Log.i(TAG, "onCompleteRegistrationLockV1Reminder()");
assertState(State.REGISTRATION_LOCK_V1);
assertState(State.NO_REGISTRATION_LOCK);
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
@ -219,7 +253,29 @@ public final class PinState {
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
updateState(State.PIN_WITH_REGISTRATION_LOCK_ENABLED);
updateState(buildInferredStateFromOtherFields());
}
/**
* Should only be called by {@link org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob}.
*/
@WorkerThread
public static synchronized void onMigrateToRegistrationLockV2(@NonNull Context context, @NonNull String pin)
throws IOException, UnauthenticatedResponseException
{
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
pinChangeSession.enableRegistrationLock(masterKey);
kbsValues.setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
TextSecurePreferences.clearRegistrationLockV1(context);
updateState(buildInferredStateFromOtherFields());
}
public static synchronized boolean shouldShowRegistrationLockV1Reminder() {
@ -273,7 +329,7 @@ public final class PinState {
}
}
throw new IllegalStateException();
throw new IllegalStateException("Expected: " + Arrays.toString(allowed) + ", Actual: " + currentState);
}
private static @NonNull State getState() {
@ -289,6 +345,7 @@ public final class PinState {
}
private static void updateState(@NonNull State state) {
Log.i(TAG, "Updating state to: " + state);
SignalStore.pinValues().setPinState(state.serialize());
}

View File

@ -81,7 +81,8 @@ public class AvatarHelper {
* Whether or not an avatar is present for the given recipient.
*/
public static boolean hasAvatar(@NonNull Context context, @NonNull RecipientId recipientId) {
return getAvatarFile(context, recipientId).exists();
File avatarFile = getAvatarFile(context, recipientId);
return avatarFile.exists() && avatarFile.length() > 0;
}
/**

View File

@ -34,8 +34,7 @@ public final class ProfileName implements Parcelable {
this(in.readString(), in.readString());
}
public @NonNull
String getGivenName() {
public @NonNull String getGivenName() {
return givenName;
}

View File

@ -2,8 +2,11 @@ package org.thoughtcrime.securesms.profiles.edit;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
@ -23,6 +26,12 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
public static @NonNull Intent getIntent(@NonNull Context context, boolean showToolbar) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, showToolbar);
return intent;
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);

View File

@ -45,6 +45,8 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
@ -307,9 +309,7 @@ public class EditProfileFragment extends Fragment {
private void handleUpload() {
viewModel.submitProfile(uploadResult -> {
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
if (SignalStore.kbsValues().hasPin()) {
SignalStore.registrationValues().setRegistrationComplete();
}
RegistrationUtil.markRegistrationPossiblyComplete();
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PROFILE_NAMES_FOR_ALL);

View File

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.registration;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
public final class RegistrationUtil {
private static final String TAG = Log.tag(RegistrationUtil.class);
private RegistrationUtil() {}
/**
* There's several events where a registration may or may not be considered complete based on what
* path a user has taken. This will only truly mark registration as complete if all of the
* requirements are met.
*/
public static void markRegistrationPossiblyComplete() {
if (SignalStore.kbsValues().hasPin() && !Recipient.self().getProfileName().isEmpty()) {
Log.i(TAG, "Marking registration completed.", new Throwable());
SignalStore.registrationValues().setRegistrationComplete();
StorageSyncHelper.scheduleSyncForDataChange();
}
}
}

View File

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
@ -305,16 +306,14 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
}
private void sendEmailToSupport() {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:"));
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ getString(R.string.RegistrationActivity_support_email) });
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.RegistrationActivity_code_support_subject));
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.RegistrationActivity_code_support_body,
getDevice(),
getAndroidVersion(),
BuildConfig.VERSION_NAME,
Locale.getDefault()));
startActivity(intent);
CommunicationActions.openEmail(requireContext(),
getString(R.string.RegistrationActivity_support_email),
getString(R.string.RegistrationActivity_code_support_subject),
getString(R.string.RegistrationActivity_code_support_body,
getDevice(),
getAndroidVersion(),
BuildConfig.VERSION_NAME,
Locale.getDefault()));
}
private static String getDevice() {

View File

@ -13,7 +13,9 @@ import androidx.navigation.ActivityNavigator;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
@ -30,12 +32,11 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
FragmentActivity activity = requireActivity();
if (!isReregister()) {
if (SignalStore.storageServiceValues().needsAccountRestore()) {
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
} else if (!isReregister()) {
final Intent main = new Intent(activity, MainActivity.class);
final Intent profile = new Intent(activity, EditProfileActivity.class);
profile.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
final Intent profile = EditProfileActivity.getIntent(activity, false);
Intent kbs = CreateKbsPinActivity.getIntentForPinCreate(requireContext());
activity.startActivity(chainIntents(chainIntents(profile, kbs), main));

View File

@ -39,8 +39,10 @@ import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException;
import java.util.List;
@ -210,16 +212,18 @@ public final class CodeVerificationRequest {
Log.i(TAG, "Calling verifyAccountWithCode(): reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2));
UUID uuid = accountManager.verifyAccountWithCode(code,
null,
registrationId,
!hasFcm,
registrationLockV1,
registrationLockV2,
unidentifiedAccessKey,
universalUnidentifiedAccess,
AppCapabilities.getCapabilities(isV2RegistrationLock));
// TODO [greyson] [pins] ^^ This needs to be updated. It's not just for reglock, but also if they needed to enter a PIN at all
VerifyAccountResponse response = accountManager.verifyAccountWithCode(code,
null,
registrationId,
!hasFcm,
registrationLockV1,
registrationLockV2,
unidentifiedAccessKey,
universalUnidentifiedAccess,
AppCapabilities.getCapabilities(true));
UUID uuid = UuidUtil.parseOrThrow(response.getUuid());
boolean hasPin = response.isStorageCapable();
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
@ -259,7 +263,7 @@ public final class CodeVerificationRequest {
TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false);
PinState.onRegistration(context, kbsData, pin);
PinState.onRegistration(context, kbsData, pin, hasPin);
}
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {

View File

@ -12,7 +12,7 @@ public final class KeyBackupSystemWrongPinException extends Exception {
this.tokenResponse = tokenResponse;
}
@NonNull TokenResponse getTokenResponse() {
public @NonNull TokenResponse getTokenResponse() {
return tokenResponse;
}
}

View File

@ -8,11 +8,13 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class StorageSyncValidations {
@ -20,15 +22,35 @@ public final class StorageSyncValidations {
private StorageSyncValidations() {}
public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result) {
Set<StorageId> allSet = new HashSet<>(result.getManifest().getStorageIds());
Set<StorageId> insertSet = new HashSet<>(Stream.of(result.getInserts()).map(SignalStorageRecord::getId).toList());
validateManifestAndInserts(result.getManifest(), result.getInserts());
if (result.getDeletes().size() > 0) {
Set<String> allSetEncoded = Stream.of(result.getManifest().getStorageIds()).map(StorageId::getRaw).map(Base64::encodeBytes).collect(Collectors.toSet());
for (byte[] delete : result.getDeletes()) {
String encoded = Base64.encodeBytes(delete);
if (allSetEncoded.contains(encoded)) {
throw new DeletePresentInFullIdSetError();
}
}
}
}
public static void validateForcePush(@NonNull SignalStorageManifest manifest, @NonNull List<SignalStorageRecord> inserts) {
validateManifestAndInserts(manifest, inserts);
}
private static void validateManifestAndInserts(@NonNull SignalStorageManifest manifest, @NonNull List<SignalStorageRecord> inserts) {
Set<StorageId> allSet = new HashSet<>(manifest.getStorageIds());
Set<StorageId> insertSet = new HashSet<>(Stream.of(inserts).map(SignalStorageRecord::getId).toList());
int accountCount = 0;
for (StorageId id : result.getManifest().getStorageIds()) {
for (StorageId id : manifest.getStorageIds()) {
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0;
}
if (result.getInserts().size() > insertSet.size()) {
if (inserts.size() > insertSet.size()) {
throw new DuplicateInsertInWriteError();
}
@ -40,7 +62,7 @@ public final class StorageSyncValidations {
throw new MissingAccountError();
}
for (SignalStorageRecord insert : result.getInserts()) {
for (SignalStorageRecord insert : inserts) {
if (!allSet.contains(insert.getId())) {
throw new InsertNotPresentInFullIdSetError();
}
@ -57,17 +79,6 @@ public final class StorageSyncValidations {
}
}
}
if (result.getDeletes().size() > 0) {
Set<String> allSetEncoded = Stream.of(result.getManifest().getStorageIds()).map(StorageId::getRaw).map(Base64::encodeBytes).collect(Collectors.toSet());
for (byte[] delete : result.getDeletes()) {
String encoded = Base64.encodeBytes(delete);
if (allSetEncoded.contains(encoded)) {
throw new DeletePresentInFullIdSetError();
}
}
}
}
private static final class DuplicateInsertInWriteError extends Error {

View File

@ -150,6 +150,17 @@ public class CommunicationActions {
}
}
public static void openEmail(@NonNull Context context, @NonNull String address, @Nullable String subject, @Nullable String body) {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:"));
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ address });
intent.putExtra(Intent.EXTRA_SUBJECT, Util.emptyIfNull(subject));
intent.putExtra(Intent.EXTRA_TEXT, Util.emptyIfNull(body));
context.startActivity(intent);
}
private static void startInsecureCallInternal(@NonNull Activity activity, @NonNull Recipient recipient) {
try {
Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress()));

View File

@ -172,6 +172,10 @@ public class Util {
return "";
}
public static @NonNull String emptyIfNull(@Nullable String value) {
return value != null ? value : "";
}
public static <E> List<List<E>> chunk(@NonNull List<E> list, int chunkSize) {
List<List<E>> chunks = new ArrayList<>(list.size() / chunkSize);

View File

@ -38,6 +38,7 @@ import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
@ -290,4 +291,9 @@ public class ViewUtil {
}
return result;
}
public static void hideKeyboard(@NonNull Context context, @NonNull View view) {
InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".pin.PinRestoreActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/pin_restore" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/pin_restore_pin_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/RegistrationLockFragment__enter_your_pin"
android:textAppearance="@style/TextAppearance.Signal.Title1"
app:layout_constraintBottom_toTopOf="@id/pin_restore_keyboard_toggle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.20" />
<TextView
android:id="@+id/pin_restore_pin_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="27dp"
android:gravity="center_horizontal"
android:minHeight="66dp"
android:text="@string/RegistrationLockFragment__enter_the_pin_you_created"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/core_grey_60"
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_title" />
<EditText
android:id="@+id/pin_restore_pin_input"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
android:inputType="numberPassword"
android:minWidth="210dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_description" />
<TextView
android:id="@+id/pin_restore_pin_input_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_input"
tools:text="@string/RegistrationLockFragment__incorrect_pin_try_again" />
<Button
android:id="@+id/pin_restore_forgot_pin"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/PinRestoreEntryFragment_need_help"
android:textColor="@color/core_ultramarine"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_input_label"
tools:visibility="visible"/>
<Button
android:id="@+id/pin_restore_keyboard_toggle"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
app:layout_constraintBottom_toTopOf="@id/pin_restore_pin_confirm"
app:layout_constraintTop_toBottomOf="@id/pin_restore_forgot_pin"
app:layout_constraintVertical_bias="1.0"
tools:text="Create Alphanumeric Pin" />
<com.dd.CircularProgressButton
android:id="@+id/pin_restore_pin_confirm"
style="@style/Button.Registration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
app:cpb_textIdle="@string/RegistrationActivity_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/pin_restore_skip_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/PinRestoreEntryFragment_skip"
android:visibility="gone"
style="@style/Button.Borderless.Registration"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/pin_locked_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="49dp"
android:gravity="center_horizontal"
android:text="@string/PinRestoreLockedFragment_create_your_pin"
android:textAppearance="@style/TextAppearance.Signal.Title1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/pin_locked_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="27dp"
android:gravity="center_horizontal"
android:minHeight="66dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/core_grey_60"
android:text="@string/PinRestoreLockedFragment_youve_run_out_of_pin_guesses"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pin_locked_title" />
<Button
android:id="@+id/pin_locked_next"
style="@style/Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:background="@drawable/cta_button_background"
android:text="@string/PinRestoreLockedFragment_create_new_pin"
android:textColor="@color/core_white"
app:layout_constraintBottom_toTopOf="@id/pin_locked_learn_more"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/pin_locked_learn_more"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:text="@string/AccountLockedFragment__learn_more"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/signup"
app:startDestination="@id/pinEntryFragment">
<fragment
android:id="@+id/pinEntryFragment"
android:name="org.thoughtcrime.securesms.pin.PinRestoreEntryFragment"
android:label="fragment_pin_entry"
tools:layout="@layout/pin_restore_entry_fragment">
<action
android:id="@+id/action_accountLocked"
app:destination="@id/pinLockedFragment"
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/pinLockedFragment"
android:name="org.thoughtcrime.securesms.pin.PinRestoreLockedFragment"
android:label="fragment_pin_locked"
tools:layout="@layout/account_locked_fragment"/>
</navigation>

View File

@ -132,6 +132,7 @@
<!-- CommunicationActions -->
<string name="CommunicationActions_no_browser_found">No web browser found.</string>
<string name="CommunicationActions_no_email_app_found">No email app found.</string>
<string name="CommunicationActions_a_cellular_call_is_already_in_progress">A cellular call is already in progress.</string>
<string name="CommunicationActions_start_video_call">Start video call?</string>
<string name="CommunicationActions_start_voice_call">Start voice call?</string>
@ -788,6 +789,32 @@
<!-- PlayServicesProblemFragment -->
<string name="PlayServicesProblemFragment_the_version_of_google_play_services_you_have_installed_is_not_functioning">The version of Google Play Services you have installed is not functioning correctly. Please reinstall Google Play Services and try again.</string>
<!-- PinRestoreEntryFragment -->
<string name="PinRestoreEntryFragment_incorrect_pin">Incorrect PIN</string>
<string name="PinRestoreEntryFragment_skip_pin_entry">Skip PIN entry?</string>
<string name="PinRestoreEntryFragment_need_help">Need help?</string>
<string name="PinRestoreEntryFragment_your_pin_is_a_d_digit_code">Your PIN is a %1$d+ digit code you created that can be numeric or alphanumeric.\n\nIf you cant remember your PIN, you can create a new one. You can register and use your account but youll lose some saved settings like your profile information.</string>
<string name="PinRestoreEntryFragment_if_you_cant_remember_your_pin">If you cant remember your PIN, you can create a new one. You can register and use your account but youll lose some saved settings like your profile information.</string>
<string name="PinRestoreEntryFragment_create_new_pin">Create New PIN</string>
<string name="PinRestoreEntryFragment_contact_support">Contact Support</string>
<string name="PinRestoreEntryFragment_cancel">Cancel</string>
<string name="PinRestoreEntryFragment_skip">Skip</string>
<plurals name="PinRestoreEntryFragment_you_have_d_attempt_remaining">
<item quantity="one">You have %1$d attempt remaining. If you run out of attempts, you can create a new PIN. You can register and use your account but youll lose some saved settings like your profile information.</item>
<item quantity="many">You have %1$d attempts remaining. If you run out of attempts, you can create a new PIN. You can register and use your account but youll lose some saved settings like your profile information.</item>
</plurals>
<string name="PinRestoreEntryFragment_support_email" translatable="false">support@signal.org</string>
<string name="PinRestoreEntryFragment_signal_registration_need_help_with_pin">Signal Registration - Need Help with PIN for Android</string>
<string name="PinRestoreEntryFragment_subject_signal_registration">Subject: Signal Registration - Need Help with PIN for Android\nDevice info: %1$s\nAndroid version: %2$s\nSignal version: %3$s\nLocale: %4$s</string>
<string name="PinRestoreEntryFragment_enter_alphanumeric_pin">Enter alphanumeric PIN</string>
<string name="PinRestoreEntryFragment_enter_numeric_pin">Enter numeric PIN</string>
<!-- PinRestoreLockedFragment -->
<string name="PinRestoreLockedFragment_create_your_pin">Create your PIN</string>
<string name="PinRestoreLockedFragment_youve_run_out_of_pin_guesses">You\'ve run out of PIN guesses, but you can still access your Signal account by creating a new PIN. For your privacy and security your account will be restored without any saved profile information or settings.</string>
<string name="PinRestoreLockedFragment_create_new_pin">Create new PIN</string>
<string name="PinRestoreLockedFragment_learn_more_url" translatable="false">https://support.signal.org/hc/articles/360007059792</string>
<!-- RatingManager -->
<string name="RatingManager_rate_this_app">Rate this app</string>
<string name="RatingManager_if_you_enjoy_using_this_app_please_take_a_moment">If you enjoy using this app, please take a moment to help us by rating it.</string>

View File

@ -102,7 +102,7 @@ public class PinsForAllScheduleTest {
public void whenUserIsANewInstallAndFlagIsDisabled_whenIShouldDisplay_thenIExpectFalse() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
when(kbsValues.isV2RegistrationLockEnabled()).thenReturn(true);
when(kbsValues.hasPin()).thenReturn(true);
when(FeatureFlags.pinsForAll()).thenReturn(false);
// WHEN
@ -116,7 +116,7 @@ public class PinsForAllScheduleTest {
public void whenUserIsANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectFalse() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
when(kbsValues.isV2RegistrationLockEnabled()).thenReturn(true);
when(kbsValues.hasPin()).thenReturn(true);
when(FeatureFlags.pinsForAll()).thenReturn(true);
// WHEN

View File

@ -71,12 +71,19 @@ public final class KeyBackupService {
/**
* Only call before registration, to see how many tries are left.
* <p>
* Pass the token to the newRegistrationSession.
* Pass the token to {@link #newRegistrationSession(String, TokenResponse)}.
*/
public TokenResponse getToken(String authAuthorization) throws IOException {
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
}
/**
* Retrieve the authorization token to be used with other requests.
*/
public String getAuthorization() throws IOException {
return pushServiceSocket.getKeyBackupServiceAuthorization();
}
/**
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
*

View File

@ -62,6 +62,7 @@ import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
@ -239,10 +240,10 @@ public class SignalServiceAccountManager {
* @return The UUID of the user that was registered.
* @throws IOException
*/
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
SignalServiceProfile.Capabilities capabilities)
public VerifyAccountResponse verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
SignalServiceProfile.Capabilities capabilities)
throws IOException
{
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,

View File

@ -271,23 +271,17 @@ public class PushServiceSocket {
}
}
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
public VerifyAccountResponse verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
SignalServiceProfile.Capabilities capabilities)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities);
String requestBody = JsonUtil.toJson(signalingKeyEntity);
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
VerifyAccountResponse response = JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
Optional<UUID> uuid = UuidUtil.parse(response.getUuid());
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities);
String requestBody = JsonUtil.toJson(signalingKeyEntity);
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
if (uuid.isPresent()) {
return uuid.get();
} else {
throw new IOException("Invalid UUID!");
}
return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
}
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,

View File

@ -6,7 +6,14 @@ public class VerifyAccountResponse {
@JsonProperty
private String uuid;
@JsonProperty
private boolean storageCapable;
public String getUuid() {
return uuid;
}
public boolean isStorageCapable() {
return storageCapable;
}
}