]> WPIA git - gigi.git/commitdiff
Merge changes from topic '143'
authorFelix Dörre <felix@dogcraft.de>
Tue, 19 Jun 2018 18:20:54 +0000 (20:20 +0200)
committerGerrit Code Review <gigi-system@dogcraft.de>
Tue, 19 Jun 2018 18:20:54 +0000 (20:20 +0200)
* changes:
  add: optionally check pwned passwords
  add: PasswordHashChecker implementation
  add: DelegatingPasswordChecker implementation
  chg: move PasswordChecker object to Gigi class
  add: PasswordChecker interface

14 files changed:
config/gigi.properties.template
debian/control
debian/gigi.properties.5
links.txt
src/club/wpia/gigi/Gigi.java
src/club/wpia/gigi/dbObjects/User.java
src/club/wpia/gigi/pages/main/Signup.java
src/club/wpia/gigi/passwords/DelegatingPasswordChecker.java [new file with mode: 0644]
src/club/wpia/gigi/passwords/PasswordChecker.java [new file with mode: 0644]
src/club/wpia/gigi/passwords/PasswordHashChecker.java [new file with mode: 0644]
src/club/wpia/gigi/passwords/PasswordStrengthChecker.java [moved from src/club/wpia/gigi/util/PasswordStrengthChecker.java with 54% similarity]
tests/club/wpia/gigi/passwords/TestDelegatingPasswordChecker.java [new file with mode: 0644]
tests/club/wpia/gigi/passwords/TestPasswordStrengthChecker.java [moved from tests/club/wpia/gigi/util/TestPasswordStrengthChecker.java with 92% similarity]
tests/club/wpia/gigi/testUtils/ConfiguredTest.java

index 5e939e2d3e644e77a8719f44d468af9aa65d9291..85f2afde0fcb74e6b3a835bc409fe80b72dcfa67 100644 (file)
@@ -13,6 +13,7 @@ sql.user=
 sql.password=
 
 highFinancialValue=/path/to/alexa/list
+#knownPasswordHashes = /usr/share/pwned-passwords/pwned-passwords.bin
 
 time.testValidMonths=12
 time.reverificationDays=90
index 54c5b165387810ad2e9fdfddf42c015a9cfeaee4..fc5499d13aa64d801034b128a92d856ef1a43828 100644 (file)
@@ -11,6 +11,7 @@ Homepage: https://wpia.club
 Package: wpia-gigi
 Architecture: all
 Depends: java7-runtime-headless, wpia-gigi-setuid, libpostgresql-jdbc-java, libdnsjava-java, ${shlibs:Depends}, ${misc:Depends}
+Recommends: pwned-passwords-bin
 Conflicts: wpia-gigi-testing
 Description: WPIA Web-DB software.
  This program is used to manage accounts and certificates.
index fc54d8b8d1d74ac7b66eb8d3948671e01a829986..c0cc96df84c411b751f9bd27e44bcb093eb08a7b 100644 (file)
@@ -124,6 +124,16 @@ Defaults to \fI25\fR.
 A path to a plain text file of Internet domain names, one per line,
 which Gigi should refuse to issue certificates to.
 .TP
+.B knownPasswordHashes
+A path to a file of SHA-1 hashes of known passwords.
+The file should contain the hashes in binary format, without any separators, and should be sorted.
+Gigi will refuse user passwords with hashes that are found in this file.
+If this option is specified, Gigi will refuse startup if the file cannot be opened,
+otherwise it will attempt to use the file
+.I /usr/share/pwned-passwords/pwned-passwords.bin
+(provided by the \fBpwned-passwords-bin\fR package)
+but continue startup if the file cannot be opened.
+.TP
 .B time.testValidMonths
 The maximum time, in months, for which a passed agent quiz is considered recent.
 Defaults to \fI12\fR.
index d9dd6808812621b2c7c099e464769d5f73d66be0..728e1ed64eb6174886aa4300a34a7b096f8f4106 100644 (file)
--- a/links.txt
+++ b/links.txt
@@ -10,6 +10,7 @@
 /kb/names
 /kb/lostPassword
 /kb/goodPassword
