package helpers; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bouncycastle.crypto.generators.SCrypt; /** * Wrapper for Bouncy Castle's scrypt implementation. * * Generates salted scrypt hashes in modular crypt format (MCF). * * @author Mark George <mark.george@otago.ac.nz> */ public 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 ScryptHelper() { } /** * Generates an MCF formatted salted scrypt hash for the password. * * @param password The password to hash. * @return The MCF formatted hash. */ public static char[] hash(char[] password) { byte[] salt = salt(); byte[] hash = hash(password, salt, N, r, p, dkLen); Base64.Encoder encoder = Base64.getEncoder(); int costParams = (log2(N)) << 16 | r << 8 | p; 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("$"); return mcf.array(); } /** * Checks an MCF formatted hash (as generated by the hash method) against a * password. * * @param mcfHash The MCF formatted hash. * @param password The password. * * @return True if the password matches the hash, false if not. */ public static boolean check(char[] mcfHash, char[] password) { Pattern regex = Pattern.compile("\\$(\\d+?)\\$(.+?)\\$(.+?)\\$.*"); CharBuffer mcfHashCb = CharBuffer.wrap(mcfHash); Matcher matcher = regex.matcher(mcfHashCb); if (matcher.matches()) { int costParams = Integer.parseInt(matcher.group(1)); Base64.Decoder decoder = Base64.getDecoder(); byte[] salt = decoder.decode(matcher.group(2)); byte[] hash = decoder.decode(matcher.group(3)); int hN = 1 << (costParams >> 16); 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; } } return true; } else { throw new IllegalArgumentException("Hash is not in a recognisable format!"); } } /** * 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) { return SCrypt.generate(toBytes(password), salt, N, r, p, dkLen); } static byte[] salt() { try { byte[] salt = new byte[saltSize]; SecureRandom.getInstance("SHA1PRNG").nextBytes(salt); return salt; } catch (NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } } static int log2(int operand) { int result = 0; do { operand = operand >> 1; result++; } while (operand > 1); return result; } /* * Source: https://stackoverflow.com/a/9670279 */ static byte[] toBytes(char[] chars) { CharBuffer charBuffer = CharBuffer.wrap(chars); ByteBuffer byteBuffer = Charset.forName("UTF-8").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); } }