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 @@ Registration Lock helps protect your phone number from unauthorized registration attempts. This feature can be disabled at any time in your Signal privacy settings Registration Lock Enable - The Registration Lock PIN must be at least 4 digits. + The Registration Lock PIN must be at least %d digits. The two PINs you entered do not match. Error connecting to the service Disable Registration Lock PIN? diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java index 17f1a24c6..137709e54 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java @@ -1,10 +1,12 @@ package org.thoughtcrime.securesms.registration.v2; import org.junit.Test; +import org.thoughtcrime.securesms.registration.v2.testdata.KbsTestVector; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.kbs.HashedPin; import org.whispersystems.signalservice.api.kbs.KbsData; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.internal.util.JsonUtil; import java.io.IOException; @@ -12,17 +14,17 @@ import java.io.InputStream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom; public final class HashedPinKbsDataTest { @Test public void vectors_createNewKbsData() throws IOException { - for (KbsTestVector vector : getKbsTestVectorList().getVectors()) { + for (KbsTestVector vector : getKbsTestVectorList()) { HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash()); - KbsData kbsData = hashedPin.createNewKbsData(mockRandom(vector.getMasterKey())); + KbsData kbsData = hashedPin.createNewKbsData(MasterKey.createNew(mockRandom(vector.getMasterKey()))); assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize()); assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText()); @@ -33,7 +35,7 @@ public final class HashedPinKbsDataTest { @Test public void vectors_decryptKbsDataIVCipherText() throws IOException, InvalidCiphertextException { - for (KbsTestVector vector : getKbsTestVectorList().getVectors()) { + for (KbsTestVector vector : getKbsTestVectorList()) { HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash()); KbsData kbsData = hashedPin.decryptKbsDataIVCipherText(vector.getIvAndCipher()); @@ -45,12 +47,12 @@ public final class HashedPinKbsDataTest { } } - private static KbsTestVectorList getKbsTestVectorList() throws IOException { + private static KbsTestVector[] getKbsTestVectorList() throws IOException { try (InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_vectors.json")) { - KbsTestVectorList data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), KbsTestVectorList.class); + KbsTestVector[] data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), KbsTestVector[].class); - assertFalse(data.getVectors().isEmpty()); + assertTrue(data.length > 0); return data; } diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVectorList.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVectorList.java deleted file mode 100644 index 933149bdd..000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVectorList.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.thoughtcrime.securesms.registration.v2; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -public final class KbsTestVectorList { - - @JsonProperty("vectors") - private List vectors; - - public List getVectors() { - return vectors; - } -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHasher_normalize_Test.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHasher_normalize_Test.java new file mode 100644 index 000000000..75a3aa900 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHasher_normalize_Test.java @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.registration.v2; + +import org.junit.Test; +import org.thoughtcrime.securesms.registration.v2.testdata.PinSanitationVector; +import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.signalservice.internal.registrationpin.PinHasher; +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public final class PinHasher_normalize_Test { + + @Test + public void vectors_normalize() throws IOException { + for (PinSanitationVector vector : getKbsPinSanitationTestVectorList()) { + byte[] normalized = PinHasher.normalize(vector.getPin()); + + if (!Arrays.equals(vector.getBytes(), normalized)) { + assertEquals(String.format("%s [%s]", vector.getName(), vector.getPin()), + Hex.toStringCondensed(vector.getBytes()), + Hex.toStringCondensed(normalized)); + } + } + } + + private static PinSanitationVector[] getKbsPinSanitationTestVectorList() throws IOException { + try (InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_pin_normalization_vectors.json")) { + + PinSanitationVector[] data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), PinSanitationVector[].class); + + assertTrue(data.length > 0); + + return data; + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVector.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java similarity index 95% rename from app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVector.java rename to app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java index 9eb1715fe..2aa3640cb 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVector.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.registration.v2; +package org.thoughtcrime.securesms.registration.v2.testdata; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinSanitationVector.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinSanitationVector.java new file mode 100644 index 000000000..5c2485d26 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinSanitationVector.java @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.registration.v2.testdata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import org.thoughtcrime.securesms.testutil.HexDeserializer; + +public class PinSanitationVector { + + @JsonProperty("name") + private String name; + + @JsonProperty("pin") + private String pin; + + @JsonProperty("bytes") + @JsonDeserialize(using = HexDeserializer.class) + private byte[] bytes; + + public String getName() { + return name; + } + + public String getPin() { + return pin; + } + + public byte[] getBytes() { + return bytes; + } +} \ No newline at end of file diff --git a/app/src/test/resources/data/kbs_pin_normalization_vectors.json b/app/src/test/resources/data/kbs_pin_normalization_vectors.json new file mode 100644 index 000000000..33a5ba68b --- /dev/null +++ b/app/src/test/resources/data/kbs_pin_normalization_vectors.json @@ -0,0 +1,66 @@ +[ + { + "name": "Empty", + "pin": "", + "bytes": "" + }, + { + "pin": "password", + "bytes": "70617373776f7264" + }, + { + "name": "Trailing space", + "pin": "password ", + "bytes": "70617373776f7264" + }, + { + "name": "Leading and trailing spaces", + "pin": " password ", + "bytes": "70617373776f7264" + }, + { + "name": "Space in word", + "pin": "pass word", + "bytes": "7061737320776f7264" + }, + { + "name": "Leading and trailing spaces and space in word", + "pin": " pass word ", + "bytes": "7061737320776f7264" + }, + { + "name": "Arabic digits", + "pin": "12345", + "bytes": "3132333435" + }, + { + "name": "Leading and trailing spaces around digits", + "pin": " 12345 ", + "bytes": "3132333435" + }, + { + "name": "Non-arabic digits", + "pin": "١٢٣٤٥", + "bytes": "3132333435" + }, + { + "name": "Mixed digits", + "pin": "١٢٣4٥", + "bytes": "3132333435" + }, + { + "name": "Non-arabic digits with non-digit", + "pin": "١٢٣٤٥A", + "bytes": "d9a1d9a2d9a3d9a4d9a541" + }, + { + "name": "NFKD Test, Double Char", + "pin": "Ä", + "bytes": "41cc88" + }, + { + "name": "NFKD Test, Single Char", + "pin": "Ä", + "bytes": "41cc88" + } +] \ No newline at end of file diff --git a/app/src/test/resources/data/kbs_vectors.json b/app/src/test/resources/data/kbs_vectors.json index e32c5d949..c4522e6d5 100644 --- a/app/src/test/resources/data/kbs_vectors.json +++ b/app/src/test/resources/data/kbs_vectors.json @@ -1,13 +1,20 @@ -{ - "vectors": [ - { - "pin": "password", - "backup_id": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", - "argon2_hash": "65AADD2441A6C1979A2EA515DBB7092112703378D6BD83E8C4FF7771F6A7733F88A787415A2ECD79DA0D1016A82A27C5C695C9A19B88B0AA1D35683280AA9A67", - "master_key": "202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F", - "kbs_access_key": "88A787415A2ECD79DA0D1016A82A27C5C695C9A19B88B0AA1D35683280AA9A67", - "iv_and_cipher": "B18815B9B6C159CA9BB7E4F0486BD977AE84BF807F03157091DD04425C921D7D4CA7D5C4E27E31FD75DEF120135434D7", - "registration_lock": "2bf7988224ba35d3554966c65e8dc8c54974b034bdd44cabfd3f15fdb185e3c6" - } - ] -} \ No newline at end of file +[ + { + "pin":"password", + "backup_id":"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "argon2_hash":"44652df80490fc66bb864a9e638b2f7dc9e20649671dd66bbb9c37bee2bfecf1ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8", + "master_key":"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "kbs_access_key":"ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8", + "iv_and_cipher":"3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769", + "registration_lock":"2bf7988224ba35d3554966c65e8dc8c54974b034bdd44cabfd3f15fdb185e3c6" + }, + { + "pin":"anotherpassword", + "backup_id":"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "argon2_hash":"b6f16aa0591732e339b7e99cdd5fd6586a1c285c9d66876947fd82f66ed99757301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b", + "master_key":"88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67", + "kbs_access_key":"301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b", + "iv_and_cipher":"9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9", + "registration_lock":"4a458afa1b07493b23ee9b3f287b70416b2388ca39b5b8c27b4b7585bf73f413" + } +] \ No newline at end of file diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java index 58ad5096a..279313de1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java @@ -2,6 +2,9 @@ package org.whispersystems.signalservice.api; import org.whispersystems.libsignal.logging.Log; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.api.kbs.KbsData; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher; import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation; @@ -14,8 +17,7 @@ import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil; -import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException; -import org.whispersystems.signalservice.internal.registrationpin.PinStretcher; +import org.whispersystems.signalservice.internal.storage.protos.SignalStorage; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; @@ -116,8 +118,13 @@ public final class KeyBackupService { } @Override - public RegistrationLockData restorePin(String pin) - throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException + public byte[] hashSalt() { + return currentToken.getBackupId(); + } + + @Override + public RegistrationLockData restorePin(HashedPin hashedPin) + throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException { int attempt = 0; SecureRandom random = new SecureRandom(); @@ -128,7 +135,7 @@ public final class KeyBackupService { attempt++; try { - return restorePin(pin, token); + return restorePin(hashedPin, token); } catch (TokenException tokenException) { token = tokenException.getToken(); @@ -149,15 +156,13 @@ public final class KeyBackupService { } } - private RegistrationLockData restorePin(String pin, TokenResponse token) - throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException + private RegistrationLockData restorePin(HashedPin hashedPin, TokenResponse token) + throws UnauthenticatedResponseException, IOException, TokenException { - PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin); - try { final int remainingTries = token.getTries(); final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation(); - final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName)); + final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(hashedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName)); final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName); final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation); @@ -169,7 +174,8 @@ public final class KeyBackupService { switch (status.getStatus()) { case OK: Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries())); - PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray()); + KbsData kbsData = hashedPin.decryptKbsDataIVCipherText(status.getData().toByteArray()); + MasterKey masterKey = kbsData.getMasterKey(); return new RegistrationLockData(masterKey, nextToken); case PIN_MISMATCH: Log.i(TAG, "Restore PIN_MISMATCH"); @@ -203,16 +209,15 @@ public final class KeyBackupService { } @Override - public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException { - PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin) - .withNewSecurePinKey2(); + public RegistrationLockData setPin(HashedPin hashedPin, MasterKey masterKey) throws IOException, UnauthenticatedResponseException { + KbsData newKbsData = hashedPin.createNewKbsData(masterKey); - TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(), - masterKey.getPinKey2(), + TokenResponse tokenResponse = putKbsData(newKbsData.getKbsAccessKey(), + newKbsData.getCipherText(), enclaveName, currentToken); - pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock()); + pushServiceSocket.setRegistrationLock(masterKey.deriveRegistrationLock()); return new RegistrationLockData(masterKey, tokenResponse); } @@ -264,16 +269,21 @@ public final class KeyBackupService { } } - public interface RestoreSession { + public interface HashSession { - RegistrationLockData restorePin(String pin) - throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException; + byte[] hashSalt(); } - public interface PinChangeSession { + public interface RestoreSession extends HashSession { - RegistrationLockData setPin(String pin) - throws IOException, UnauthenticatedResponseException, InvalidPinException; + RegistrationLockData restorePin(HashedPin hashedPin) + throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException; + } + + public interface PinChangeSession extends HashSession { + + RegistrationLockData setPin(HashedPin hashedPin, MasterKey masterKey) + throws IOException, UnauthenticatedResponseException; void removePin() throws IOException, UnauthenticatedResponseException; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java index 741a51651..145e344db 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java @@ -1,19 +1,19 @@ package org.whispersystems.signalservice.api; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; -import org.whispersystems.signalservice.internal.registrationpin.PinStretcher; public final class RegistrationLockData { - private final PinStretcher.MasterKey masterKey; - private final TokenResponse tokenResponse; + private final MasterKey masterKey; + private final TokenResponse tokenResponse; - RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) { + RegistrationLockData(MasterKey masterKey, TokenResponse tokenResponse) { this.masterKey = masterKey; this.tokenResponse = tokenResponse; } - public PinStretcher.MasterKey getMasterKey() { + public MasterKey getMasterKey() { return masterKey; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java index 409f6a0f0..68509a01d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java @@ -3,8 +3,6 @@ package org.whispersystems.signalservice.api.kbs; import org.whispersystems.signalservice.api.crypto.HmacSIV; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; -import java.security.SecureRandom; - import static java.util.Arrays.copyOfRange; /** @@ -33,11 +31,10 @@ public final class HashedPin { /** * Creates a new {@link KbsData} to store on KBS. */ - public KbsData createNewKbsData(SecureRandom random) { - byte[] M = new byte[32]; - random.nextBytes(M); + public KbsData createNewKbsData(MasterKey masterKey) { + byte[] M = masterKey.serialize(); byte[] IVC = HmacSIV.encrypt(K, M); - return new KbsData(M, kbsAccessKey, IVC); + return new KbsData(masterKey, kbsAccessKey, IVC); } /** @@ -45,6 +42,10 @@ public final class HashedPin { */ public KbsData decryptKbsDataIVCipherText(byte[] IVC) throws InvalidCiphertextException { byte[] masterKey = HmacSIV.decrypt(K, IVC); - return new KbsData(masterKey, kbsAccessKey, IVC); + return new KbsData(new MasterKey(masterKey), kbsAccessKey, IVC); + } + + public byte[] getKbsAccessKey() { + return kbsAccessKey; } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java index eb76d2bf4..6a7abc6dc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java @@ -8,8 +8,8 @@ public final class KbsData { private final byte[] kbsAccessKey; private final byte[] cipherText; - KbsData(byte[] masterKey, byte[] kbsAccessKey, byte[] cipherText) { - this.masterKey = new MasterKey(masterKey); + KbsData(MasterKey masterKey, byte[] kbsAccessKey, byte[] cipherText) { + this.masterKey = masterKey; this.kbsAccessKey = kbsAccessKey; this.cipherText = cipherText; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java index 5add74dc1..6542a6006 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java @@ -3,18 +3,29 @@ package org.whispersystems.signalservice.api.kbs; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.StringUtil; +import java.security.SecureRandom; +import java.util.Arrays; + import static org.whispersystems.signalservice.api.crypto.CryptoUtil.hmacSha256; public final class MasterKey { + private static final int LENGTH = 32; + private final byte[] masterKey; public MasterKey(byte[] masterKey) { - if (masterKey.length != 32) throw new AssertionError(); + if (masterKey.length != LENGTH) throw new AssertionError(); this.masterKey = masterKey; } + public static MasterKey createNew(SecureRandom secureRandom) { + byte[] key = new byte[LENGTH]; + secureRandom.nextBytes(key); + return new MasterKey(key); + } + public String deriveRegistrationLock() { return Hex.toStringCondensed(derive("Registration Lock")); } @@ -30,4 +41,16 @@ public final class MasterKey { public byte[] serialize() { return masterKey.clone(); } + + @Override + public boolean equals(Object o) { + if (o == null || o.getClass() != getClass()) return false; + + return Arrays.equals(((MasterKey) o).masterKey, masterKey); + } + + @Override + public int hashCode() { + return Arrays.hashCode(masterKey); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java deleted file mode 100644 index b070fe1e7..000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.whispersystems.signalservice.internal.registrationpin; - -public final class InvalidPinException extends Exception { - - InvalidPinException(String message) { - super(message); - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinHasher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinHasher.java new file mode 100644 index 000000000..808df43e0 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinHasher.java @@ -0,0 +1,52 @@ +package org.whispersystems.signalservice.internal.registrationpin; + +import org.whispersystems.signalservice.api.kbs.HashedPin; + +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; + +public final class PinHasher { + + public static byte[] normalize(String pin) { + pin = pin.trim(); + + if (allNumeric(pin)) { + pin = new String(toArabic(pin)); + } + + pin = Normalizer.normalize(pin, Normalizer.Form.NFKD); + + return pin.getBytes(StandardCharsets.UTF_8); + } + + public static HashedPin hashPin(byte[] normalizedPinBytes, Argon2 argon2) { + return HashedPin.fromArgon2Hash(argon2.hash(normalizedPinBytes)); + } + + public interface Argon2 { + byte[] hash(byte[] password); + } + + private static boolean allNumeric(CharSequence pin) { + for (int i = 0; i < pin.length(); i++) { + if (!Character.isDigit(pin.charAt(i))) return false; + } + return true; + } + + /** + * Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters. + */ + private static char[] toArabic(CharSequence numerals) { + int length = numerals.length(); + char[] arabic = new char[length]; + + for (int i = 0; i < length; i++) { + int digit = Character.digit(numerals.charAt(i), 10); + + arabic[i] = (char) ('0' + digit); + } + + return arabic; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java deleted file mode 100644 index 4384d1db4..000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.whispersystems.signalservice.internal.registrationpin; - -import org.whispersystems.signalservice.internal.util.Hex; -import org.whispersystems.signalservice.internal.util.Util; - -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -import javax.crypto.Mac; -import javax.crypto.SecretKeyFactory; -import javax.crypto.ShortBufferException; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -public final class PinStretcher { - - private static final String HMAC_SHA256 = "HmacSHA256"; - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - public static StretchedPin stretchPin(CharSequence pin) throws InvalidPinException { - return new StretchedPin(pin); - } - - public static class StretchedPin { - private final byte[] stretchedPin; - private final byte[] pinKey1; - private final byte[] kbsAccessKey; - - private StretchedPin(byte[] stretchedPin, byte[] pinKey1, byte[] kbsAccessKey) { - this.stretchedPin = stretchedPin; - this.pinKey1 = pinKey1; - this.kbsAccessKey = kbsAccessKey; - } - - private StretchedPin(CharSequence pin) throws InvalidPinException { - if (pin.length() < 4) throw new InvalidPinException("Pin too short"); - - char[] arabicPin = toArabic(pin); - - stretchedPin = pbkdf2HmacSHA256(arabicPin, "nosalt", 20000, 256); - - try { - Mac mac = Mac.getInstance(HMAC_SHA256); - mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256)); - mac.update("Master Key Encryption".getBytes(UTF_8)); - - pinKey1 = new byte[32]; - mac.doFinal(pinKey1, 0); - - mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256)); - mac.update("KBS Access Key".getBytes(UTF_8)); - - kbsAccessKey = new byte[32]; - mac.doFinal(kbsAccessKey, 0); - } catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) { - throw new AssertionError(e); - } - } - - public MasterKey withPinKey2(byte[] pinKey2) { - return new MasterKey(pinKey1, pinKey2, this); - } - - public MasterKey withNewSecurePinKey2() { - return withPinKey2(Util.getSecretBytes(32)); - } - - public byte[] getPinKey1() { - return pinKey1; - } - - public byte[] getStretchedPin() { - return stretchedPin; - } - - public byte[] getKbsAccessKey() { - return kbsAccessKey; - } - } - - public static class MasterKey extends StretchedPin { - private final byte[] pinKey2; - private final byte[] masterKey; - private final String registrationLock; - - private MasterKey(byte[] pinKey1, byte[] pinKey2, StretchedPin stretchedPin) { - super(stretchedPin.stretchedPin, stretchedPin.pinKey1, stretchedPin.kbsAccessKey); - - if (pinKey2.length != 32) { - throw new AssertionError("PinKey2 must be exactly 32 bytes"); - } - - this.pinKey2 = pinKey2.clone(); - - try { - Mac mac = Mac.getInstance(HMAC_SHA256); - - mac.init(new SecretKeySpec(pinKey1, HMAC_SHA256)); - mac.update(pinKey2); - - masterKey = new byte[32]; - mac.doFinal(masterKey, 0); - - mac.init(new SecretKeySpec(masterKey, HMAC_SHA256)); - mac.update("Registration Lock".getBytes(UTF_8)); - - byte[] registration_lock_token_bytes = new byte[32]; - mac.doFinal(registration_lock_token_bytes, 0); - registrationLock = Hex.toStringCondensed(registration_lock_token_bytes); - - } catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) { - throw new AssertionError(e); - } - } - - public byte[] getPinKey2() { - return pinKey2; - } - - public String getRegistrationLock() { - return registrationLock; - } - - public byte[] getMasterKey() { - return masterKey; - } - } - - private static byte[] pbkdf2HmacSHA256(char[] pin, String salt, int iterationCount, int outputSize) { - byte[] saltBytes = salt.getBytes(Charset.forName("UTF-8")); - - try { - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - PBEKeySpec spec = new PBEKeySpec(pin, saltBytes, iterationCount, outputSize); - byte[] encoded = skf.generateSecret(spec).getEncoded(); - - spec.clearPassword(); - - return encoded; - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new AssertionError("Could not stretch pin", e); - } - } - - /** - * Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters. - */ - private static char[] toArabic(CharSequence numerals) throws InvalidPinException { - int length = numerals.length(); - char[] arabic = new char[length]; - - for (int i = 0; i < length; i++) { - int digit = Character.digit(numerals.charAt(i), 10); - - if (digit < 0) { - throw new InvalidPinException("Pin must only consist of decimals"); - } - - arabic[i] = (char) ('0' + digit); - } - - return arabic; - } -} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/kbs/MasterKeyTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/kbs/MasterKeyTest.java new file mode 100644 index 000000000..3e2f881af --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/kbs/MasterKeyTest.java @@ -0,0 +1,50 @@ +package org.whispersystems.signalservice.api.kbs; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public final class MasterKeyTest { + + @Test(expected = AssertionError.class) + public void wrong_length_too_short() { + new MasterKey(new byte[31]); + } + + @Test(expected = AssertionError.class) + public void wrong_length_too_long() { + new MasterKey(new byte[33]); + } + + @Test(expected = NullPointerException.class) + public void invalid_input_null() { + //noinspection ConstantConditions + new MasterKey(null); + } + + @Test + public void equality() { + byte[] masterKeyBytes1 = new byte[32]; + byte[] masterKeyBytes2 = new byte[32]; + MasterKey masterKey1 = new MasterKey(masterKeyBytes1); + MasterKey masterKey2 = new MasterKey(masterKeyBytes2); + + assertEquals(masterKey1, masterKey2); + assertEquals(masterKey1.hashCode(), masterKey2.hashCode()); + } + + @Test + public void in_equality() { + byte[] masterKeyBytes1 = new byte[32]; + byte[] masterKeyBytes2 = new byte[32]; + + masterKeyBytes1[0] = 1; + + MasterKey masterKey1 = new MasterKey(masterKeyBytes1); + MasterKey masterKey2 = new MasterKey(masterKeyBytes2); + + assertNotEquals(masterKey1, masterKey2); + assertNotEquals(masterKey1.hashCode(), masterKey2.hashCode()); + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java deleted file mode 100644 index d0d7bae50..000000000 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.whispersystems.signalservice.internal.registrationpin; - -import org.junit.Test; - -public final class PinStretchFailureTest { - - @Test(expected = InvalidPinException.class) - public void non_numeric_pin() throws InvalidPinException { - PinStretcher.stretchPin("A"); - } - - @Test(expected = InvalidPinException.class) - public void empty() throws InvalidPinException { - PinStretcher.stretchPin(""); - } - - @Test(expected = InvalidPinException.class) - public void too_few_digits() throws InvalidPinException { - PinStretcher.stretchPin("123"); - } - - @Test(expected = AssertionError.class) - public void pin_key_2_too_short() throws InvalidPinException { - PinStretcher.stretchPin("0000").withPinKey2(new byte[31]); - } - - @Test(expected = AssertionError.class) - public void pin_key_2_too_long() throws InvalidPinException { - PinStretcher.stretchPin("0000").withPinKey2(new byte[33]); - } -} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java deleted file mode 100644 index 4624e1766..000000000 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.whispersystems.signalservice.internal.registrationpin; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.whispersystems.signalservice.internal.util.Hex; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -@RunWith(Parameterized.class) -public final class PinStretchTest { - - private final String pin; - private final byte[] expectedStretchedPin; - private final byte[] expectedKeyPin1; - private final byte[] pinKey2; - private final byte[] expectedMasterKey; - private final String expectedRegistrationLock; - private final byte[] expectedKbsAccessKey; - - @Parameterized.Parameters - public static Collection data() { - return Arrays.asList(new Object[]{ - "12345", - "4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d", - "0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f", - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2", - "65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d", - "7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba" - }, - new Object[]{ - "12345", - "4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d", - "0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f", - "abababababababababababababababababababababababababababababababab", - "01198dc427cbf9c6b47f344654d75a263e53b992db73be44b201f357d072dc38", - "bd1f4e129cc705c26c2fcebd3fbc6e7db60caade89e6c465c68ed60aeedbb0c3", - "7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba" - }, - new Object[]{ - "١٢٣٤٥", - "4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d", - "0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f", - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2", - "65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d", - "7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba" - }, - new Object[]{ - "9876543210", - "1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9", - "40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3", - "abababababababababababababababababababababababababababababababab", - "127a435c15be2528f4b735423f8ee558b789e8ea1f6fe64d144d5b21a87c4e06", - "348d327acb823b54a988cf6bea647a154e21da25cbb121a115c13b871dccd548", - "90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e", - }, - new Object[]{ - "9876543210", - "1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9", - "40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3", - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "128833dbde1af3da703852b6b5a845e226fe9c7e069427b9c1e41279c0cdfb3a", - "6be0b17899cfb5c4316b92acc7db3b6a2fa5b9a19ef3e58a1c84a4de49230aa6", - "90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e", - }, - new Object[]{ - "0123", - "b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7", - "bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567", - "abababababababababababababababababababababababababababababababab", - "ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd", - "d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290", - "c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91" - }, - new Object[]{ - "௦௧௨௩", - "b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7", - "bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567", - "abababababababababababababababababababababababababababababababab", - "ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd", - "d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290", - "c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91" - }); - } - - public PinStretchTest(String pin, - String expectedStretchedPin, - String expectedKeyPin1, - String pinKey2, - String expectedMasterKey, - String expectedRegistrationLock, - String expectedKbsAccessKey) throws IOException { - this.pin = pin; - this.expectedStretchedPin = Hex.fromStringCondensed(expectedStretchedPin); - this.expectedKeyPin1 = Hex.fromStringCondensed(expectedKeyPin1); - this.pinKey2 = Hex.fromStringCondensed(pinKey2); - this.expectedMasterKey = Hex.fromStringCondensed(expectedMasterKey); - this.expectedRegistrationLock = expectedRegistrationLock; - this.expectedKbsAccessKey = Hex.fromStringCondensed(expectedKbsAccessKey); - } - - @Test - public void stretch_pin() throws InvalidPinException { - PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin); - - assertArrayEquals(expectedStretchedPin, stretchedPin.getStretchedPin()); - assertArrayEquals(expectedKeyPin1, stretchedPin.getPinKey1()); - assertArrayEquals(expectedKbsAccessKey, stretchedPin.getKbsAccessKey()); - - PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(pinKey2); - - assertArrayEquals(pinKey2, masterKey.getPinKey2()); - assertArrayEquals(expectedMasterKey, masterKey.getMasterKey()); - assertEquals(expectedRegistrationLock, masterKey.getRegistrationLock()); - - assertArrayEquals(expectedStretchedPin, masterKey.getStretchedPin()); - assertArrayEquals(expectedKeyPin1, masterKey.getPinKey1()); - assertArrayEquals(expectedKbsAccessKey, masterKey.getKbsAccessKey()); - } -}