diff --git a/app/build.gradle b/app/build.gradle index 79c99851f..cbe680972 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -176,19 +176,11 @@ dependencies { testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1' testImplementation 'androidx.test:core:1.2.0' - androidTestImplementation 'androidx.multidex:multidex:2.0.1' - androidTestImplementation 'androidx.multidex:multidex-instrumentation:2.0.0' - androidTestImplementation 'com.google.dexmaker:dexmaker:1.2' - androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2' - androidTestImplementation ('org.assertj:assertj-core:1.7.1') { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } - androidTestImplementation ('com.squareup.assertj:assertj-android:1.1.1') { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - exclude group: 'com.android.support', module: 'support-annotations' - } testImplementation 'org.robolectric:robolectric:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2' + + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } dependencyVerification { @@ -238,8 +230,8 @@ android { buildConfigField "String", "USER_AGENT", "\"OWA\"" buildConfigField "boolean", "DEV_BUILD", "false" buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\"" - buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"f2e2a5004794a6c1bac5c4949eadbc243dd02e02d1a93f10fe24584fb70815d8\"" - buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"f51f435802ada769e67aaf5744372bb7e7d519eecf996d335eb5b46b872b5789\"" + buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"\"" + buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" @@ -258,6 +250,8 @@ android { universalApk true } } + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { @@ -309,7 +303,7 @@ android { buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"" buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\"" - buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"b5a865941f95887018c86725cc92308d34a3084dc2b4e7bd2de5e5e1690b50c6\"" + buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"a1e9c1d3f352b5c4f0fc7a421b98119e60e5ff703c28fbea85c66bfa7306deab\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"" } release { diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/TextSecureTestCase.java b/app/src/androidTest/java/org/thoughtcrime/securesms/TextSecureTestCase.java deleted file mode 100644 index 0de79c8d4..000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/TextSecureTestCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.thoughtcrime.securesms; - -import android.content.Context; -import android.test.InstrumentationTestCase; - -public class TextSecureTestCase extends InstrumentationTestCase { - - @Override - public void setUp() { - System.setProperty("dexmaker.dexcache", getInstrumentation().getTargetContext().getCacheDir().getPath()); - } - - protected Context getContext() { - return getInstrumentation().getContext(); - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/lock/PinHashing_hashPin_Test.java b/app/src/androidTest/java/org/thoughtcrime/securesms/lock/PinHashing_hashPin_Test.java new file mode 100644 index 000000000..5a1824c61 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/lock/PinHashing_hashPin_Test.java @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.lock; + +import org.junit.Test; +import org.thoughtcrime.securesms.util.Hex; +import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.api.kbs.KbsData; +import org.whispersystems.signalservice.api.kbs.MasterKey; + +import java.io.IOException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public final class PinHashing_hashPin_Test { + + @Test + public void argon2_hashed_pin_password() throws IOException { + byte[] backupId = Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f")); + + HashedPin hashedPin = PinHashing.hashPin("password", () -> backupId); + KbsData kbsData = hashedPin.createNewKbsData(masterKey); + + assertArrayEquals(Hex.fromStringCondensed("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"), hashedPin.getKbsAccessKey()); + assertArrayEquals(Hex.fromStringCondensed("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"), kbsData.getKbsAccessKey()); + assertArrayEquals(Hex.fromStringCondensed("3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769"), kbsData.getCipherText()); + assertEquals(masterKey, kbsData.getMasterKey()); + } + + @Test + public void argon2_hashed_pin_another_password() throws IOException { + byte[] backupId = Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"); + MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67")); + + HashedPin hashedPin = PinHashing.hashPin("anotherpassword ", () -> backupId); + KbsData kbsData = hashedPin.createNewKbsData(masterKey); + + assertArrayEquals(Hex.fromStringCondensed("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"), hashedPin.getKbsAccessKey()); + assertArrayEquals(Hex.fromStringCondensed("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"), kbsData.getKbsAccessKey()); + assertArrayEquals(Hex.fromStringCondensed("9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9"), kbsData.getCipherText()); + assertEquals(masterKey, kbsData.getMasterKey()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java index 47370b387..e1b370d88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java @@ -60,7 +60,7 @@ public class MultiDeviceKeysUpdateJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - MasterKey masterKey = SignalStore.kbsValues().getMasterKey(); + MasterKey masterKey = SignalStore.kbsValues().getPinBackedMasterKey(); byte[] storageServiceKey = masterKey != null ? masterKey.deriveStorageServiceKey() : null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index aaecc2274..cbc37d01e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -69,7 +69,7 @@ public class StorageForcePushJob extends BaseJob { protected void onRun() throws IOException, RetryLaterException { if (!FeatureFlags.STORAGE_SERVICE) throw new AssertionError(); - MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey(); + MasterKey kbsMasterKey = SignalStore.kbsValues().getPinBackedMasterKey(); if (kbsMasterKey == null) { Log.w(TAG, "No KBS master key is set! Must abort."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index b783af23a..8b5416581 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -110,7 +110,7 @@ public class StorageSyncJob extends BaseJob { SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context); - MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey(); + MasterKey kbsMasterKey = SignalStore.kbsValues().getPinBackedMasterKey(); if (kbsMasterKey == null) { Log.w(TAG, "No KBS master key is set! Must abort."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index 2eca7ac4b..df7bbd81a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -1,22 +1,22 @@ package org.thoughtcrime.securesms.keyvalue; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.util.JsonUtils; import org.whispersystems.signalservice.api.RegistrationLockData; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; -import org.whispersystems.signalservice.internal.registrationpin.PinStretcher; import java.io.IOException; +import java.security.SecureRandom; public final class KbsValues { - private static final String REGISTRATION_LOCK_PREF_V2 = "kbs.registration_lock_v2"; - private static final String REGISTRATION_LOCK_TOKEN_PREF = "kbs.registration_lock_token"; - private static final String REGISTRATION_LOCK_PIN_KEY_2_PREF = "kbs.registration_lock_pin_key_2"; - private static final String REGISTRATION_LOCK_MASTER_KEY = "kbs.registration_lock_master_key"; - private static final String REGISTRATION_LOCK_TOKEN_RESPONSE = "kbs.registration_lock_token_response"; + private 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 LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash"; private final KeyValueStore store; @@ -24,58 +24,85 @@ public final class KbsValues { this.store = store; } - public void setRegistrationLockMasterKey(@Nullable RegistrationLockData registrationLockData) { - KeyValueStore.Writer editor = store.beginWrite(); - - if (registrationLockData == null) { - editor.remove(REGISTRATION_LOCK_PREF_V2) - .remove(REGISTRATION_LOCK_TOKEN_RESPONSE) - .remove(REGISTRATION_LOCK_MASTER_KEY) - .remove(REGISTRATION_LOCK_TOKEN_PREF) - .remove(REGISTRATION_LOCK_PIN_KEY_2_PREF); - } else { - PinStretcher.MasterKey masterKey = registrationLockData.getMasterKey(); - String tokenResponse; - try { - tokenResponse = JsonUtils.toJson(registrationLockData.getTokenResponse()); - } catch (IOException e) { - throw new AssertionError(e); - } - - editor.putBoolean(REGISTRATION_LOCK_PREF_V2, true) - .putString(REGISTRATION_LOCK_TOKEN_RESPONSE, tokenResponse) - .putBlob(REGISTRATION_LOCK_MASTER_KEY, masterKey.getMasterKey()) - .putString(REGISTRATION_LOCK_TOKEN_PREF, masterKey.getRegistrationLock()) - .putBlob(REGISTRATION_LOCK_PIN_KEY_2_PREF, masterKey.getPinKey2()); - } - - editor.commit(); + /** + * Deliberately does not clear the {@link #MASTER_KEY}. + */ + public void clearRegistrationLock() { + store.beginWrite() + .remove(V2_LOCK_ENABLED) + .remove(TOKEN_RESPONSE) + .remove(LOCK_LOCAL_PIN_HASH) + .commit(); } - public @Nullable MasterKey getMasterKey() { - byte[] blob = store.getBlob(REGISTRATION_LOCK_MASTER_KEY, null); - if (blob != null) { - return new MasterKey(blob); - } else { - return null; + public synchronized void setRegistrationLockMasterKey(@NonNull RegistrationLockData registrationLockData, @NonNull String localPinHash) { + MasterKey masterKey = registrationLockData.getMasterKey(); + String tokenResponse; + try { + tokenResponse = JsonUtils.toJson(registrationLockData.getTokenResponse()); + } catch (IOException e) { + throw new AssertionError(e); } + + store.beginWrite() + .putBoolean(V2_LOCK_ENABLED, true) + .putString(TOKEN_RESPONSE, tokenResponse) + .putBlob(MASTER_KEY, masterKey.serialize()) + .putString(LOCK_LOCAL_PIN_HASH, localPinHash) + .commit(); + } + + /** + * Finds or creates the master key. Therefore this will always return a master key whether backed + * up or not. + *
+ * If you only want a key when it's backed up, use {@link #getPinBackedMasterKey()}.
+ */
+ public synchronized @NonNull MasterKey getOrCreateMasterKey() {
+ byte[] blob = store.getBlob(MASTER_KEY, null);
+
+ if (blob == null) {
+ store.beginWrite()
+ .putBlob(MASTER_KEY, MasterKey.createNew(new SecureRandom()).serialize())
+ .commit();
+ blob = store.getBlob(MASTER_KEY, null);
+ }
+
+ return new MasterKey(blob);
+ }
+
+ /**
+ * Returns null if master key is not backed up by a pin.
+ */
+ public synchronized @Nullable MasterKey getPinBackedMasterKey() {
+ if (!isV2RegistrationLockEnabled()) return null;
+ return getMasterKey();
+ }
+
+ private synchronized @Nullable MasterKey getMasterKey() {
+ byte[] blob = store.getBlob(MASTER_KEY, null);
+ return blob != null ? new MasterKey(blob) : null;
}
public @Nullable String getRegistrationLockToken() {
- return store.getString(REGISTRATION_LOCK_TOKEN_PREF, null);
+ MasterKey masterKey = getPinBackedMasterKey();
+ if (masterKey == null) {
+ return null;
+ } else {
+ return masterKey.deriveRegistrationLock();
+ }
}
- public @Nullable byte[] getRegistrationLockPinKey2() {
- return store.getBlob(REGISTRATION_LOCK_PIN_KEY_2_PREF, null);
+ public @Nullable String getLocalPinHash() {
+ return store.getString(LOCK_LOCAL_PIN_HASH, null);
}
public boolean isV2RegistrationLockEnabled() {
- return store.getBoolean(REGISTRATION_LOCK_PREF_V2, false);
+ return store.getBoolean(V2_LOCK_ENABLED, false);
}
- public @Nullable
- TokenResponse getRegistrationLockTokenResponse() {
- String token = store.getString(REGISTRATION_LOCK_TOKEN_RESPONSE, null);
+ public @Nullable TokenResponse getRegistrationLockTokenResponse() {
+ String token = store.getString(TOKEN_RESPONSE, null);
if (token == null) return null;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java b/app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java
new file mode 100644
index 000000000..af3f38d72
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java
@@ -0,0 +1,63 @@
+package org.thoughtcrime.securesms.lock;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.signal.argon2.Argon2;
+import org.signal.argon2.Argon2Exception;
+import org.signal.argon2.MemoryCost;
+import org.signal.argon2.Type;
+import org.signal.argon2.Version;
+import org.thoughtcrime.securesms.util.Util;
+import org.whispersystems.signalservice.api.KeyBackupService;
+import org.whispersystems.signalservice.api.kbs.HashedPin;
+import org.whispersystems.signalservice.internal.registrationpin.PinHasher;
+
+public final class PinHashing {
+
+ private static final Type KBS_PIN_ARGON_TYPE = Type.Argon2id;
+ private static final Type LOCAL_PIN_ARGON_TYPE = Type.Argon2i;
+
+ private PinHashing() {
+ }
+
+ public static HashedPin hashPin(@NonNull String pin, @NonNull KeyBackupService.HashSession hashSession) {
+ return PinHasher.hashPin(PinHasher.normalize(pin), password -> {
+ try {
+ return new Argon2.Builder(Version.V13)
+ .type(KBS_PIN_ARGON_TYPE)
+ .memoryCost(MemoryCost.MiB(16))
+ .parallelism(1)
+ .iterations(32)
+ .hashLength(64)
+ .build()
+ .hash(password, hashSession.hashSalt())
+ .getHash();
+ } catch (Argon2Exception e) {
+ throw new AssertionError(e);
+ }
+ });
+ }
+
+ public static String localPinHash(@NonNull String pin) {
+ byte[] normalized = PinHasher.normalize(pin);
+ try {
+ return new Argon2.Builder(Version.V13)
+ .type(LOCAL_PIN_ARGON_TYPE)
+ .memoryCost(MemoryCost.KiB(256))
+ .parallelism(1)
+ .iterations(50)
+ .hashLength(32)
+ .build()
+ .hash(normalized, Util.getSecretBytes(16))
+ .getEncoded();
+ } catch (Argon2Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public static boolean verifyLocalPinHash(@NonNull String localPinHash, @NonNull String pin) {
+ byte[] normalized = PinHasher.normalize(pin);
+ return Argon2.verify(localPinHash, normalized, LOCAL_PIN_ARGON_TYPE);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java
index 7be27d926..120610677 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java
@@ -39,14 +39,15 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
+import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+import org.whispersystems.signalservice.api.kbs.HashedPin;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
-import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
-import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
import java.io.IOException;
@@ -54,12 +55,15 @@ public final class RegistrationLockDialog {
private static final String TAG = Log.tag(RegistrationLockDialog.class);
+ private static final int MIN_V2_NUMERIC_PIN_LENGTH_ENTRY = 4;
+ private static final int MIN_V2_NUMERIC_PIN_LENGTH_SETTING = 4;
+
public static void showReminderIfNecessary(@NonNull Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
if (!RegistrationLockReminders.needsReminder(context)) return;
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) &&
- TextUtils.isEmpty(SignalStore.kbsValues().getRegistrationLockToken())) {
+ !SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
// Neither v1 or v2 to check against
Log.w(TAG, "Reg lock enabled, but no pin stored to verify against");
return;
@@ -117,62 +121,38 @@ public final class RegistrationLockDialog {
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
String pin = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
- return new TextWatcher() {
+ return new AfterTextChanged((Editable s) -> {
+ if (s != null && s.toString().replace(" ", "").equals(pin)) {
+ dialog.dismiss();
+ RegistrationLockReminders.scheduleReminder(context, true);
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {}
-
- @Override
- public void afterTextChanged(Editable s) {
- if (s != null && s.toString().replace(" ", "").equals(pin)) {
- dialog.dismiss();
- RegistrationLockReminders.scheduleReminder(context, true);
-
- if (FeatureFlags.KBS) {
- Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
- ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
- }
+ if (FeatureFlags.KBS) {
+ Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
+ ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
}
}
- };
+ });
}
private static TextWatcher getV2PinWatcher(@NonNull Context context, AlertDialog dialog) {
- KbsValues kbsValues = SignalStore.kbsValues();
- String registrationLockToken = kbsValues.getRegistrationLockToken();
- byte[] pinKey2 = kbsValues.getRegistrationLockPinKey2();
- TokenResponse registrationLockTokenResponse = kbsValues.getRegistrationLockTokenResponse();
+ KbsValues kbsValues = SignalStore.kbsValues();
+ MasterKey masterKey = kbsValues.getPinBackedMasterKey();
+ String localPinHash = kbsValues.getLocalPinHash();
- if (registrationLockToken == null) throw new AssertionError("No V2 reg lock token set at time of reminder");
- if (pinKey2 == null) throw new AssertionError("No pin key2 set at time of reminder");
- if (registrationLockTokenResponse == null) throw new AssertionError("No registrationLockTokenResponse set at time of reminder");
+ if (masterKey == null) throw new AssertionError("No masterKey set at time of reminder");
+ if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
- return new TextWatcher() {
+ return new AfterTextChanged((Editable s) -> {
+ if (s == null) return;
+ String pin = s.toString();
+ if (TextUtils.isEmpty(pin)) return;
+ if (pin.length() < MIN_V2_NUMERIC_PIN_LENGTH_ENTRY) return;
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {}
-
- @Override
- public void afterTextChanged(Editable s) {
- if (s == null) return;
- String pin = s.toString();
- if (TextUtils.isEmpty(pin)) return;
- if (pin.length() < 4) return;
-
- try {
- if (registrationLockToken.equals(PinStretcher.stretchPin(pin).withPinKey2(pinKey2).getRegistrationLock())) {
- dialog.dismiss();
- RegistrationLockReminders.scheduleReminder(context, true);
- }
- } catch (InvalidPinException e) {
- Log.w(TAG, e);
- }
+ if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
+ dialog.dismiss();
+ RegistrationLockReminders.scheduleReminder(context, true);
}
- };
+ });
}
@SuppressLint("StaticFieldLeak")
@@ -198,8 +178,10 @@ public final class RegistrationLockDialog {
String pinValue = pin.getText().toString().replace(" ", "");
String repeatValue = repeat.getText().toString().replace(" ", "");
- if (pinValue.length() < 4) {
- Toast.makeText(context, R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_four_digits, Toast.LENGTH_LONG).show();
+ if (pinValue.length() < MIN_V2_NUMERIC_PIN_LENGTH_SETTING) {
+ Toast.makeText(context,
+ context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, MIN_V2_NUMERIC_PIN_LENGTH_SETTING),
+ Toast.LENGTH_LONG).show();
return;
}
@@ -226,27 +208,29 @@ public final class RegistrationLockDialog {
TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pinValue);
} else {
Log.i(TAG, "Setting pin on KBS");
- KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
- RegistrationLockData kbsData = keyBackupService.newPinChangeSession()
- .setPin(pinValue);
- RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
- .restorePin(pinValue);
- String restoredLock = restoredData.getMasterKey()
- .getRegistrationLock();
- if (!restoredLock.equals(kbsData.getMasterKey().getRegistrationLock())) {
+ KbsValues kbsValues = SignalStore.kbsValues();
+ MasterKey masterKey = kbsValues.getOrCreateMasterKey();
+ KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
+ KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
+ HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
+ RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
+ RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
+ .restorePin(hashedPin);
+
+ if (!restoredData.getMasterKey().equals(masterKey)) {
throw new AssertionError("Failed to set the pin correctly");
} else {
Log.i(TAG, "Set and retrieved pin on KBS successfully");
}
- SignalStore.kbsValues().setRegistrationLockMasterKey(restoredData);
+ kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
TextSecurePreferences.clearOldRegistrationLockPin(context);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
}
return true;
- } catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException | InvalidPinException e) {
+ } catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException e) {
Log.w(TAG, e);
return false;
}
@@ -308,7 +292,7 @@ public final class RegistrationLockDialog {
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
keyBackupService.newPinChangeSession(currentToken).removePin();
- kbsValues.setRegistrationLockMasterKey(null);
+ kbsValues.clearRegistrationLock();
// It is possible a migration has not occurred, in this case, we need to remove the old V1 Pin
if (TextSecurePreferences.isV1RegistrationLockEnabled(context)) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java
index a13263ffa..f6d1ad594 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java
@@ -9,13 +9,18 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobs.BaseJob;
+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.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.signalservice.api.KeyBackupService;
+import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.RegistrationLockData;
+import org.whispersystems.signalservice.api.kbs.HashedPin;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
-import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
import java.io.IOException;
@@ -49,7 +54,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
}
@Override
- protected void onRun() throws IOException, UnauthenticatedResponseException {
+ protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException {
if (!FeatureFlags.KBS) {
Log.i(TAG, "Not migrating pin to KBS");
return;
@@ -61,27 +66,33 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
}
//noinspection deprecation Only acceptable place to read the old pin.
- String registrationLockPin = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
+ String pinValue = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
- if (registrationLockPin == null | TextUtils.isEmpty(registrationLockPin)) {
+ if (pinValue == null | TextUtils.isEmpty(pinValue)) {
Log.i(TAG, "No old pin to migrate");
return;
}
Log.i(TAG, "Migrating pin to Key Backup Service");
- try {
- RegistrationLockData registrationPinV2Key = ApplicationDependencies.getKeyBackupService()
- .newPinChangeSession()
- .setPin(registrationLockPin);
+ KbsValues kbsValues = SignalStore.kbsValues();
+ MasterKey masterKey = kbsValues.getOrCreateMasterKey();
+ KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
+ KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
+ HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
+ RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
+ RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
+ .restorePin(hashedPin);
- SignalStore.kbsValues().setRegistrationLockMasterKey(registrationPinV2Key);
- TextSecurePreferences.clearOldRegistrationLockPin(context);
- } catch (InvalidPinException e) {
- Log.w(TAG, "The V1 pin cannot be migrated.", e);
- return;
+ if (!restoredData.getMasterKey().equals(masterKey)) {
+ throw new RuntimeException("Failed to migrate the pin correctly");
+ } else {
+ Log.i(TAG, "Set and retrieved pin on KBS successfully");
}
+ kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
+ TextSecurePreferences.clearOldRegistrationLockPin(context);
+
Log.i(TAG, "Pin migrated to Key Backup Service");
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java
index eaf0c89b6..3d5d2ef56 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java
@@ -45,7 +45,12 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary");
- this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1).setOnPreferenceClickListener(new AccountLockClickListener());
+ SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
+ regLock.setChecked(
+ TextSecurePreferences.isV1RegistrationLockEnabled(requireContext()) || SignalStore.kbsValues().isV2RegistrationLockEnabled()
+ );
+ regLock.setOnPreferenceClickListener(new AccountLockClickListener());
+
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener());
@@ -149,10 +154,12 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private class AccountLockClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
- if (((SwitchPreferenceCompat)preference).isChecked()) {
- RegistrationLockDialog.showRegistrationUnlockPrompt(requireContext(), (SwitchPreferenceCompat)preference);
+ Context context = requireContext();
+
+ if (TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
+ RegistrationLockDialog.showRegistrationUnlockPrompt(context, (SwitchPreferenceCompat)preference);
} else {
- RegistrationLockDialog.showRegistrationLockPrompt(requireContext(), (SwitchPreferenceCompat)preference);
+ RegistrationLockDialog.showRegistrationLockPrompt(context, (SwitchPreferenceCompat)preference);
}
return true;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java
index 427489054..16fca0bcc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java
@@ -17,7 +17,9 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
+import org.thoughtcrime.securesms.keyvalue.KbsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.lock.PinHashing;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
@@ -36,11 +38,12 @@ import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+import org.whispersystems.signalservice.api.kbs.HashedPin;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.push.LockedException;
-import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
import java.io.IOException;
import java.util.List;
@@ -160,11 +163,12 @@ public final class CodeVerificationRequest {
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
RegistrationLockData kbsData = restoreMasterKey(pin, basicStorageCredentials, kbsTokenResponse);
- String registrationLock = kbsData != null ? kbsData.getMasterKey().getRegistrationLock() : null;
+ String registrationLock = kbsData != null ? kbsData.getMasterKey().deriveRegistrationLock() : null;
boolean present = fcmToken != null;
+ String pinForServer = basicStorageCredentials == null ? pin : null;
UUID uuid = accountManager.verifyAccountWithCode(code, null, registrationId, !present,
- pin, registrationLock,
+ pinForServer, registrationLock,
unidentifiedAccessKey, universalUnidentifiedAccess);
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
@@ -203,8 +207,8 @@ public final class CodeVerificationRequest {
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false);
- SignalStore.kbsValues().setRegistrationLockMasterKey(kbsData);
if (kbsData == null) {
+ SignalStore.kbsValues().clearRegistrationLock();
//noinspection deprecation Only acceptable place to write the old pin.
TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pin);
//noinspection deprecation Only acceptable place to write the old pin enabled state.
@@ -216,6 +220,7 @@ public final class CodeVerificationRequest {
}
}
} else {
+ SignalStore.kbsValues().setRegistrationLockMasterKey(kbsData, PinHashing.localPinHash(pin));
repostPinToResetTries(context, pin, kbsData);
}
if (pin != null) {
@@ -227,19 +232,23 @@ public final class CodeVerificationRequest {
private static void repostPinToResetTries(@NonNull Context context, @Nullable String pin, @NonNull RegistrationLockData kbsData) {
if (!FeatureFlags.KBS) return;
+ if (pin == null) return;
+
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
try {
- RegistrationLockData newData = keyBackupService.newPinChangeSession(kbsData.getTokenResponse())
- .setPin(pin);
- SignalStore.kbsValues().setRegistrationLockMasterKey(newData);
+ KbsValues kbsValues = SignalStore.kbsValues();
+ MasterKey masterKey = kbsValues.getOrCreateMasterKey();
+ KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(kbsData.getTokenResponse());
+ HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
+ RegistrationLockData newData = pinChangeSession.setPin(hashedPin, masterKey);
+
+ kbsValues.setRegistrationLockMasterKey(newData, PinHashing.localPinHash(pin));
TextSecurePreferences.clearOldRegistrationLockPin(context);
} catch (IOException e) {
Log.w(TAG, "May have failed to reset pin attempts!", e);
} catch (UnauthenticatedResponseException e) {
Log.w(TAG, "Failed to reset pin attempts", e);
- } catch (InvalidPinException e) {
- throw new AssertionError(e);
}
}
@@ -267,7 +276,8 @@ public final class CodeVerificationRequest {
try {
Log.i(TAG, "Restoring pin from KBS");
- RegistrationLockData kbsData = session.restorePin(pin);
+ HashedPin hashedPin = PinHashing.hashPin(pin, session);
+ RegistrationLockData kbsData = session.restorePin(hashedPin);
if (kbsData != null) {
Log.i(TAG, "Found registration lock token on KBS.");
} else {
@@ -280,9 +290,6 @@ public final class CodeVerificationRequest {
} catch (KeyBackupServicePinException e) {
Log.w(TAG, "Incorrect pin", e);
throw new KeyBackupSystemWrongPinException(e.getToken());
- } catch (InvalidPinException e) {
- Log.w(TAG, "Invalid pin", e);
- return null;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
index 545c3ead3..1ec6e6509 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
@@ -162,7 +162,7 @@ public class TextSecurePreferences {
@Deprecated
private static final String REGISTRATION_LOCK_PIN_PREF_V1 = "pref_registration_lock_pin";
- private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time";
+ private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time_2";
private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval";
private static final String SERVICE_OUTAGE = "pref_service_outage";
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e8cc2598f..b260f5e42 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1737,7 +1737,7 @@