+/kb/knownPasswordHash
 /kb/verificationHandbook
 /kb/truststores
 /ttp/user
index 15a52143e745394df63b579e7dedb1ac2c14e003..660c3d308c9645d7f3003b7cee51d1cb8115c132 100644 (file)
@@ -4,7 +4,12 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.math.BigInteger;
+import java.nio.channels.FileChannel;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
 import java.security.KeyStore;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.security.cert.X509Certificate;
 import java.util.Calendar;
 import java.util.Collections;
@@ -78,6 +83,10 @@ import club.wpia.gigi.pages.statistics.StatisticsRoles;
 import club.wpia.gigi.pages.wot.Points;
 import club.wpia.gigi.pages.wot.RequestTTPPage;
 import club.wpia.gigi.pages.wot.VerifyPage;
+import club.wpia.gigi.passwords.DelegatingPasswordChecker;
+import club.wpia.gigi.passwords.PasswordChecker;
+import club.wpia.gigi.passwords.PasswordHashChecker;
+import club.wpia.gigi.passwords.PasswordStrengthChecker;
 import club.wpia.gigi.ping.PingerDaemon;
 import club.wpia.gigi.util.AuthorizationContext;
 import club.wpia.gigi.util.DomainAssessment;
@@ -245,6 +254,8 @@ public final class Gigi extends HttpServlet {
 
     private static Gigi instance;
 
+    private static PasswordChecker passwordChecker;
+
     private static final Template baseTemplate = new Template(Gigi.class.getResource("Gigi.templ"));
 
     private PingerDaemon pinger;
@@ -273,6 +284,44 @@ public final class Gigi extends HttpServlet {
             this.truststore = truststore;
             pinger = new PingerDaemon(truststore);
             pinger.start();
+            Gigi.passwordChecker = getPasswordChecker(conf);
+        }
+    }
+
+    private PasswordChecker getPasswordChecker(Properties conf) {
+        final String knownPasswordHashesPath;
+        final boolean knownPasswordHashesRequired;
+        String knownPasswordHashesConfig = conf.getProperty("knownPasswordHashes");
+        if (knownPasswordHashesConfig != null) {
+            knownPasswordHashesPath = knownPasswordHashesConfig;
+            knownPasswordHashesRequired = true;
+        } else {
+            knownPasswordHashesPath = "/usr/share/pwned-passwords/pwned-passwords.bin";
+            knownPasswordHashesRequired = false;
+        }
+
+        final MessageDigest sha1;
+        try {
+            sha1 = MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        }
+
+        try {
+            final FileChannel knownPasswordHashesFile = FileChannel.open(
+                FileSystems.getDefault().getPath(knownPasswordHashesPath));
+            return new DelegatingPasswordChecker(new PasswordChecker[] {
+                    new PasswordStrengthChecker(),
+                    new PasswordHashChecker(knownPasswordHashesFile, sha1)
+                });
+        } catch (IOException e) {
+            if (knownPasswordHashesRequired) {
+                throw new RuntimeException("Error while opening password hash database, refusing startup", e);
+            } else {
+                System.err.println("Error while opening password hash database, passwords will be checked only by strength");
+                e.printStackTrace();
+                return new PasswordStrengthChecker();
+            }
         }
     }
 
@@ -521,4 +570,15 @@ public final class Gigi extends HttpServlet {
         instance.pinger.interrupt();
     }
 
+    public static PasswordChecker getPasswordChecker() {
+        if (passwordChecker == null) {
+            throw new IllegalStateException("Not yet initialized!");
+        }
+        return passwordChecker;
+    }
+
+    public static void setPasswordChecker(PasswordChecker passwordChecker) {
+        Gigi.passwordChecker = passwordChecker;
+    }
+
 }
index 3c2cd6b03f284d9b8ed71c14a14c59dc3a8400dd..9868e36a1d912efbeb6de98bc8d96537365fc6f0 100644 (file)
@@ -10,7 +10,9 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.TreeSet;
 
+import club.wpia.gigi.Gigi;
 import club.wpia.gigi.GigiApiException;
 import club.wpia.gigi.database.GigiPreparedStatement;
 import club.wpia.gigi.database.GigiResultSet;
