add: import scrypt 1.4.0
authorFelix Dörre <felix@dogcraft.de>
Tue, 2 Dec 2014 01:13:21 +0000 (02:13 +0100)
committerJanis Streib <janis@dogcraft.de>
Wed, 31 Dec 2014 01:36:11 +0000 (02:36 +0100)
lib/scrypt/com/lambdaworks/crypto/PBKDF.java [new file with mode: 0644]
lib/scrypt/com/lambdaworks/crypto/SCrypt.java [new file with mode: 0644]
lib/scrypt/com/lambdaworks/crypto/SCryptUtil.java [new file with mode: 0644]
tests/com/lambdaworks/crypto/test/CryptoTestUtil.java [new file with mode: 0644]
tests/com/lambdaworks/crypto/test/PBKDFTest.java [new file with mode: 0644]
tests/com/lambdaworks/crypto/test/SCryptTest.java [new file with mode: 0644]
tests/com/lambdaworks/crypto/test/SCryptUtilTest.java [new file with mode: 0644]

diff --git a/lib/scrypt/com/lambdaworks/crypto/PBKDF.java b/lib/scrypt/com/lambdaworks/crypto/PBKDF.java
new file mode 100644 (file)
index 0000000..43dc2f4
--- /dev/null
@@ -0,0 +1,87 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.GeneralSecurityException;
+import static java.lang.System.arraycopy;
+
+/**
+ * An implementation of the Password-Based Key Derivation Function as specified
+ * in RFC 2898.
+ *
+ * @author  Will Glozer
+ */
+public class PBKDF {
+    /**
+     * Implementation of PBKDF2 (RFC2898).
+     *
+     * @param   alg     HMAC algorithm to use.
+     * @param   P       Password.
+     * @param   S       Salt.
+     * @param   c       Iteration count.
+     * @param   dkLen   Intended length, in octets, of the derived key.
+     *
+     * @return  The derived key.
+     *
+     * @throws  GeneralSecurityException
+     */
+    public static byte[] pbkdf2(String alg, byte[] P, byte[] S, int c, int dkLen) throws GeneralSecurityException {
+        Mac mac = Mac.getInstance(alg);
+        mac.init(new SecretKeySpec(P, alg));
+        byte[] DK = new byte[dkLen];
+        pbkdf2(mac, S, c, DK, dkLen);
+        return DK;
+    }
+
+    /**
+     * Implementation of PBKDF2 (RFC2898).
+     *
+     * @param   mac     Pre-initialized {@link Mac} instance to use.
+     * @param   S       Salt.
+     * @param   c       Iteration count.
+     * @param   DK      Byte array that derived key will be placed in.
+     * @param   dkLen   Intended length, in octets, of the derived key.
+     *
+     * @throws  GeneralSecurityException
+     */
+    public static void pbkdf2(Mac mac, byte[] S, int c, byte[] DK, int dkLen) throws GeneralSecurityException {
+        int hLen = mac.getMacLength();
+
+        if (dkLen > (Math.pow(2, 32) - 1) * hLen) {
+            throw new GeneralSecurityException("Requested key length too long");
+        }
+
+        byte[] U      = new byte[hLen];
+        byte[] T      = new byte[hLen];
+        byte[] block1 = new byte[S.length + 4];
+
+        int l = (int) Math.ceil((double) dkLen / hLen);
+        int r = dkLen - (l - 1) * hLen;
+
+        arraycopy(S, 0, block1, 0, S.length);
+
+        for (int i = 1; i <= l; i++) {
+            block1[S.length + 0] = (byte) (i >> 24 & 0xff);
+            block1[S.length + 1] = (byte) (i >> 16 & 0xff);
+            block1[S.length + 2] = (byte) (i >> 8  & 0xff);
+            block1[S.length + 3] = (byte) (i >> 0  & 0xff);
+
+            mac.update(block1);
+            mac.doFinal(U, 0);
+            arraycopy(U, 0, T, 0, hLen);
+
+            for (int j = 1; j < c; j++) {
+                mac.update(U);
+                mac.doFinal(U, 0);
+
+                for (int k = 0; k < hLen; k++) {
+                    T[k] ^= U[k];
+                }
+            }
+
+            arraycopy(T, 0, DK, (i - 1) * hLen, (i == l ? r : hLen));
+        }
+    }
+}
diff --git a/lib/scrypt/com/lambdaworks/crypto/SCrypt.java b/lib/scrypt/com/lambdaworks/crypto/SCrypt.java
new file mode 100644 (file)
index 0000000..6274238
--- /dev/null
@@ -0,0 +1,214 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto;
+
+import com.lambdaworks.jni.*;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.GeneralSecurityException;
+
+import static java.lang.Integer.MAX_VALUE;
+import static java.lang.System.arraycopy;
+
+/**
+ * An implementation of the <a href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt</a>
+ * key derivation function. This class will attempt to load a native library
+ * containing the optimized C implementation from
+ * <a href="http://www.tarsnap.com/scrypt.html">http://www.tarsnap.com/scrypt.html<a> and
+ * fall back to the pure Java version if that fails.
+ *
+ * @author  Will Glozer
+ */
+public class SCrypt {
+    private static final boolean native_library_loaded;
+
+    static {
+        LibraryLoader loader = LibraryLoaders.loader();
+        native_library_loaded = loader.load("scrypt", true);
+    }
+
+    /**
+     * Implementation of the <a href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt KDF</a>.
+     * Calls the native implementation {@link #scryptN} when the native library was successfully
+     * loaded, otherwise calls {@link #scryptJ}.
+     *
+     * @param passwd    Password.
+     * @param salt      Salt.
+     * @param N         CPU cost parameter.
+     * @param r         Memory cost parameter.
+     * @param p         Parallelization parameter.
+     * @param dkLen     Intended length of the derived key.
+     *
+     * @return The derived key.
+     *
+     * @throws GeneralSecurityException when HMAC_SHA256 is not available.
+     */
+    public static byte[] scrypt(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException {
+        return native_library_loaded ? scryptN(passwd, salt, N, r, p, dkLen) : scryptJ(passwd, salt, N, r, p, dkLen);
+    }
+
+    /**
+     * Native C implementation of the <a href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt KDF</a> using
+     * the code from <a href="http://www.tarsnap.com/scrypt.html">http://www.tarsnap.com/scrypt.html<a>.
+     *
+     * @param passwd    Password.
+     * @param salt      Salt.
+     * @param N         CPU cost parameter.
+     * @param r         Memory cost parameter.
+     * @param p         Parallelization parameter.
+     * @param dkLen     Intended length of the derived key.
+     *
+     * @return The derived key.
+     */
+    public static native byte[] scryptN(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen);
+
+    /**
+     * Pure Java implementation of the <a href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt KDF</a>.
+     *
+     * @param passwd    Password.
+     * @param salt      Salt.
+     * @param N         CPU cost parameter.
+     * @param r         Memory cost parameter.
+     * @param p         Parallelization parameter.
+     * @param dkLen     Intended length of the derived key.
+     *
+     * @return The derived key.
+     *
+     * @throws GeneralSecurityException when HMAC_SHA256 is not available.
+     */
+    public static byte[] scryptJ(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException {
+        if (N < 2 || (N & (N - 1)) != 0) throw new IllegalArgumentException("N must be a power of 2 greater than 1");
+
+        if (N > MAX_VALUE / 128 / r) throw new IllegalArgumentException("Parameter N is too large");
+        if (r > MAX_VALUE / 128 / p) throw new IllegalArgumentException("Parameter r is too large");
+
+        Mac mac = Mac.getInstance("HmacSHA256");
+        mac.init(new SecretKeySpec(passwd, "HmacSHA256"));
+
+        byte[] DK = new byte[dkLen];
+
+        byte[] B  = new byte[128 * r * p];
+        byte[] XY = new byte[256 * r];
+        byte[] V  = new byte[128 * r * N];
+        int i;
+
+        PBKDF.pbkdf2(mac, salt, 1, B, p * 128 * r);
+
+        for (i = 0; i < p; i++) {
+            smix(B, i * 128 * r, r, N, V, XY);
+        }
+
+        PBKDF.pbkdf2(mac, B, 1, DK, dkLen);
+
+        return DK;
+    }
+
+    public static void smix(byte[] B, int Bi, int r, int N, byte[] V, byte[] XY) {
+        int Xi = 0;
+        int Yi = 128 * r;
+        int i;
+
+        arraycopy(B, Bi, XY, Xi, 128 * r);
+
+        for (i = 0; i < N; i++) {
+            arraycopy(XY, Xi, V, i * (128 * r), 128 * r);
+            blockmix_salsa8(XY, Xi, Yi, r);
+        }
+
+        for (i = 0; i < N; i++) {
+            int j = integerify(XY, Xi, r) & (N - 1);
+            blockxor(V, j * (128 * r), XY, Xi, 128 * r);
+            blockmix_salsa8(XY, Xi, Yi, r);
+        }
+
+        arraycopy(XY, Xi, B, Bi, 128 * r);
+    }
+
+    public static void blockmix_salsa8(byte[] BY, int Bi, int Yi, int r) {
+        byte[] X = new byte[64];
+        int i;
+
+        arraycopy(BY, Bi + (2 * r - 1) * 64, X, 0, 64);
+
+        for (i = 0; i < 2 * r; i++) {
+            blockxor(BY, i * 64, X, 0, 64);
+            salsa20_8(X);
+            arraycopy(X, 0, BY, Yi + (i * 64), 64);
+        }
+
+        for (i = 0; i < r; i++) {
+            arraycopy(BY, Yi + (i * 2) * 64, BY, Bi + (i * 64), 64);
+        }
+
+        for (i = 0; i < r; i++) {
+            arraycopy(BY, Yi + (i * 2 + 1) * 64, BY, Bi + (i + r) * 64, 64);
+        }
+    }
+
+    public static int R(int a, int b) {
+        return (a << b) | (a >>> (32 - b));
+    }
+
+    public static void salsa20_8(byte[] B) {
+        int[] B32 = new int[16];
+        int[] x   = new int[16];
+        int i;
+
+        for (i = 0; i < 16; i++) {
+            B32[i]  = (B[i * 4 + 0] & 0xff) << 0;
+            B32[i] |= (B[i * 4 + 1] & 0xff) << 8;
+            B32[i] |= (B[i * 4 + 2] & 0xff) << 16;
+            B32[i] |= (B[i * 4 + 3] & 0xff) << 24;
+        }
+
+        arraycopy(B32, 0, x, 0, 16);
+
+        for (i = 8; i > 0; i -= 2) {
+            x[ 4] ^= R(x[ 0]+x[12], 7);  x[ 8] ^= R(x[ 4]+x[ 0], 9);
+            x[12] ^= R(x[ 8]+x[ 4],13);  x[ 0] ^= R(x[12]+x[ 8],18);
+            x[ 9] ^= R(x[ 5]+x[ 1], 7);  x[13] ^= R(x[ 9]+x[ 5], 9);
+            x[ 1] ^= R(x[13]+x[ 9],13);  x[ 5] ^= R(x[ 1]+x[13],18);
+            x[14] ^= R(x[10]+x[ 6], 7);  x[ 2] ^= R(x[14]+x[10], 9);
+            x[ 6] ^= R(x[ 2]+x[14],13);  x[10] ^= R(x[ 6]+x[ 2],18);
+            x[ 3] ^= R(x[15]+x[11], 7);  x[ 7] ^= R(x[ 3]+x[15], 9);
+            x[11] ^= R(x[ 7]+x[ 3],13);  x[15] ^= R(x[11]+x[ 7],18);
+            x[ 1] ^= R(x[ 0]+x[ 3], 7);  x[ 2] ^= R(x[ 1]+x[ 0], 9);
+            x[ 3] ^= R(x[ 2]+x[ 1],13);  x[ 0] ^= R(x[ 3]+x[ 2],18);
+            x[ 6] ^= R(x[ 5]+x[ 4], 7);  x[ 7] ^= R(x[ 6]+x[ 5], 9);
+            x[ 4] ^= R(x[ 7]+x[ 6],13);  x[ 5] ^= R(x[ 4]+x[ 7],18);
+            x[11] ^= R(x[10]+x[ 9], 7);  x[ 8] ^= R(x[11]+x[10], 9);
+            x[ 9] ^= R(x[ 8]+x[11],13);  x[10] ^= R(x[ 9]+x[ 8],18);
+            x[12] ^= R(x[15]+x[14], 7);  x[13] ^= R(x[12]+x[15], 9);
+            x[14] ^= R(x[13]+x[12],13);  x[15] ^= R(x[14]+x[13],18);
+        }
+
+        for (i = 0; i < 16; ++i) B32[i] = x[i] + B32[i];
+
+        for (i = 0; i < 16; i++) {
+            B[i * 4 + 0] = (byte) (B32[i] >> 0  & 0xff);
+            B[i * 4 + 1] = (byte) (B32[i] >> 8  & 0xff);
+            B[i * 4 + 2] = (byte) (B32[i] >> 16 & 0xff);
+            B[i * 4 + 3] = (byte) (B32[i] >> 24 & 0xff);
+        }
+    }
+
+    public static void blockxor(byte[] S, int Si, byte[] D, int Di, int len) {
+        for (int i = 0; i < len; i++) {
+            D[Di + i] ^= S[Si + i];
+        }
+    }
+
+    public static int integerify(byte[] B, int Bi, int r) {
+        int n;
+
+        Bi += (2 * r - 1) * 64;
+
+        n  = (B[Bi + 0] & 0xff) << 0;
+        n |= (B[Bi + 1] & 0xff) << 8;
+        n |= (B[Bi + 2] & 0xff) << 16;
+        n |= (B[Bi + 3] & 0xff) << 24;
+
+        return n;
+    }
+}
diff --git a/lib/scrypt/com/lambdaworks/crypto/SCryptUtil.java b/lib/scrypt/com/lambdaworks/crypto/SCryptUtil.java
new file mode 100644 (file)
index 0000000..ca29a00
--- /dev/null
@@ -0,0 +1,112 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import static com.lambdaworks.codec.Base64.*;
+
+/**
+ * Simple {@link SCrypt} interface for hashing passwords using the
+ * <a href="http://www.tarsnap.com/scrypt.html">scrypt</a> key derivation function
+ * and comparing a plain text password to a hashed one. The hashed output is an
+ * extended implementation of the Modular Crypt Format that also includes the scrypt
+ * algorithm parameters.
+ *
+ * Format: <code>$s0$PARAMS$SALT$KEY</code>.
+ *
+ * <dl>
+ * <dd>PARAMS</dd><dt>32-bit hex integer containing log2(N) (16 bits), r (8 bits), and p (8 bits)</dt>
+ * <dd>SALT</dd><dt>base64-encoded salt</dt>
+ * <dd>KEY</dd><dt>base64-encoded derived key</dt>
+ * </dl>
+ *
+ * <code>s0</code> identifies version 0 of the scrypt format, using a 128-bit salt and 256-bit derived key.
+ *
+ * @author  Will Glozer
+ */
+public class SCryptUtil {
+    /**
+     * Hash the supplied plaintext password and generate output in the format described
+     * in {@link SCryptUtil}.
+     *
+     * @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(String passwd, int N, int r, int p) {
+        try {
+            byte[] salt = new byte[16];
+            SecureRandom.getInstance("SHA1PRNG").nextBytes(salt);
+
+            byte[] derived = SCrypt.scrypt(passwd.getBytes("UTF-8"), 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();
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalStateException("JVM doesn't support UTF-8?");
+        } catch (GeneralSecurityException e) {
+            throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
+        }
+    }
+
+    /**
+     * Compare the supplied plaintext password to a hashed password.
+     *
+     * @param   passwd  Plaintext password.
+     * @param   hashed  scrypt hashed password.
+     *
+     * @return true if passwd matches hashed value.
+     */
+    public static boolean check(String passwd, String hashed) {
+        try {
+            String[] parts = hashed.split("\\$");
+
+            if (parts.length != 5 || !parts[1].equals("s0")) {
+                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());
+
+            int N = (int) Math.pow(2, params >> 16 & 0xffff);
+            int r = (int) params >> 8 & 0xff;
+            int p = (int) params      & 0xff;
+
+            byte[] derived1 = SCrypt.scrypt(passwd.getBytes("UTF-8"), salt, N, r, p, 32);
+
+            if (derived0.length != derived1.length) return false;
+
+            int result = 0;
+            for (int i = 0; i < derived0.length; i++) {
+                result |= derived0[i] ^ derived1[i];
+            }
+            return result == 0;
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalStateException("JVM doesn't support UTF-8?");
+        } catch (GeneralSecurityException e) {
+            throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
+        }
+    }
+
+    private static int log2(int n) {
+        int log = 0;
+        if ((n & 0xffff0000 ) != 0) { n >>>= 16; log = 16; }
+        if (n >= 256) { n >>>= 8; log += 8; }
+        if (n >= 16 ) { n >>>= 4; log += 4; }
+        if (n >= 4  ) { n >>>= 2; log += 2; }
+        return log + (n >>> 1);
+    }
+}
diff --git a/tests/com/lambdaworks/crypto/test/CryptoTestUtil.java b/tests/com/lambdaworks/crypto/test/CryptoTestUtil.java
new file mode 100644 (file)
index 0000000..418c5f9
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+public class CryptoTestUtil {
+    public static byte[] decode(String str) {
+        byte[] bytes = new byte[str.length() / 2];
+        int index = 0;
+
+        for (int i = 0; i < str.length(); i += 2) {
+            int high = hexValue(str.charAt(i));
+            int low = hexValue(str.charAt(i + 1));
+            bytes[index++] = (byte) ((high << 4) + low);
+        }
+
+        return bytes;
+    }
+
+    public static int hexValue(char c) {
+        return c >= 'a' ? c - 87 : c - 48;
+    }
+}
diff --git a/tests/com/lambdaworks/crypto/test/PBKDFTest.java b/tests/com/lambdaworks/crypto/test/PBKDFTest.java
new file mode 100644 (file)
index 0000000..92a08b6
--- /dev/null
@@ -0,0 +1,141 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+import com.lambdaworks.crypto.PBKDF;
+import org.junit.Test;
+
+import java.math.BigInteger;
+
+import static org.junit.Assert.*;
+import static com.lambdaworks.crypto.test.CryptoTestUtil.*;
+
+public class PBKDFTest {
+    @Test
+    public void pbkdf2_hmac_sha1_rfc6070() throws Exception {
+        String alg = "HmacSHA1";
+        byte[] P, S;
+        int c, dkLen;
+        String DK;
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 1;
+        dkLen = 20;
+        DK = "0c60c80f961f0e71f3a9b524af6012062fe037a6";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 2;
+        dkLen = 20;
+        DK = "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 20;
+        DK = "4b007901b765489abead49d926f721d065a429c1";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 16777216;
+        dkLen = 20;
+        DK = "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "passwordPASSWORDpassword".getBytes("UTF-8");
+        S = "saltSALTsaltSALTsaltSALTsaltSALTsalt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 25;
+        DK = "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "pass\0word".getBytes("UTF-8");
+        S = "sa\0lt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 16;
+        DK = "56fa6aa75548099dcc37d7f03425e0c3";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+    }
+
+    @Test
+    public void pbkdf2_hmac_sha1_rfc3962() throws Exception {
+        String alg = "HmacSHA1";
+        byte[] P, S;
+        int c, dkLen;
+        String DK;
+
+        P = "password".getBytes("UTF-8");
+        S = "ATHENA.MIT.EDUraeburn".getBytes("UTF-8");
+        c = 1;
+
+        dkLen = 16;
+        DK = "cdedb5281bb2f801565a1122b2563515";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "ATHENA.MIT.EDUraeburn".getBytes("UTF-8");
+        c = 2;
+
+        dkLen = 16;
+        DK = "01dbee7f4a9e243e988b62c73cda935d";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "ATHENA.MIT.EDUraeburn".getBytes("UTF-8");
+        c = 1200;
+
+        dkLen = 16;
+        DK = "5c08eb61fdf71e4e4ec3cf6ba1f5512b";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = new BigInteger("1234567878563412", 16).toByteArray();
+        c = 5;
+
+        dkLen = 16;
+        DK = "d1daa78615f287e6a1c8b120d7062a49";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+    }
+
+    @Test
+    public void pbkdf2_hmac_sha256_scrypt() throws Exception {
+        String alg = "HmacSHA256";
+        byte[] P, S;
+        int c, dkLen;
+        String DK;
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 32;
+        DK = "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+    }
+}
diff --git a/tests/com/lambdaworks/crypto/test/SCryptTest.java b/tests/com/lambdaworks/crypto/test/SCryptTest.java
new file mode 100644 (file)
index 0000000..bd34bb2
--- /dev/null
@@ -0,0 +1,84 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+import com.lambdaworks.crypto.SCrypt;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static com.lambdaworks.crypto.test.CryptoTestUtil.*;
+import static com.lambdaworks.crypto.SCrypt.*;
+
+public class SCryptTest {
+    @Test
+    public void scrypt_paper_appendix_b() throws Exception {
+        byte[] P, S;
+        int N, r, p, dkLen;
+        String DK;
+
+        // empty key & salt test missing because unsupported by JCE
+
+        P = "password".getBytes("UTF-8");
+        S = "NaCl".getBytes("UTF-8");
+        N = 1024;
+        r = 8;
+        p = 16;
+        dkLen = 64;
+        DK = "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640";
+
+        assertArrayEquals(decode(DK), SCrypt.scrypt(P, S, N, r, p, dkLen));
+
+        P = "pleaseletmein".getBytes("UTF-8");
+        S = "SodiumChloride".getBytes("UTF-8");
+        N = 16384;
+        r = 8;
+        p = 1;
+        dkLen = 64;
+        DK = "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887";
+
+        assertArrayEquals(decode(DK), scrypt(P, S, N, r, p, dkLen));
+
+        P = "pleaseletmein".getBytes("UTF-8");
+        S = "SodiumChloride".getBytes("UTF-8");
+        N = 1048576;
+        r = 8;
+        p = 1;
+        dkLen = 64;
+        DK = "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4";
+
+        assertArrayEquals(decode(DK), SCrypt.scrypt(P, S, N, r, p, dkLen));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void scrypt_invalid_N_zero() throws Exception {
+        byte[] P = "pleaseletmein".getBytes("UTF-8");
+        byte[] S = "SodiumChloride".getBytes("UTF-8");
+        scrypt(P, S, 0, 1, 1, 64);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void scrypt_invalid_N_odd() throws Exception {
+        byte[] P = "pleaseletmein".getBytes("UTF-8");
+        byte[] S = "SodiumChloride".getBytes("UTF-8");
+        scrypt(P, S, 3, 1, 1, 64);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void scrypt_invalid_N_large() throws Exception {
+        byte[] P = "pleaseletmein".getBytes("UTF-8");
+        byte[] S = "SodiumChloride".getBytes("UTF-8");
+        int    r = 8;
+        int    N = Integer.MAX_VALUE / 128;
+        scrypt(P, S, N, r, 1, 64);
+    }
+
+//    @Test(expected = IllegalArgumentException.class)
+//    public void scrypt_invalid_r_large() throws Exception {
+//        byte[] P = "pleaseletmein".getBytes("UTF-8");
+//        byte[] S = "SodiumChloride".getBytes("UTF-8");
+//        int    N = 1024;
+//        int    r = Integer.MAX_VALUE / 128 + 1;
+//        int    p = 0;
+//        scrypt(P, S, N, r, p, 64);
+//    }
+}
diff --git a/tests/com/lambdaworks/crypto/test/SCryptUtilTest.java b/tests/com/lambdaworks/crypto/test/SCryptUtilTest.java
new file mode 100644 (file)
index 0000000..f673657
--- /dev/null
@@ -0,0 +1,59 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+import com.lambdaworks.codec.Base64;
+import com.lambdaworks.crypto.SCryptUtil;
+import org.junit.Assert;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class SCryptUtilTest {
+    String passwd = "secret";
+
+    @Test
+    public void scrypt() {
+        int N = 16384;
+        int r = 8;
+        int p = 1;
+
+        String hashed = SCryptUtil.scrypt(passwd, N, r, p);
+        String[] parts = hashed.split("\\$");
+
+        assertEquals(5, parts.length);
+        assertEquals("s0", parts[1]);
+        Assert.assertEquals(16, Base64.decode(parts[3].toCharArray()).length);
+        assertEquals(32, Base64.decode(parts[4].toCharArray()).length);
+
+        int params = Integer.valueOf(parts[2], 16);
+
+        assertEquals(N, (int) Math.pow(2, params >> 16 & 0xffff));
+        assertEquals(r, params >> 8 & 0xff);
+        assertEquals(p, params >> 0 & 0xff);
+    }
+
+    @Test
+    public void check() {
+        String hashed = SCryptUtil.scrypt(passwd, 16384, 8, 1);
+
+        assertTrue(SCryptUtil.check(passwd, hashed));
+        assertFalse(SCryptUtil.check("s3cr3t", hashed));
+    }
+
+    @Test
+    public void format_0_rp_max() throws Exception {
+        int N = 2;
+        int r = 255;
+        int p = 255;
+
+        String hashed = SCryptUtil.scrypt(passwd, N, r, p);
+        assertTrue(SCryptUtil.check(passwd, hashed));
+
+        String[] parts = hashed.split("\\$");
+        int params = Integer.valueOf(parts[2], 16);
+
+        assertEquals(N, (int) Math.pow(2, params >>> 16 & 0xffff));
+        assertEquals(r, params >> 8 & 0xff);
+        assertEquals(p, params >> 0 & 0xff);
+    }
+}