diff --git a/src/main/java/helpers/ScryptHelper.java b/src/main/java/helpers/ScryptHelper.java index 418087d..8cb74a9 100644 --- a/src/main/java/helpers/ScryptHelper.java +++ b/src/main/java/helpers/ScryptHelper.java @@ -84,42 +84,68 @@ */ public static boolean check(CharSequence mcfHash, CharSequence password) { - Pattern regex = Pattern.compile("\\$(\\d+?)\\$(.+?)\\$(.+?)\\$"); + // expected format: $params$salt$hash$ + if (mcfHash == null || mcfHash.length() == 0 || mcfHash.charAt(0) != '$') { + throw new IllegalArgumentException("Hash is not in a recognisable format"); + } - Matcher matcher = regex.matcher(mcfHash); + int[] indices = new int[4]; + int count = 0; - if (matcher.matches()) { - int costParams = Integer.parseInt(matcher.group(1)); + // find positions of the delimiters + for (int i = 0; i < mcfHash.length() && count < 4; i++) { + if (mcfHash.charAt(i) == '$') { + indices[count++] = i; + } + } - Base64.Decoder decoder = Base64.getDecoder(); + if (count != 4) { + throw new IllegalArgumentException("Invalid format: missing delimiters"); + } - byte[] salt = decoder.decode(matcher.group(2)); - byte[] hash = decoder.decode(matcher.group(3)); + CharSequence paramsSeq = mcfHash.subSequence(indices[0] + 1, indices[1]); + int costParams = parseAsInt(paramsSeq); + Base64.Decoder decoder = Base64.getDecoder(); + + CharSequence saltSeq = mcfHash.subSequence(indices[1] + 1, indices[2]); + byte[] salt = decoder.decode(toBytes(saltSeq)); + + CharSequence hashSeq = mcfHash.subSequence(indices[2] + 1, indices[3]); + byte[] hash = decoder.decode(toBytes(hashSeq)); + + try { int hN = 1 << (costParams >> 16); - int hr = costParams >> 8 & 255; + int hr = (costParams >> 8) & 255; int hp = costParams & 255; byte[] cHash = hash(password, salt, hN, hr, hp, hash.length); - for (int i = 0; i < cHash.length; i++) { - if (cHash[i] != hash[i]) { - return false; - } - } + boolean matches = MessageDigest.isEqual(cHash, hash); - boolean result = MessageDigest.isEqual(cHash, hash); - - Arrays.fill(salt, (byte) 0); - Arrays.fill(hash, (byte) 0); Arrays.fill(cHash, (byte) 0); - return result; + return matches; - } else { - throw new IllegalArgumentException("Hash is not in a recognisable format!"); + } finally { + Arrays.fill(salt, (byte) 0); + Arrays.fill(hash, (byte) 0); } + } + /** + * Parses a CharSequence as an integer without creating a String. + */ + private static int parseAsInt(CharSequence s) { + int val = 0; + for (int i = 0; i < s.length(); i++) { + int digit = Character.digit(s.charAt(i), 10); + if (digit < 0) { + throw new NumberFormatException("Invalid digit: " + s.charAt(i)); + } + val = val * 10 + digit; + } + return val; } static byte[] hash(CharSequence password, byte[] salt, int N, int r, int p, int dkLen) { @@ -132,7 +158,7 @@ SecureRandom.getInstanceStrong().nextBytes(salt); return salt; } catch (NoSuchAlgorithmException ex) { - throw new IllegalStateException("SHA1PRNG not supported on this JVM!", ex); + throw new IllegalStateException("Strong RNG not supported on this computer", ex); } }