Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 118 additions & 50 deletions src/main/java/com/lambdaworks/crypto/SCryptUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.lambdaworks.codec.Base64.*;

Expand All @@ -31,9 +33,11 @@
* @author Will Glozer
*/
public class SCryptUtil {

/**
* Hash the supplied plaintext password and generate output in the format described
* in {@link SCryptUtil}.
* in {@link SCryptUtil}. This method maybe unsafe as it uses Strings, which are not guaranteed to be
* freed immediately.
*
* @param passwd Password.
* @param N CPU cost parameter.
Expand All @@ -45,46 +49,69 @@ public class SCryptUtil {
public static String scrypt(String passwd, int N, int r, int p) {
byte[] bytes = passwd.getBytes(StandardCharsets.UTF_8);
try {
return scryptInternal(bytes, N, r, p);
return new String(scrypt(bytes, N, r, p), StandardCharsets.UTF_8);
} finally {
wipeArray(bytes);
}
}

/**
* Hash the supplied plaintext password and generate output in the format described
* in {@link SCryptUtil}.
* in {@link SCryptUtil}. This call will aggressively clean up password data in memory.
*
* @param passwd Password in UTF-8 encoding.
* @param N CPU cost parameter.
* @param r Memory cost parameter.
* @param p Parallelization parameter.
*
* @param passwd Password.
* @param N CPU cost parameter.
* @param r Memory cost parameter.
* @param p Parallelization parameter.
* @return The hashed password.
*/
public static String scrypt(char[] passwd, int N, int r, int p) {
byte[] bytes = toBytes(passwd);
try {
return scryptInternal(bytes, N, r, p);
} finally {
wipeArray(bytes);
}
}

private static String scryptInternal(byte[] passwordBytes, int N, int r, int p) {
public static byte[] scrypt(byte[] passwordBytes, int N, int r, int p) {
try {
byte[] salt = new byte[16];
SecureRandom.getInstance("SHA1PRNG").nextBytes(salt);

byte[] derived = SCrypt.scrypt(passwordBytes, salt, N, r, p, 32);

String params = Long.toString(log2(N) << 16L | r << 8 | p, 16);

StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2);
sb.append("$s0$").append(params).append('$');
sb.append(encode(salt)).append('$');
sb.append(encode(derived));

return sb.toString();
byte[] params = Long.toString(log2(N) << 16L | r << 8 | p, 16).getBytes(StandardCharsets.UTF_8);

byte[] prefix = "$s0$".getBytes(StandardCharsets.UTF_8);
byte[] dollar = "$".getBytes(StandardCharsets.UTF_8);
final char[] charEncodedSalt = encode(salt);
byte[] byteEncodedSalt = toBytes(charEncodedSalt);
wipeArray(charEncodedSalt);
final char[] charEncodedDerived = encode(derived);
byte[] byteEncodedDerived = toBytes(charEncodedDerived);
wipeArray(charEncodedDerived);

byte[] result = new byte[prefix.length
+ params.length
+ dollar.length
+ byteEncodedSalt.length
+ dollar.length
+ byteEncodedDerived.length];
System.arraycopy(prefix, 0, result, 0, prefix.length);
System.arraycopy(params, 0, result, prefix.length, params.length);
System.arraycopy(dollar, 0, result, prefix.length
+ params.length, dollar.length);
System.arraycopy(byteEncodedSalt, 0, result, prefix.length
+ params.length
+ dollar.length, byteEncodedSalt.length);
System.arraycopy(dollar, 0, result, prefix.length
+ params.length
+ dollar.length
+ byteEncodedSalt.length, dollar.length);
System.arraycopy(byteEncodedDerived, 0, result, prefix.length
+ params.length
+ dollar.length
+ byteEncodedSalt.length
+ dollar.length, byteEncodedDerived.length);
wipeArray(salt);
wipeArray(derived);
wipeArray(params);
wipeArray(byteEncodedSalt);
wipeArray(byteEncodedDerived);
return result;
} catch (GeneralSecurityException e) {
throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
}
Expand All @@ -102,47 +129,42 @@ private static byte[] toBytes(char[] chars) {

/**
* Compare the supplied plaintext password to a hashed password.
* This method maybe unsafe as it uses Strings, which are not guaranteed to be
* freed immediatelly.
*
* @param passwd Plaintext password.
* @param hashed scrypt hashed password.
*
* @return true if passwd matches hashed value.
*/
public static boolean check(String passwd, String hashed) {
byte[] bytes = passwd.getBytes(StandardCharsets.UTF_8);
try {
return checkInternal(bytes, hashed);
} finally {
wipeArray(bytes);
}
return check(passwd.getBytes(StandardCharsets.UTF_8), hashed.getBytes(StandardCharsets.UTF_8));
}

/**
* Compare the supplied plaintext password to a hashed password.
* This call will aggressively clean up password data in memory.
*
* @param passwd Plaintext password encoded in UTF-8.
* @param hashed scrypt hashed password encoded in UTF-8.
*
* @param passwd Plaintext password.
* @param hashed scrypt hashed password.
* @return true if passwd matches hashed value.
*/
public static boolean check(char[] passwd, String hashed) {
byte[] bytes = toBytes(passwd);
try {
return checkInternal(bytes, hashed);
} finally {
wipeArray(bytes);
}
}

private static boolean checkInternal(byte[] passwordBytes, String hashed) {
public static boolean check(byte[] passwordBytes, byte[] hashed) {
try {
String[] parts = hashed.split("\\$");
byte[][] parts = split(hashed, "$".getBytes(StandardCharsets.UTF_8));

if (parts.length != 5 || !parts[1].equals("s0")) {
if (parts.length != 5 || !Arrays.equals(parts[1], "s0".getBytes(StandardCharsets.UTF_8))) {
throw new IllegalArgumentException("Invalid hashed value");
}

long params = Long.parseLong(parts[2], 16);
byte[] salt = decode(parts[3].toCharArray());
byte[] derived0 = decode(parts[4].toCharArray());
long params = Long.parseLong(new String(parts[2], StandardCharsets.UTF_8), 16);
final char[] charEncodedSalt = toCharArray(parts[3]);
byte[] salt = decode(charEncodedSalt);
wipeArray(charEncodedSalt);
final char[] charEncodedDerived = toCharArray(parts[4]);
byte[] derived0 = decode(charEncodedDerived);
wipeArray(charEncodedDerived);

int N = (int) Math.pow(2, params >> 16 & 0xffff);
int r = (int) params >> 8 & 0xff;
Expand All @@ -156,12 +178,53 @@ private static boolean checkInternal(byte[] passwordBytes, String hashed) {
for (int i = 0; i < derived0.length; i++) {
result |= derived0[i] ^ derived1[i];
}
wipeArray(derived0);
wipeArray(derived1);
return result == 0;
} catch (GeneralSecurityException e) {
throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
}
}

private static char[] toCharArray(byte[] bs) {
char[] result = new char[bs.length];
for(int i = 0; i < bs.length; i++) {
result[i] = (char) bs[i];
}
return result;
}

/**
* Splites a byte array into chunks delimited by <code>separatorBytes</code>, similar
* to {@link String#split}.
*/
private static byte[][] split(byte[] array, byte[] separatorBytes) {
List<byte[]> result = new ArrayList<byte[]>();
int lastSplitIndex = 0;
for(int i = 0; i < array.length - separatorBytes.length; i++)
{
boolean found = false;
int j = 0;
while (j < separatorBytes.length && array[i + j] == separatorBytes[j])
j++;
found = (j == separatorBytes.length);
if(found) {
byte[] substring = new byte[i - lastSplitIndex];
System.arraycopy(array, lastSplitIndex, substring, 0, i - lastSplitIndex);
result.add(substring);
lastSplitIndex = i + j;
} else if(array.length == i + separatorBytes.length + 1) {
byte[] substring = new byte[i + separatorBytes.length + 1 - lastSplitIndex];
System.arraycopy(array, lastSplitIndex, substring, 0, i + separatorBytes.length + 1 - lastSplitIndex);
result.add(substring);
}
}
if(result.isEmpty()) {
result.add(array);
}
return result.toArray(new byte[0][]);
}

private static int log2(int n) {
int log = 0;
if ((n & 0xffff0000 ) != 0) { n >>>= 16; log = 16; }
Expand All @@ -174,4 +237,9 @@ private static int log2(int n) {
private static void wipeArray(byte[] array) {
Arrays.fill(array, (byte) 0);
}

private static void wipeArray(char[] array) {
Arrays.fill(array, (char) 0);
}

}