Newer
Older
BouncyScrypt / src / main / java / helpers / ScryptHelper.java
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);
	}

}