Skip to content

Commit

Permalink
Prevent the creation of 'weak' PINs.
Browse files Browse the repository at this point in the history
Simple checks to prevent the same number, or sequentially
increasing/decreasing PINs. e.g. 1111, 1234, 54321, etc.
  • Loading branch information
alan-signal authored and greyson-signal committed May 4, 2020
1 parent b7296a4 commit 87eab27
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 28 deletions.
@@ -1,15 +1,20 @@
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.PluralsRes;
import androidx.autofill.HintConstants;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;

import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SpanUtil;

public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {

Expand Down Expand Up @@ -47,6 +52,15 @@ protected CreateKbsPinViewModel initializeViewModel() {
CreateKbsPinFragmentArgs args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());

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 -> {
getLabel().setText(getLabelText(k));
getInput().getText().clear();
Expand Down Expand Up @@ -76,4 +90,23 @@ private String getLabelText(@NonNull PinKeyboardType keyboard) {
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
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);
}
}
Expand Up @@ -2,18 +2,22 @@

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.whispersystems.signalservice.internal.registrationpin.PinValidityChecker;

public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {

private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
private final MutableLiveData<PinKeyboardType> keyboard = new MutableLiveData<>(PinKeyboardType.NUMERIC);
private final SingleLiveEvent<NavigationEvent> events = new SingleLiveEvent<>();
private final SingleLiveEvent<PinErrorEvent> errors = new SingleLiveEvent<>();

@Override
public LiveData<KbsPin> getUserEntry() {
Expand All @@ -27,6 +31,8 @@ public LiveData<PinKeyboardType> getKeyboard() {

LiveData<NavigationEvent> getNavigationEvents() { return events; }

LiveData<PinErrorEvent> getErrorEvents() { return errors; }

@Override
@MainThread
public void setUserEntry(String userEntry) {
Expand All @@ -42,8 +48,14 @@ public void toggleAlphaNumeric() {
@Override
@MainThread
public void confirm() {
events.setValue(new NavigationEvent(Preconditions.checkNotNull(this.getUserEntry().getValue()),
Preconditions.checkNotNull(this.getKeyboard().getValue())));
KbsPin pin = Preconditions.checkNotNull(this.getUserEntry().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 {
Expand All @@ -63,4 +75,8 @@ PinKeyboardType getKeyboard() {
return keyboard;
}
}

enum PinErrorEvent {
WEAK_PIN
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Expand Up @@ -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__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__choose_a_stronger_pin">Choose a stronger PIN</string>

<!-- ConfirmKbsPinFragment -->
<string name="ConfirmKbsPinFragment__pins_dont_match">PINs don\'t match. Try again.</string>
Expand Down
@@ -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;
}
}
}
@@ -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 changes: 62 additions & 0 deletions app/src/test/resources/data/kbs_pin_validity_vectors.json
@@ -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
}
]
Expand Up @@ -10,8 +10,8 @@ public final class PinHasher {
public static byte[] normalize(String pin) {
pin = pin.trim();

if (allNumeric(pin)) {
pin = new String(toArabic(pin));
if (PinString.allNumeric(pin)) {
pin = PinString.toArabic(pin);
}

pin = Normalizer.normalize(pin, Normalizer.Form.NFKD);
Expand All @@ -26,27 +26,4 @@ public static HashedPin hashPin(byte[] normalizedPinBytes, Argon2 argon2) {
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;
}
}
@@ -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);
}
}

2 comments on commit 87eab27

@Meteor0id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. This address most of the concerns I had with short PINs. But it does not yet help against social engineering, an attacker coupd still try a personalized educated guess on the PIN of his target. If the target had set a 4 difit PIN chances are he used the same PIN as he has set to unlock his SIM with, or that he uses a family members year of birth as his PIN. It would imo be better to raise the minimum digits to 5, because years come in 4 digits, and SIM card PIN are also often 4 digits.

@Meteor0id
Copy link
Contributor

@Meteor0id Meteor0id commented on 87eab27 May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's please also ban 1984. the nature of Signal makes it extremely likely that that will be a commonly used PIN.

Please sign in to comment.