@@ -21,11 +23,11 @@ import club.wpia.gigi.email.EmailProvider;
 import club.wpia.gigi.localisation.Language;
 import club.wpia.gigi.output.DateSelector;
 import club.wpia.gigi.pages.PasswordResetPage;
+import club.wpia.gigi.passwords.PasswordStrengthChecker;
 import club.wpia.gigi.util.CalendarUtil;
 import club.wpia.gigi.util.DayDate;
 import club.wpia.gigi.util.Notary;
 import club.wpia.gigi.util.PasswordHash;
-import club.wpia.gigi.util.PasswordStrengthChecker;
 import club.wpia.gigi.util.TimeConditions;
 
 /**
@@ -209,7 +211,17 @@ public class User extends CertificateOwner {
     }
 
     private void setPassword(String newPass) throws GigiApiException {
-        PasswordStrengthChecker.assertStrongPassword(newPass, getNames(), getEmail());
+        Name[] names = getNames();
+        TreeSet<String> nameParts = new TreeSet<>();
+        for (int i = 0; i < names.length; i++) {
+            for (NamePart string : names[i].getParts()) {
+                nameParts.add(string.getValue());
+            }
+        }
+        GigiApiException gaPassword = Gigi.getPasswordChecker().checkPassword(newPass, nameParts.toArray(new String[nameParts.size()]), getEmail());
+        if (gaPassword != null) {
+            throw gaPassword;
+        }
         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE users SET `password`=? WHERE id=?")) {
             ps.setString(1, PasswordHash.hash(newPass));
             ps.setInt(2, getId());
index 5c68c38e1f3e0a9dfa06199c48fbeb6a6ada0e1a..6b8dd75ff1fdfc3ea65aa5a44f447d45cc4b66c1 100644 (file)
@@ -8,6 +8,7 @@ import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 
+import club.wpia.gigi.Gigi;
 import club.wpia.gigi.GigiApiException;
 import club.wpia.gigi.database.GigiPreparedStatement;
 import club.wpia.gigi.database.GigiResultSet;
@@ -23,10 +24,10 @@ import club.wpia.gigi.output.template.SprintfCommand;
 import club.wpia.gigi.output.template.Template;
 import club.wpia.gigi.output.template.TranslateCommand;
 import club.wpia.gigi.pages.Page;
+import club.wpia.gigi.passwords.PasswordStrengthChecker;
 import club.wpia.gigi.util.CalendarUtil;
 import club.wpia.gigi.util.HTMLEncoder;
 import club.wpia.gigi.util.Notary;
-import club.wpia.gigi.util.PasswordStrengthChecker;
 import club.wpia.gigi.util.RateLimit.RateLimitException;
 
 public class Signup extends Form {
@@ -127,13 +128,13 @@ public class Signup extends Form {
         } else if ( !pw1.equals(pw2)) {
             ga.mergeInto(new GigiApiException("Passwords don't match"));
         }
-        int pwpoints = PasswordStrengthChecker.checkpw(pw1, ni.getNamePartsPlain(), email);
-        if (pwpoints < 3) {
-            ga.mergeInto(new GigiApiException(new SprintfCommand("The Password you submitted failed to contain enough differing characters and/or contained words from your name and/or email address. For the current requirements and to learn more, visit our {0}FAQ{1}.", Arrays.asList("!(/kb/goodPassword", "!'</a>'"))));
-        }
         if ( !ga.isEmpty()) {
             throw ga;
         }
+        GigiApiException gaPassword = Gigi.getPasswordChecker().checkPassword(pw1, ni.getNamePartsPlain(), email);
+        if (gaPassword != null) {
+            throw gaPassword;
+        }
         GigiApiException ga2 = new GigiApiException();
         try (GigiPreparedStatement q1 = new GigiPreparedStatement("SELECT * FROM `emails` WHERE `email`=? AND `deleted` IS NULL"); GigiPreparedStatement q2 = new GigiPreparedStatement("SELECT * FROM `certOwners` INNER JOIN `users` ON `users`.`id`=`certOwners`.`id` WHERE `email`=? AND `deleted` IS NULL")) {
             q1.setString(1, email);
diff --git a/src/club/wpia/gigi/passwords/DelegatingPasswordChecker.java b/src/club/wpia/gigi/passwords/DelegatingPasswordChecker.java
new file mode 100644 (file)
index 0000000..3d6c9d9
--- /dev/null
@@ -0,0 +1,32 @@
+package club.wpia.gigi.passwords;
+
+import club.wpia.gigi.GigiApiException;
+
+/**
+ * A {@link PasswordChecker} that delegates checks to several other PasswordCheckers
+ * and merges their error messages.
+ */
+public class DelegatingPasswordChecker implements PasswordChecker {
+
+    private final PasswordChecker[] checkers;
+
+    public DelegatingPasswordChecker(PasswordChecker[] checkers) {
+        this.checkers = checkers;
+    }
+
+    @Override
+    public GigiApiException checkPassword(String password, String[] nameParts, String email) {
+        GigiApiException exception = new GigiApiException();
+        for (PasswordChecker checker : checkers) {
+            GigiApiException currentException = checker.checkPassword(password, nameParts, email);
+            if (currentException != null) {
+                exception.mergeInto(currentException);
+            }
+        }
+        if (exception.isEmpty()) {
+            return null;
+        } else {
+            return exception;
+        }
+    }
+}
diff --git a/src/club/wpia/gigi/passwords/PasswordChecker.java b/src/club/wpia/gigi/passwords/PasswordChecker.java
new file mode 100644 (file)
index 0000000..822abf2
--- /dev/null
@@ -0,0 +1,23 @@
+package club.wpia.gigi.passwords;
+
+import club.wpia.gigi.GigiApiException;
+import club.wpia.gigi.dbObjects.Name;
+
+/**
+ * A strategy to check whether a password is acceptable for use or not.
+ */
+public interface PasswordChecker {
+
+    /**
+     * Checks if a password is acceptable for use.
+     * Most implementations judge a password’s strength in some way
+     * and reject weak passwords.
+     *
+     * @param password The password to check.
+     * @param nameParts The name parts of the user that wants to use this password.
+     * @param email The email address of the user that wants to use this password.
+     * @return {@code null} if the password is acceptable,
+     * otherwise a {@link GigiApiException} with an appropriate error message.
+     */
+    public GigiApiException checkPassword(String password, String[] nameParts, String email);
+}
diff --git a/src/club/wpia/gigi/passwords/PasswordHashChecker.java b/src/club/wpia/gigi/passwords/PasswordHashChecker.java
new file mode 100644 (file)
index 0000000..32eda62
--- /dev/null
@@ -0,0 +1,130 @@
+package club.wpia.gigi.passwords;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+import club.wpia.gigi.GigiApiException;
+import club.wpia.gigi.output.template.SprintfCommand;
+
+/**
+ * A {@link PasswordChecker} which searches for a password
+ * in a given file containing hashes of known breached passwords.
+ * If the password is found, it is rejected.
+ */
+public class PasswordHashChecker implements PasswordChecker {
+
+    private final FileChannel database;
+
+    private final MessageDigest digest;
+
+    private final int digestLength;
+
+    private final ByteBuffer hashBuffer;
+
+    /**
+     * @param database The database of password hashes.
+     *                 It should contain the hashes in binary format, with no separating characters,
+     *                 sorted in ascending order.
+     * @param digest   The digest function that was used to compile the database.
+     */
+    public PasswordHashChecker(FileChannel database, MessageDigest digest) {
+        this.database = database;
+        this.digest = digest;
+        this.digestLength = digest.getDigestLength();
+        this.hashBuffer = ByteBuffer.allocate(digestLength);
+    }
+
+    @Override
+    public GigiApiException checkPassword(String password, String[] nameParts, String email) {
+        byte[] passwordHash = passwordHash(password);
+        boolean knownPasswordHash;
+        try {
+            knownPasswordHash = knownPasswordHash(passwordHash);
+        } catch (IOException e) {
+            System.err.printf(
+                "Error while reading password hash database, ACCEPTING POTENTIALLY KNOWN PASSWORD for user with email %s.",
+                email
+            );
+            e.printStackTrace();
+            knownPasswordHash = false;
+        }
+        if (knownPasswordHash) {
+            return new GigiApiException(new SprintfCommand(
+                "The password you submitted was found in a public database of passwords exposed in data breaches. If you use this password anywhere else, you should change it. See our {0}FAQ{1} for more information.",
+                Arrays.asList("!(/kb/knownPasswordHash", "!'</a>'")
+            ));
+        } else {
+            return null;
+        }
+    }
+
+    private byte[] passwordHash(String password) {
+        byte[] passwordBytes = password.getBytes(Charset.forName("UTF-8"));
+        byte[] passwordHash;
+        synchronized (digest) {
+            digest.reset();
+            digest.update(passwordBytes);
+            passwordHash = digest.digest();
+        }
+        return passwordHash;
+    }
+
+    private boolean knownPasswordHash(byte[] passwordHash) throws IOException {
+        long targetEstimate = estimateHashOffset(passwordHash);
+        long bestGuess = targetEstimate;
+
+        hashBuffer.clear();
+        database.read(hashBuffer, bestGuess);
+        // first, guess the location of the hash within the file
+        for (int i = 0; i < 4; i++) {
+            long bestGuessEstimate = estimateHashOffset(hashBuffer.array());
+            if (bestGuessEstimate == targetEstimate) {
+                break;
+            }
+            bestGuess = bestGuess + targetEstimate - bestGuessEstimate;
+            hashBuffer.clear();
+            database.read(hashBuffer, bestGuess);
+        }
+        int searchDirection = compareHashes(passwordHash, hashBuffer.array());
+        if (searchDirection == 0) {
+            return true;
+        }
+        // then, do a linear search from that location until we’ve either found or moved past the hash
+        int newSearchDirection = searchDirection;
+        while (searchDirection == newSearchDirection) {
+            bestGuess += digestLength * searchDirection;
+            hashBuffer.clear();
+            database.read(hashBuffer, bestGuess);
+            newSearchDirection = compareHashes(passwordHash, hashBuffer.array());
+        }
+        return newSearchDirection == 0;
+    }
+
+    private int compareHashes(byte[] left, byte[] right) {
+        for (int i = 0; i < digestLength; i++) {
+            int leftByte = Byte.toUnsignedInt(left[i]);
+            int rightByte = Byte.toUnsignedInt(right[i]);
+            if (leftByte < rightByte) {
+                return -1;
+            }
+            if (leftByte > rightByte) {
+                return 1;
+            }
+        }
+        return 0;
+    }
+
+    private long estimateHashOffset(byte[] hash) throws IOException {
+        long pos = (database.size()
+                * ((hash[0] & 0xFF) << 8 | (hash[1] & 0xFF)))
+                / (1L << 16);
+        pos += (database.size()
+                * ((hash[2] & 0xFF) << 8 | (hash[3] & 0xFF)))
+                / (1L << 32);
+        return (pos / digestLength) * digestLength;
+    }
+}
similarity index 54%
rename from src/club/wpia/gigi/util/PasswordStrengthChecker.java
rename to src/club/wpia/gigi/passwords/PasswordStrengthChecker.java
index 6d5280afa09d55e7817e5eabbba483ad8fa3c5a6..c1d8b24641e8c3e33fabae18cdfc52a43da948c8 100644 (file)
@@ -1,4 +1,4 @@
-package club.wpia.gigi.util;
+package club.wpia.gigi.passwords;
 
 import java.util.Arrays;
 import java.util.TreeSet;
