Prevent the creation of 'weak' PINs.

Simple checks to prevent the same number, or sequentially
increasing/decreasing PINs. e.g. 1111, 1234, 54321, etc.
fork-5.53.8
Alan Evans 2020-05-04 17:50:38 -03:00 zatwierdzone przez Greyson Parrelli
rodzic b7296a4fe3
commit 87eab27996
9 zmienionych plików z 287 dodań i 28 usunięć

Wyświetl plik

@ -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);
}
} }

Wyświetl plik

@ -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
}
} }

Wyświetl plik

@ -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>

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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
}
]

Wyświetl plik

@ -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;
}
} }

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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;
}
}