diff --git a/src/main/java/helpers/ScryptHelper.java b/src/main/java/helpers/ScryptHelper.java index 134dd82..bf4c903 100644 --- a/src/main/java/helpers/ScryptHelper.java +++ b/src/main/java/helpers/ScryptHelper.java @@ -3,6 +3,7 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; @@ -14,48 +15,65 @@ /** * Wrapper for Bouncy Castle's scrypt implementation. * - * Generates salted scrypt hashes in modular crypt format (MCF). + * Generates salted scrypt hashes in a format similar to Modular Crypt Format (MCF). * * @author Mark George */ -public class ScryptHelper { +public final class ScryptHelper { // the standard work factors for scrypt private static final int N = 16384; private static final int r = 8; private static final int p = 1; - private static final int saltSize = 32; - private static final int dkLen = 32; + private static final int saltSize = 64; + private static final int dkLen = 64; + + private static final Charset utf8 = StandardCharsets.UTF_8; private ScryptHelper() { } /** - * Generates an MCF formatted salted scrypt hash for the password. + * Generates an MCF-like formatted salted scrypt hash for the password. * * @param password The password to hash. * @return The MCF formatted hash. */ - public static char[] hash(char[] password) { + public static CharBuffer hash(CharSequence password) { byte[] salt = salt(); byte[] hash = hash(password, salt, N, r, p, dkLen); - Base64.Encoder encoder = Base64.getEncoder(); + Base64.Encoder b64encoder = Base64.getEncoder(); int costParams = (log2(N)) << 16 | r << 8 | p; + byte[] dollar = "$".getBytes(utf8); + byte[] params = String.valueOf(costParams).getBytes(utf8); + byte[] b64salt = b64encoder.encode(salt); + byte[] b64hash = b64encoder.encode(hash); - CharBuffer mcf = CharBuffer.allocate(2 * (saltSize + dkLen)); - mcf.append("$").append(String.valueOf(costParams)); - mcf.append("$").append(encoder.encodeToString(salt)); - mcf.append("$").append(encoder.encodeToString(hash)); - mcf.append("$"); + int size = (4 * dollar.length) + params.length + b64salt.length + b64hash.length; - return mcf.array(); + ByteBuffer mcf = ByteBuffer.allocate(size); + mcf.put(dollar).put(params); + mcf.put(dollar).put(b64salt); + mcf.put(dollar).put(b64hash); + mcf.put(dollar); + mcf.flip(); + + CharBuffer result = utf8.decode(mcf); + + Arrays.fill(hash, (byte) 0); + Arrays.fill(salt, (byte) 0); + Arrays.fill(b64hash, (byte) 0); + Arrays.fill(b64salt, (byte) 0); + Arrays.fill(mcf.array(), (byte) 0); + + return result; } /** - * Checks an MCF formatted hash (as generated by the hash method) against a + * Checks a hash (as generated by the hash method) against a * password. * * @param mcfHash The MCF formatted hash. @@ -63,12 +81,11 @@ * * @return True if the password matches the hash, false if not. */ - public static boolean check(char[] mcfHash, char[] password) { - Pattern regex = Pattern.compile("\\$(\\d+?)\\$(.+?)\\$(.+?)\\$.*"); + public static boolean check(CharSequence mcfHash, CharSequence password) { - CharBuffer mcfHashCb = CharBuffer.wrap(mcfHash); + Pattern regex = Pattern.compile("\\$(\\d+?)\\$(.+?)\\$(.+?)\\$"); - Matcher matcher = regex.matcher(mcfHashCb); + Matcher matcher = regex.matcher(mcfHash); if (matcher.matches()) { int costParams = Integer.parseInt(matcher.group(1)); @@ -98,30 +115,21 @@ } - /** - * Overwrites the contents of a char array with zeros. - * - * @param chars char array to zero. - */ - public static void zero(char[] chars) { - Arrays.fill(chars, '0'); - } - - static byte[] hash(char[] password, byte[] salt, int N, int r, int p, int dkLen) { + static byte[] hash(CharSequence password, byte[] salt, int N, int r, int p, int dkLen) { return SCrypt.generate(toBytes(password), salt, N, r, p, dkLen); } - static byte[] salt() { + private static byte[] salt() { try { byte[] salt = new byte[saltSize]; SecureRandom.getInstance("SHA1PRNG").nextBytes(salt); return salt; } catch (NoSuchAlgorithmException ex) { - throw new RuntimeException(ex); + throw new IllegalStateException("SHA1PRNG not supported on this JVM!", ex); } } - static int log2(int operand) { + private static int log2(int operand) { int result = 0; do { @@ -135,34 +143,12 @@ /* * Source: https://stackoverflow.com/a/9670279 */ - static byte[] toBytes(char[] chars) { + private static byte[] toBytes(CharSequence chars) { CharBuffer charBuffer = CharBuffer.wrap(chars); - ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer); + ByteBuffer byteBuffer = utf8.encode(charBuffer); byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); Arrays.fill(byteBuffer.array(), (byte) 0); return bytes; } - public static void main(String[] args) throws Exception { - // standard vector test - byte[] hash = hash("pleaseletmein".toCharArray(), "SodiumChloride".getBytes("UTF-8"), 1 << 14, 8, 1, 64); - - // output should match https://tools.ietf.org/html/rfc7914.html#page-13 - for (byte b : hash) { - System.out.printf("%02x ", b); - } - System.out.println(); - - char[] aHash = hash("testing321".toCharArray()); - System.out.println(aHash); - - System.out.println("Check (good password): " + check(aHash, "testing321".toCharArray())); - System.out.println("Check (bad password): " + check(aHash, "testing123".toCharArray())); - - char[] bloogy = "bloogy".toCharArray(); - System.out.println(bloogy); - zero(bloogy); - System.out.println(bloogy); - } - }