GitBucket
4.21.2
Toggle navigation
Snippets
Sign in
Files
Branches
1
Releases
Issues
2
Pull requests
Labels
Priorities
Milestones
Wiki
Forks
mark.george
/
BouncyScrypt
Browse code
Updated deps, upped work factor, add timing tests
master
1 parent
793e4c9
commit
4af74161a37d20c56cf4971d74fc178770fd429e
Chris Edwards
authored
on 9 Jul 2021
Patch
Showing
3 changed files
build.gradle
src/main/java/helpers/ScryptHelper.java
src/test/java/helpers/TestScryptHelper.java
Ignore Space
Show notes
View
build.gradle
plugins { id 'java' } repositories { mavenCentral() } dependencies { implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69' testImplementation group: 'junit', name: 'junit', version: '4.13.2' testImplementation group: 'org.hamcrest', name: 'hamcrest', version: '2.2' }
plugins { id 'java' } repositories { jcenter() } dependencies { compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.66' testImplementation group: 'junit', name: 'junit', version: '4.12' testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3' } test.onlyIf { project.gradle.startParameter.taskNames.contains(':test'); }
Ignore Space
Show notes
View
src/main/java/helpers/ScryptHelper.java
package helpers; 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; 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 a format similar to Modular Crypt Format * (MCF). * * @author Mark George <mark.george@otago.ac.nz> */ public final class ScryptHelper { // the standard work factors for scrypt private static final int N = 32768; private static final int r = 8; private static final int p = 1; 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-like formatted salted scrypt hash for the password. * * @param password The password to hash. * @return The MCF formatted hash. */ public static CharBuffer hash(CharSequence password) { byte[] salt = salt(); byte[] hash = hash(password, salt, N, r, p, dkLen); 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); int size = (4 * dollar.length) + params.length + b64salt.length + b64hash.length; 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 a 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(CharSequence mcfHash, CharSequence password) { Pattern regex = Pattern.compile("\\$(\\d+?)\\$(.+?)\\$(.+?)\\$"); Matcher matcher = regex.matcher(mcfHash); 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!"); } } 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); } private static byte[] salt() { try { byte[] salt = new byte[saltSize]; SecureRandom.getInstance("SHA1PRNG").nextBytes(salt); return salt; } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA1PRNG not supported on this JVM!", ex); } } private static int log2(int operand) { double log2 = Math.log(operand) / Math.log(2); if (log2 % 1 != 0) { throw new IllegalArgumentException("N must be a power of 2."); } else { return Math.toIntExact(Math.round(log2)); } } /* * Source: https://stackoverflow.com/a/9670279 */ private static byte[] toBytes(CharSequence chars) { CharBuffer charBuffer = CharBuffer.wrap(chars); ByteBuffer byteBuffer = utf8.encode(charBuffer); byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); Arrays.fill(byteBuffer.array(), (byte) 0); return bytes; } }
package helpers; 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; 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 a format similar to Modular Crypt Format * (MCF). * * @author Mark George <mark.george@otago.ac.nz> */ 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 = 64; private static final int dkLen = 64; private static final Charset utf8 = StandardCharsets.UTF_8; private ScryptHelper() { } /** * Generates an MCF-like formatted salted scrypt hash for the password. * * @param password The password to hash. * @return The MCF formatted hash. */ public static CharBuffer hash(CharSequence password) { byte[] salt = salt(); byte[] hash = hash(password, salt, N, r, p, dkLen); 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); int size = (4 * dollar.length) + params.length + b64salt.length + b64hash.length; 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 a 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(CharSequence mcfHash, CharSequence password) { Pattern regex = Pattern.compile("\\$(\\d+?)\\$(.+?)\\$(.+?)\\$"); Matcher matcher = regex.matcher(mcfHash); 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!"); } } 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); } private static byte[] salt() { try { byte[] salt = new byte[saltSize]; SecureRandom.getInstance("SHA1PRNG").nextBytes(salt); return salt; } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA1PRNG not supported on this JVM!", ex); } } private static int log2(int operand) { double log2 = Math.log(operand) / Math.log(2); if (log2 % 1 != 0) { throw new IllegalArgumentException("N must be a power of 2."); } else { return Math.toIntExact(Math.round(log2)); } } /* * Source: https://stackoverflow.com/a/9670279 */ private static byte[] toBytes(CharSequence chars) { CharBuffer charBuffer = CharBuffer.wrap(chars); ByteBuffer byteBuffer = utf8.encode(charBuffer); byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); Arrays.fill(byteBuffer.array(), (byte) 0); return bytes; } }
Ignore Space
Show notes
View
src/test/java/helpers/TestScryptHelper.java
package helpers; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import org.junit.Test; public class TestScryptHelper { private static final Charset utf8 = StandardCharsets.UTF_8; @Test public void testStandardVector1() throws Exception { /* Check against the test vectors described at: https://tools.ietf.org/html/rfc7914#section-12 */ byte[] standardVector = new byte[]{(byte) 0x70, (byte) 0x23, (byte) 0xbd, (byte) 0xcb, (byte) 0x3a, (byte) 0xfd, (byte) 0x73, (byte) 0x48, (byte) 0x46, (byte) 0x1c, (byte) 0x06, (byte) 0xcd, (byte) 0x81, (byte) 0xfd, (byte) 0x38, (byte) 0xeb, (byte) 0xfd, (byte) 0xa8, (byte) 0xfb, (byte) 0xba, (byte) 0x90, (byte) 0x4f, (byte) 0x8e, (byte) 0x3e, (byte) 0xa9, (byte) 0xb5, (byte) 0x43, (byte) 0xf6, (byte) 0x54, (byte) 0x5d, (byte) 0xa1, (byte) 0xf2, (byte) 0xd5, (byte) 0x43, (byte) 0x29, (byte) 0x55, (byte) 0x61, (byte) 0x3f, (byte) 0x0f, (byte) 0xcf, (byte) 0x62, (byte) 0xd4, (byte) 0x97, (byte) 0x05, (byte) 0x24, (byte) 0x2a, (byte) 0x9a, (byte) 0xf9, (byte) 0xe6, (byte) 0x1e, (byte) 0x85, (byte) 0xdc, (byte) 0x0d, (byte) 0x65, (byte) 0x1e, (byte) 0x40, (byte) 0xdf, (byte) 0xcf, (byte) 0x01, (byte) 0x7b, (byte) 0x45, (byte) 0x57, (byte) 0x58, (byte) 0x87}; String password = "pleaseletmein"; byte[] salt = "SodiumChloride".getBytes(utf8); byte[] hash = ScryptHelper.hash(password, salt, 16384, 8, 1, 64); assertThat(hash, is(standardVector)); } @Test public void testStandardVector2() throws Exception { /* Check against the test vectors described at: https://tools.ietf.org/html/rfc7914#section-12 */ byte[] standardVector = new byte[]{(byte) 0xfd, (byte) 0xba, (byte) 0xbe, (byte) 0x1c, (byte) 0x9d, (byte) 0x34, (byte) 0x72, (byte) 0x00, (byte) 0x78, (byte) 0x56, (byte) 0xe7, (byte) 0x19, (byte) 0x0d, (byte) 0x01, (byte) 0xe9, (byte) 0xfe, (byte) 0x7c, (byte) 0x6a, (byte) 0xd7, (byte) 0xcb, (byte) 0xc8, (byte) 0x23, (byte) 0x78, (byte) 0x30, (byte) 0xe7, (byte) 0x73, (byte) 0x76, (byte) 0x63, (byte) 0x4b, (byte) 0x37, (byte) 0x31, (byte) 0x62, (byte) 0x2e, (byte) 0xaf, (byte) 0x30, (byte) 0xd9, (byte) 0x2e, (byte) 0x22, (byte) 0xa3, (byte) 0x88, (byte) 0x6f, (byte) 0xf1, (byte) 0x09, (byte) 0x27, (byte) 0x9d, (byte) 0x98, (byte) 0x30, (byte) 0xda, (byte) 0xc7, (byte) 0x27, (byte) 0xaf, (byte) 0xb9, (byte) 0x4a, (byte) 0x83, (byte) 0xee, (byte) 0x6d, (byte) 0x83, (byte) 0x60, (byte) 0xcb, (byte) 0xdf, (byte) 0xa2, (byte) 0xcc, (byte) 0x06, (byte) 0x40}; String password = "password"; byte[] salt = "NaCl".getBytes(utf8); byte[] hash = ScryptHelper.hash(password, salt, 1024, 8, 16, 64); assertThat(hash, is(standardVector)); } @Test public void testUTF8Password() throws Exception { String password = "\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testSimplePassword() throws Exception { String password = "testing123"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testLongPassword() throws Exception { String password = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testEmptyPassword() throws Exception { String password = ""; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testBadPassword() throws Exception { String password = "testing123"; String bad = "testing321"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, bad); assertThat(result, is(false)); } @Test public void testCharBufferPassword() throws Exception { CharBuffer password = CharBuffer.wrap("testing123"); String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testHashTiming() throws Exception { final int REPS = 100; long startTime = System.currentTimeMillis(); for (int i = 0; i < REPS; i++) { ScryptHelper.hash("wibble" + i); } long endTime = System.currentTimeMillis(); System.out.println("Mean time per hash(): " + (endTime - startTime) / (float)REPS + " ms"); } @Test public void testCheckTiming() throws Exception { final int REPS = 100; CharBuffer hash = ScryptHelper.hash("wibble42"); long startTime = System.currentTimeMillis(); for (int i = 0; i < REPS; i++) { ScryptHelper.check(hash, "wibble" + i); } long endTime = System.currentTimeMillis(); System.out.println("Mean time per check(): " + (endTime - startTime) / (float)REPS + " ms"); } }
package helpers; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import org.junit.Test; public class TestScryptHelper { private static final Charset utf8 = StandardCharsets.UTF_8; @Test public void testStandardVector1() throws Exception { /* Check against the test vectors described at: https://tools.ietf.org/html/rfc7914#section-12 */ byte[] standardVector = new byte[]{(byte) 0x70, (byte) 0x23, (byte) 0xbd, (byte) 0xcb, (byte) 0x3a, (byte) 0xfd, (byte) 0x73, (byte) 0x48, (byte) 0x46, (byte) 0x1c, (byte) 0x06, (byte) 0xcd, (byte) 0x81, (byte) 0xfd, (byte) 0x38, (byte) 0xeb, (byte) 0xfd, (byte) 0xa8, (byte) 0xfb, (byte) 0xba, (byte) 0x90, (byte) 0x4f, (byte) 0x8e, (byte) 0x3e, (byte) 0xa9, (byte) 0xb5, (byte) 0x43, (byte) 0xf6, (byte) 0x54, (byte) 0x5d, (byte) 0xa1, (byte) 0xf2, (byte) 0xd5, (byte) 0x43, (byte) 0x29, (byte) 0x55, (byte) 0x61, (byte) 0x3f, (byte) 0x0f, (byte) 0xcf, (byte) 0x62, (byte) 0xd4, (byte) 0x97, (byte) 0x05, (byte) 0x24, (byte) 0x2a, (byte) 0x9a, (byte) 0xf9, (byte) 0xe6, (byte) 0x1e, (byte) 0x85, (byte) 0xdc, (byte) 0x0d, (byte) 0x65, (byte) 0x1e, (byte) 0x40, (byte) 0xdf, (byte) 0xcf, (byte) 0x01, (byte) 0x7b, (byte) 0x45, (byte) 0x57, (byte) 0x58, (byte) 0x87}; String password = "pleaseletmein"; byte[] salt = "SodiumChloride".getBytes(utf8); byte[] hash = ScryptHelper.hash(password, salt, 16384, 8, 1, 64); assertThat(hash, is(standardVector)); } @Test public void testStandardVector2() throws Exception { /* Check against the test vectors described at: https://tools.ietf.org/html/rfc7914#section-12 */ byte[] standardVector = new byte[]{(byte) 0xfd, (byte) 0xba, (byte) 0xbe, (byte) 0x1c, (byte) 0x9d, (byte) 0x34, (byte) 0x72, (byte) 0x00, (byte) 0x78, (byte) 0x56, (byte) 0xe7, (byte) 0x19, (byte) 0x0d, (byte) 0x01, (byte) 0xe9, (byte) 0xfe, (byte) 0x7c, (byte) 0x6a, (byte) 0xd7, (byte) 0xcb, (byte) 0xc8, (byte) 0x23, (byte) 0x78, (byte) 0x30, (byte) 0xe7, (byte) 0x73, (byte) 0x76, (byte) 0x63, (byte) 0x4b, (byte) 0x37, (byte) 0x31, (byte) 0x62, (byte) 0x2e, (byte) 0xaf, (byte) 0x30, (byte) 0xd9, (byte) 0x2e, (byte) 0x22, (byte) 0xa3, (byte) 0x88, (byte) 0x6f, (byte) 0xf1, (byte) 0x09, (byte) 0x27, (byte) 0x9d, (byte) 0x98, (byte) 0x30, (byte) 0xda, (byte) 0xc7, (byte) 0x27, (byte) 0xaf, (byte) 0xb9, (byte) 0x4a, (byte) 0x83, (byte) 0xee, (byte) 0x6d, (byte) 0x83, (byte) 0x60, (byte) 0xcb, (byte) 0xdf, (byte) 0xa2, (byte) 0xcc, (byte) 0x06, (byte) 0x40}; String password = "password"; byte[] salt = "NaCl".getBytes(utf8); byte[] hash = ScryptHelper.hash(password, salt, 1024, 8, 16, 64); assertThat(hash, is(standardVector)); } @Test public void testUTF8Password() throws Exception { String password = "\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testSimplePassword() throws Exception { String password = "testing123"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testLongPassword() throws Exception { String password = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testEmptyPassword() throws Exception { String password = ""; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } @Test public void testBadPassword() throws Exception { String password = "testing123"; String bad = "testing321"; String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, bad); assertThat(result, is(false)); } @Test public void testCharBufferPassword() throws Exception { CharBuffer password = CharBuffer.wrap("testing123"); String hash = ScryptHelper.hash(password).toString(); boolean result = ScryptHelper.check(hash, password); assertThat(result, is(true)); } }
Show line notes below