]> WPIA git - gigi.git/blob - src/club/wpia/gigi/passwords/PasswordHashChecker.java
add: PasswordHashChecker implementation
[gigi.git] / src / club / wpia / gigi / passwords / PasswordHashChecker.java
1 package club.wpia.gigi.passwords;
2
3 import java.io.IOException;
4 import java.nio.ByteBuffer;
5 import java.nio.channels.FileChannel;
6 import java.nio.charset.Charset;
7 import java.security.MessageDigest;
8 import java.util.Arrays;
9
10 import club.wpia.gigi.GigiApiException;
11 import club.wpia.gigi.output.template.SprintfCommand;
12
13 /**
14  * A {@link PasswordChecker} which searches for a password
15  * in a given file containing hashes of known breached passwords.
16  * If the password is found, it is rejected.
17  */
18 public class PasswordHashChecker implements PasswordChecker {
19
20     private final FileChannel database;
21
22     private final MessageDigest digest;
23
24     private final int digestLength;
25
26     private final ByteBuffer hashBuffer;
27
28     /**
29      * @param database The database of password hashes.
30      *                 It should contain the hashes in binary format, with no separating characters,
31      *                 sorted in ascending order.
32      * @param digest   The digest function that was used to compile the database.
33      */
34     public PasswordHashChecker(FileChannel database, MessageDigest digest) {
35         this.database = database;
36         this.digest = digest;
37         this.digestLength = digest.getDigestLength();
38         this.hashBuffer = ByteBuffer.allocate(digestLength);
39     }
40
41     @Override
42     public GigiApiException checkPassword(String password, String[] nameParts, String email) {
43         byte[] passwordHash = passwordHash(password);
44         boolean knownPasswordHash;
45         try {
46             knownPasswordHash = knownPasswordHash(passwordHash);
47         } catch (IOException e) {
48             System.err.printf(
49                 "Error while reading password hash database, ACCEPTING POTENTIALLY KNOWN PASSWORD for user with email %s.",
50                 email
51             );
52             e.printStackTrace();
53             knownPasswordHash = false;
54         }
55         if (knownPasswordHash) {
56             return new GigiApiException(new SprintfCommand(
57                 "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.",
58                 Arrays.asList("!(/kb/knownPasswordHash", "!'</a>'")
59             ));
60         } else {
61             return null;
62         }
63     }
64
65     private byte[] passwordHash(String password) {
66         byte[] passwordBytes = password.getBytes(Charset.forName("UTF-8"));
67         byte[] passwordHash;
68         synchronized (digest) {
69             digest.reset();
70             digest.update(passwordBytes);
71             passwordHash = digest.digest();
72         }
73         return passwordHash;
74     }
75
76     private boolean knownPasswordHash(byte[] passwordHash) throws IOException {
77         long targetEstimate = estimateHashOffset(passwordHash);
78         long bestGuess = targetEstimate;
79
80         hashBuffer.clear();
81         database.read(hashBuffer, bestGuess);
82         // first, guess the location of the hash within the file
83         for (int i = 0; i < 4; i++) {
84             long bestGuessEstimate = estimateHashOffset(hashBuffer.array());
85             if (bestGuessEstimate == targetEstimate) {
86                 break;
87             }
88             bestGuess = bestGuess + targetEstimate - bestGuessEstimate;
89             hashBuffer.clear();
90             database.read(hashBuffer, bestGuess);
91         }
92         int searchDirection = compareHashes(passwordHash, hashBuffer.array());
93         if (searchDirection == 0) {
94             return true;
95         }
96         // then, do a linear search from that location until we’ve either found or moved past the hash
97         int newSearchDirection = searchDirection;
98         while (searchDirection == newSearchDirection) {
99             bestGuess += digestLength * searchDirection;
100             hashBuffer.clear();
101             database.read(hashBuffer, bestGuess);
102             newSearchDirection = compareHashes(passwordHash, hashBuffer.array());
103         }
104         return newSearchDirection == 0;
105     }
106
107     private int compareHashes(byte[] left, byte[] right) {
108         for (int i = 0; i < digestLength; i++) {
109             int leftByte = Byte.toUnsignedInt(left[i]);
110             int rightByte = Byte.toUnsignedInt(right[i]);
111             if (leftByte < rightByte) {
112                 return -1;
113             }
114             if (leftByte > rightByte) {
115                 return 1;
116             }
117         }
118         return 0;
119     }
120
121     private long estimateHashOffset(byte[] hash) throws IOException {
122         long pos = (database.size()
123                 * ((hash[0] & 0xFF) << 8 | (hash[1] & 0xFF)))
124                 / (1L << 16);
125         pos += (database.size()
126                 * ((hash[2] & 0xFF) << 8 | (hash[3] & 0xFF)))
127                 / (1L << 32);
128         return (pos / digestLength) * digestLength;
129     }
130 }