add: optionally check pwned passwords
authorLucas Werkmeister <mail@lucaswerkmeister.de>
Sat, 2 Jun 2018 19:53:15 +0000 (21:53 +0200)
committerLucas Werkmeister <mail@lucaswerkmeister.de>
Tue, 12 Jun 2018 18:31:25 +0000 (20:31 +0200)
A new configuration option is added, specifying the path to a file of
known password hashes which Gigi will refuse to accept for user
accounts. If the option is not specified, Gigi attempts to use the Pwned
Passwords database (see the pwned-passwords-bin package) but continues
startup if the database cannot be opened. This is intended to be useful
for developers: production users should always configure the path to the
file explicitly, so that Gigi will refuse to start if the file cannot be
accessed for whatever reason.

The PasswordHashChecker, if used, is chained behind the usual
PasswordStrengthChecker using a DelegatingPasswordChecker.

Change-Id: I9e54bd45fa35d7ea81d44677f50635d6ab8514e0

config/gigi.properties.template
debian/control
debian/gigi.properties.5
src/club/wpia/gigi/Gigi.java

index 5e939e2..85f2afd 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 54c5b16..fc5499d 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 fc54d8b..c0cc96d 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 44e2ddd..660c3d3 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,7 +83,9 @@ 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;
@@ -277,7 +284,44 @@ public final class Gigi extends HttpServlet {
             this.truststore = truststore;
             pinger = new PingerDaemon(truststore);
             pinger.start();
-            Gigi.passwordChecker = new PasswordStrengthChecker();
+            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();
+            }
         }
     }