@@ -9,7 +9,7 @@ import club.wpia.gigi.dbObjects.Name;
 import club.wpia.gigi.dbObjects.NamePart;
 import club.wpia.gigi.output.template.SprintfCommand;
 
-public class PasswordStrengthChecker {
+public class PasswordStrengthChecker implements PasswordChecker {
 
     private static Pattern digits = Pattern.compile("\\d");
 
@@ -21,9 +21,13 @@ public class PasswordStrengthChecker {
 
     private static Pattern special = Pattern.compile("(?!\\s)\\W");
 
-    private PasswordStrengthChecker() {}
+    public PasswordStrengthChecker() {}
 
-    private static int checkpwlight(String pw) {
+    /**
+     * @param pw The password.
+     * @return Estimate of the password’s strength (positive).
+     */
+    private int ratePasswordStrength(String pw) {
         int points = 0;
         if (pw.length() > 15) {
             points++;
@@ -55,32 +59,39 @@ public class PasswordStrengthChecker {
         return points;
     }
 
-    public static int checkpw(String pw, String[] nameParts, String email) {
-        if (pw == null) {
-            return 0;
-        }
-        int light = checkpwlight(pw);
+    /**
+     * @param pw The password.
+     * @param nameParts The name parts of the user.
+     * @param email The email address of the user.
+     * @return Estimate of the password’s weakness (negative).
+     */
+    private int ratePasswordWeakness(String pw, String[] nameParts, String email) {
+        int points = 0;
         if (contained(pw, email)) {
-            light -= 2;
+            points -= 2;
         }
         for (int i = 0; i < nameParts.length; i++) {
             if (contained(pw, nameParts[i])) {
-                light -= 2;
+                points -= 2;
             }
         }
-        // TODO dictionary check
-        return light;
+        return points;
     }
 
-    public static void assertStrongPassword(String pw, Name[] names, String email) throws GigiApiException {
-        TreeSet<String> parts = new TreeSet<>();
-        for (int i = 0; i < names.length; i++) {
-            for (NamePart string : names[i].getParts()) {
-                parts.add(string.getValue());
-            }
-        }
-        if (checkpw(pw, parts.toArray(new String[parts.size()]), email) < 3) {
-            throw (new GigiApiException(new SprintfCommand("The Password you submitted failed to contain enough differing characters and/or contained words from your name and/or email address. For the current requirements and to learn more, visit our {0}FAQ{1}.", Arrays.asList("!(/kb/goodPassword", "!'</a>'"))));
+    public int ratePassword(String pw, String[] nameParts, String email) {
+        return ratePasswordStrength(pw) + ratePasswordWeakness(pw, nameParts, email);
+    }
+
+    @Override
+    public GigiApiException checkPassword(String password, String[] nameParts, String email) {
+        int points = ratePassword(password, nameParts, email);
+        if (points < 3) {
+            return new GigiApiException(new SprintfCommand(
+                "The Password you submitted failed to contain enough differing characters and/or contained words from your name and/or email address. For the current requirements and to learn more, visit our {0}FAQ{1}.",
+                Arrays.asList("!(/kb/goodPassword", "!'</a>'")
+            ));
+        } else {
+            return null;
         }
     }
 
diff --git a/tests/club/wpia/gigi/passwords/TestDelegatingPasswordChecker.java b/tests/club/wpia/gigi/passwords/TestDelegatingPasswordChecker.java
new file mode 100644 (file)
index 0000000..250758a
--- /dev/null
@@ -0,0 +1,77 @@
+package club.wpia.gigi.passwords;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import club.wpia.gigi.GigiApiException;
+import club.wpia.gigi.passwords.DelegatingPasswordChecker;
+import club.wpia.gigi.passwords.PasswordChecker;
+
+public class TestDelegatingPasswordChecker {
+
+    @Test
+    public void testNoCheckers() {
+        DelegatingPasswordChecker checker = new DelegatingPasswordChecker(new PasswordChecker[0]);
+
+        assertNull(checker.checkPassword("", new String[0], ""));
+    }
+
+    @Test
+    public void testOneChecker() {
+        DelegatingPasswordChecker checker = new DelegatingPasswordChecker(new PasswordChecker[] {
+            new PasswordChecker() {
+                @Override
+                public GigiApiException checkPassword(String password, String[] nameParts, String email) {
+                    return password.isEmpty() ?
+                        new GigiApiException("empty password") :
+                        null;
+                }
+            }
+        });
+
+        assertNull(checker.checkPassword("a strong password", new String[0], ""));
+
+        GigiApiException exception = checker.checkPassword("", new String[0], "");
+        assertNotNull(exception);
+        assertEquals("empty password", exception.getMessage());
+    }
+
+    @Test
+    public void testTwoCheckers() {
+        DelegatingPasswordChecker checker = new DelegatingPasswordChecker(new PasswordChecker[] {
+            new PasswordChecker() {
+                @Override
+                public GigiApiException checkPassword(String password, String[] nameParts, String email) {
+                    return password.equals(email) ?
+                        new GigiApiException("password = email") :
+                        null;
+                }
+            },
+            new PasswordChecker() {
+                @Override
+                public GigiApiException checkPassword(String password, String[] nameParts, String email) {
+                    return password.equals("12345") ?
+                        new GigiApiException("12345 is a bad password") :
+                        null;
+                }
+            }
+        });
+
+        assertNull(checker.checkPassword("a strong password", new String[0], "email"));
+
+        GigiApiException exception1 = checker.checkPassword("email", new String[0], "email");
+        assertNotNull(exception1);
+        assertEquals("password = email", exception1.getMessage());
+
+        GigiApiException exception2 = checker.checkPassword("12345", new String[0], "email");
+        assertNotNull(exception2);
+        assertEquals("12345 is a bad password", exception2.getMessage());
+
+        GigiApiException exception3 = checker.checkPassword("12345", new String[0], "12345");
+        assertNotNull(exception3);
+        assertThat(exception3.getMessage(), containsString("password = email"));
+        assertThat(exception3.getMessage(), containsString("12345 is a bad password"));
+    }
+}
similarity index 92%
rename from tests/club/wpia/gigi/util/TestPasswordStrengthChecker.java
rename to tests/club/wpia/gigi/passwords/TestPasswordStrengthChecker.java
index 16e2cbbd61c8248912a081ceba88b43da20894fe..396aa3dcb6c7288210af8321b08cab80b3b3c98b 100644 (file)
@@ -1,11 +1,11 @@
-package club.wpia.gigi.util;
+package club.wpia.gigi.passwords;
 
 import static org.junit.Assert.*;
 
 import org.junit.Test;
 
+import club.wpia.gigi.passwords.PasswordStrengthChecker;
 import club.wpia.gigi.testUtils.ClientBusinessTest;
-import club.wpia.gigi.util.PasswordStrengthChecker;
 
 public class TestPasswordStrengthChecker extends ClientBusinessTest {
 
@@ -14,7 +14,7 @@ public class TestPasswordStrengthChecker extends ClientBusinessTest {
     public TestPasswordStrengthChecker() {}
 
     private int check(String pw) {
-        return PasswordStrengthChecker.checkpw(pw, new String[] {
+        return new PasswordStrengthChecker().ratePassword(pw, new String[] {
                 "fname", "lname", "mname", "suffix"
         }, e);
     }
index 3ece611c8686c17bc011835a8036463d379cbd07..13eaee5da8dffdfe209ba71aafc8d838004b6777 100644 (file)
@@ -32,6 +32,7 @@ import java.util.regex.Pattern;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
+import club.wpia.gigi.Gigi;
 import club.wpia.gigi.GigiApiException;
 import club.wpia.gigi.database.DatabaseConnection;
 import club.wpia.gigi.database.DatabaseConnection.Link;
@@ -42,6 +43,7 @@ import club.wpia.gigi.dbObjects.CertificateProfile;
 import club.wpia.gigi.dbObjects.Domain;
 import club.wpia.gigi.dbObjects.DomainPingType;
 import club.wpia.gigi.dbObjects.User;
+import club.wpia.gigi.passwords.PasswordStrengthChecker;
 import club.wpia.gigi.testUtils.TestEmailReceiver.TestMail;
 import club.wpia.gigi.util.DatabaseManager;
 import club.wpia.gigi.util.DomainAssessment;
@@ -105,6 +107,7 @@ public abstract class ConfiguredTest {
         TimeConditions.init(props);
         DomainAssessment.init(props);
         PasswordHash.init(props);
+        Gigi.setPasswordChecker(new PasswordStrengthChecker());
 
         if ( !DatabaseConnection.isInited()) {
             DatabaseConnection.init(testProps);