Prevent the creation of 'weak' PINs.
Simple checks to prevent the same number, or sequentially increasing/decreasing PINs. e.g. 1111, 1234, 54321, etc.
This commit is contained in:
parent
b7296a4fe3
commit
87eab27996
9 changed files with 287 additions and 28 deletions
|
@ -1,15 +1,20 @@
|
||||||
package org.thoughtcrime.securesms.lock.v2;
|
package org.thoughtcrime.securesms.lock.v2;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.TranslateAnimation;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.PluralsRes;
|
import androidx.annotation.PluralsRes;
|
||||||
import androidx.autofill.HintConstants;
|
import androidx.autofill.HintConstants;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||||
|
|
||||||
public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {
|
public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {
|
||||||
|
|
||||||
|
@ -47,6 +52,15 @@ public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewMod
|
||||||
CreateKbsPinFragmentArgs args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
|
CreateKbsPinFragmentArgs args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
|
||||||
|
|
||||||
viewModel.getNavigationEvents().observe(getViewLifecycleOwner(), e -> onConfirmPin(e.getUserEntry(), e.getKeyboard(), args.getIsPinChange()));
|
viewModel.getNavigationEvents().observe(getViewLifecycleOwner(), e -> onConfirmPin(e.getUserEntry(), e.getKeyboard(), args.getIsPinChange()));
|
||||||
|
viewModel.getErrorEvents().observe(getViewLifecycleOwner(), e -> {
|
||||||
|
if (e == CreateKbsPinViewModel.PinErrorEvent.WEAK_PIN) {
|
||||||
|
getLabel().setText(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.red),
|
||||||
|
getString(R.string.CreateKbsPinFragment__choose_a_stronger_pin)));
|
||||||
|
shake(getInput(), () -> getInput().getText().clear());
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Unexpected PIN error!");
|
||||||
|
}
|
||||||
|
});
|
||||||
viewModel.getKeyboard().observe(getViewLifecycleOwner(), k -> {
|
viewModel.getKeyboard().observe(getViewLifecycleOwner(), k -> {
|
||||||
getLabel().setText(getLabelText(k));
|
getLabel().setText(getLabelText(k));
|
||||||
getInput().getText().clear();
|
getInput().getText().clear();
|
||||||
|
@ -76,4 +90,23 @@ public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewMod
|
||||||
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
|
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
|
||||||
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_PIN_LENGTH, KbsConstants.MINIMUM_PIN_LENGTH);
|
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_PIN_LENGTH, KbsConstants.MINIMUM_PIN_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void shake(@NonNull EditText view, @NonNull Runnable afterwards) {
|
||||||
|
TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0);
|
||||||
|
shake.setDuration(50);
|
||||||
|
shake.setRepeatCount(7);
|
||||||
|
shake.setAnimationListener(new Animation.AnimationListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animation animation) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animation animation) {
|
||||||
|
afterwards.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(Animation animation) {}
|
||||||
|
});
|
||||||
|
view.startAnimation(shake);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,22 @@ package org.thoughtcrime.securesms.lock.v2;
|
||||||
|
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.util.Preconditions;
|
import androidx.core.util.Preconditions;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||||
|
import org.whispersystems.signalservice.internal.registrationpin.PinValidityChecker;
|
||||||
|
|
||||||
public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
|
public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
|
||||||
|
|
||||||
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
|
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
|
||||||
private final MutableLiveData<PinKeyboardType> keyboard = new MutableLiveData<>(PinKeyboardType.NUMERIC);
|
private final MutableLiveData<PinKeyboardType> keyboard = new MutableLiveData<>(PinKeyboardType.NUMERIC);
|
||||||
private final SingleLiveEvent<NavigationEvent> events = new SingleLiveEvent<>();
|
private final SingleLiveEvent<NavigationEvent> events = new SingleLiveEvent<>();
|
||||||
|
private final SingleLiveEvent<PinErrorEvent> errors = new SingleLiveEvent<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LiveData<KbsPin> getUserEntry() {
|
public LiveData<KbsPin> getUserEntry() {
|
||||||
|
@ -27,6 +31,8 @@ public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPin
|
||||||
|
|
||||||
LiveData<NavigationEvent> getNavigationEvents() { return events; }
|
LiveData<NavigationEvent> getNavigationEvents() { return events; }
|
||||||
|
|
||||||
|
LiveData<PinErrorEvent> getErrorEvents() { return errors; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@MainThread
|
@MainThread
|
||||||
public void setUserEntry(String userEntry) {
|
public void setUserEntry(String userEntry) {
|
||||||
|
@ -42,8 +48,14 @@ public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPin
|
||||||
@Override
|
@Override
|
||||||
@MainThread
|
@MainThread
|
||||||
public void confirm() {
|
public void confirm() {
|
||||||
events.setValue(new NavigationEvent(Preconditions.checkNotNull(this.getUserEntry().getValue()),
|
KbsPin pin = Preconditions.checkNotNull(this.getUserEntry().getValue());
|
||||||
Preconditions.checkNotNull(this.getKeyboard().getValue())));
|
PinKeyboardType keyboard = Preconditions.checkNotNull(this.getKeyboard().getValue());
|
||||||
|
|
||||||
|
if (PinValidityChecker.valid(pin.toString())) {
|
||||||
|
events.setValue(new NavigationEvent(pin, keyboard));
|
||||||
|
} else {
|
||||||
|
errors.setValue(PinErrorEvent.WEAK_PIN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class NavigationEvent {
|
static final class NavigationEvent {
|
||||||
|
@ -63,4 +75,8 @@ public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPin
|
||||||
return keyboard;
|
return keyboard;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PinErrorEvent {
|
||||||
|
WEAK_PIN
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1930,6 +1930,7 @@
|
||||||
<string name="CreateKbsPinFragment__you_can_choose_a_new_pin_as_long_as_this_device_is_registered">You can change your PIN as long as this device is registered.</string>
|
<string name="CreateKbsPinFragment__you_can_choose_a_new_pin_as_long_as_this_device_is_registered">You can change your PIN as long as this device is registered.</string>
|
||||||
<string name="CreateKbsPinFragment__create_your_pin">Create your PIN</string>
|
<string name="CreateKbsPinFragment__create_your_pin">Create your PIN</string>
|
||||||
<string name="CreateKbsPinFragment__pins_keep_information_stored_with_signal_encrypted">PINs keep information stored with Signal encrypted so only you can access it. Your profile, settings, and contacts will restore when you reinstall Signal.</string>
|
<string name="CreateKbsPinFragment__pins_keep_information_stored_with_signal_encrypted">PINs keep information stored with Signal encrypted so only you can access it. Your profile, settings, and contacts will restore when you reinstall Signal.</string>
|
||||||
|
<string name="CreateKbsPinFragment__choose_a_stronger_pin">Choose a stronger PIN</string>
|
||||||
|
|
||||||
<!-- ConfirmKbsPinFragment -->
|
<!-- ConfirmKbsPinFragment -->
|
||||||
<string name="ConfirmKbsPinFragment__pins_dont_match">PINs don\'t match. Try again.</string>
|
<string name="ConfirmKbsPinFragment__pins_dont_match">PINs don\'t match. Try again.</string>
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package org.thoughtcrime.securesms.registration.v2;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.thoughtcrime.securesms.registration.v2.testdata.PinValidityVector;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.whispersystems.signalservice.internal.registrationpin.PinValidityChecker;
|
||||||
|
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class PinValidityChecker_validity_Test {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vectors_valid() throws IOException {
|
||||||
|
for (PinValidityVector vector : getKbsPinValidityTestVectorList()) {
|
||||||
|
boolean valid = PinValidityChecker.valid(vector.getPin());
|
||||||
|
|
||||||
|
assertEquals(String.format("%s [%s]", vector.getName(), vector.getPin()),
|
||||||
|
vector.isValid(),
|
||||||
|
valid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PinValidityVector[] getKbsPinValidityTestVectorList() throws IOException {
|
||||||
|
try (InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_pin_validity_vectors.json")) {
|
||||||
|
|
||||||
|
PinValidityVector[] data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), PinValidityVector[].class);
|
||||||
|
|
||||||
|
assertTrue(data.length > 0);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinValidityVector.java
vendored
Normal file
27
app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinValidityVector.java
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package org.thoughtcrime.securesms.registration.v2.testdata;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class PinValidityVector {
|
||||||
|
|
||||||
|
@JsonProperty("name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("pin")
|
||||||
|
private String pin;
|
||||||
|
|
||||||
|
@JsonProperty("valid")
|
||||||
|
private boolean valid;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPin() {
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
}
|
62
app/src/test/resources/data/kbs_pin_validity_vectors.json
Normal file
62
app/src/test/resources/data/kbs_pin_validity_vectors.json
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Empty",
|
||||||
|
"pin": "",
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alpha",
|
||||||
|
"pin": "abcd",
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sequential",
|
||||||
|
"pin": "1234",
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Non-sequential",
|
||||||
|
"pin": "6485",
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sequential descending",
|
||||||
|
"pin": "43210",
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sequential with space",
|
||||||
|
"pin": "1234 ",
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Non-sequential with space",
|
||||||
|
"pin": "1236 ",
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sequential Non-arabic digits",
|
||||||
|
"pin": "١٢٣٤٥",
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sequential descending Non-arabic digits",
|
||||||
|
"pin": "٥٤٣٢١",
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Non-sequential Non-arabic digits",
|
||||||
|
"pin": "١٢٣٥٤",
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All digits the same",
|
||||||
|
"pin": "9999",
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All Non-arabic digits the same",
|
||||||
|
"pin": "٢٢٢٢",
|
||||||
|
"valid": false
|
||||||
|
}
|
||||||
|
]
|
|
@ -10,8 +10,8 @@ public final class PinHasher {
|
||||||
public static byte[] normalize(String pin) {
|
public static byte[] normalize(String pin) {
|
||||||
pin = pin.trim();
|
pin = pin.trim();
|
||||||
|
|
||||||
if (allNumeric(pin)) {
|
if (PinString.allNumeric(pin)) {
|
||||||
pin = new String(toArabic(pin));
|
pin = PinString.toArabic(pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
pin = Normalizer.normalize(pin, Normalizer.Form.NFKD);
|
pin = Normalizer.normalize(pin, Normalizer.Form.NFKD);
|
||||||
|
@ -26,27 +26,4 @@ public final class PinHasher {
|
||||||
public interface Argon2 {
|
public interface Argon2 {
|
||||||
byte[] hash(byte[] password);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.whispersystems.signalservice.internal.registrationpin;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.Normalizer;
|
||||||
|
|
||||||
|
final class PinString {
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
static String 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 new String(arabic);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.whispersystems.signalservice.internal.registrationpin;
|
||||||
|
|
||||||
|
public final class PinValidityChecker {
|
||||||
|
|
||||||
|
public static boolean valid(String pin) {
|
||||||
|
pin = pin.trim();
|
||||||
|
|
||||||
|
if (pin.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinString.allNumeric(pin)) {
|
||||||
|
pin = PinString.toArabic(pin);
|
||||||
|
|
||||||
|
return !sequential(pin) &&
|
||||||
|
!sequential(reverse(pin)) &&
|
||||||
|
!allTheSame(pin);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String reverse(String string) {
|
||||||
|
char[] chars = string.toCharArray();
|
||||||
|
|
||||||
|
for (int i = 0; i < chars.length / 2; i++) {
|
||||||
|
char temp = chars[i];
|
||||||
|
chars[i] = chars[chars.length - i - 1];
|
||||||
|
chars[chars.length - i - 1] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sequential(String pin) {
|
||||||
|
int length = pin.length();
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = pin.charAt(0);
|
||||||
|
|
||||||
|
for (int i = 1; i < length; i++) {
|
||||||
|
char n = pin.charAt(i);
|
||||||
|
if (n != c + 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
c = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean allTheSame(String pin) {
|
||||||
|
int length = pin.length();
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = pin.charAt(0);
|
||||||
|
|
||||||
|
for (int i = 1; i < length; i++) {
|
||||||
|
char n = pin.charAt(i);
|
||||||
|
if (n != c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue