diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy index 715e653..daba2e8 100644 --- a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy @@ -1,7 +1,11 @@ package org.xbib.groovy.crypt import javax.crypto.Mac +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec +import java.nio.ByteBuffer +import java.nio.ByteOrder import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.security.NoSuchAlgorithmException @@ -79,6 +83,33 @@ class CryptUtil { base64Digest(plainText, salt, 'SHA-512', 'ssha512') } + static String pbkdf2(String plainText) { + // the 389-ds parameters for PBKDF2-SHA256 + pbkdf2(plainText, randomHexString(64), 30000, 256) + } + + static String pbkdf2(String plainText, String salt) { + // the 389-ds parameters for PBKDF2-SHA256: 30000 iterations, 64 bytes (128 hex) salt, 256 bytes hash length + if (salt.length() != 128) { + throw new IllegalArgumentException("salt must be 64 bytes") + } + pbkdf2(plainText, salt, 30000, 256) + } + + static String pbkdf2(String plainText, String salt, int iterations, int hashLength) { + byte[] n = htonl(iterations).array() // 4 bytes for network byte order = native byte order + byte[] b = salt.decodeHex() + byte[] hash = pbkdf2(plainText.toCharArray(), b, iterations, hashLength * 8) + int len = n.length + b .length + hash.length + byte[] result = new byte[len] + ByteBuffer buffer = ByteBuffer.wrap(result) + buffer.put(n) + buffer.put(b) + buffer.put(hash) + // 4 + 64 + 256 = 324 bytes + "{PBKDF2_SHA256}${result.encodeBase64()}" + } + static String hmacSHA1(String plainText, String secret) { hmac(plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1") } @@ -105,9 +136,32 @@ class CryptUtil { mac.doFinal(plainText).encodeHex() } - static String random(int length) { + /** + * Computes the PBKDF2 hash of a password. + * + * @param password the password to hash. + * @param salt the salt + * @param iterations the iteration count (slowness factor) + * @param bytes the length of the hash to compute in bytes + * @return the PBDKF2 hash of the password + */ + static byte[] pbkdf2(char[] plainText, byte[] salt, int iterations, int len) { + PBEKeySpec spec = new PBEKeySpec(plainText, salt, iterations, len) + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + skf.generateSecret(spec).getEncoded() + } + + static String randomHexString(int length) { + randomBytes(length).encodeHex() + } + + static byte[] randomBytes(int length) { byte[] b = new byte[length] random.nextBytes(b) - b.encodeHex() + b + } + + static ByteBuffer htonl(int value) { + ByteBuffer.allocate(4).order(ByteOrder.nativeOrder()).putInt(value) } } diff --git a/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy b/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy index c30fd84..ed01b34 100644 --- a/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy +++ b/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy @@ -169,4 +169,12 @@ class CryptTest { String code = CryptUtil.ssha512(plaintext, salt) assertEquals('{ssha512}jeWuCXRjsvKh/vK548GP9ZCs4q9Sh1u700C8eONyV+EL/P810C8vlx9Eu4vRjHq/TDoGW8FE1l/P2KG3w9lHITxo8fR/Qdgv', code,'test SSHA-512 method') } + + @Test + void testPBKDF2() { + String plaintext = 'geheim' + String salt = "3c68f1f47f41d82f3c68f1f47f41d82f3c68f1f47f41d82f3c68f1f47f41d82f3c68f1f47f41d82f3c68f1f47f41d82f3c68f1f47f41d82f3c68f1f47f41d82f" + String code = CryptUtil.pbkdf2(plaintext, salt) + assertEquals("{PBKDF2_SHA256}MHUAADxo8fR/QdgvPGjx9H9B2C88aPH0f0HYLzxo8fR/QdgvPGjx9H9B2C88aPH0f0HYLzxo8fR/QdgvPGjx9H9B2C9DUj6t+vF3mSI5b6nExWWcUnA6DXTbEa25BIMZ5ERe9JIqjkBr2p0ot9D5x4LZx9evQNexOWb+ea/stJkmi3wWKwS/uzSpEc4NZSv/+W1ZWtnK6NMkxxRJPjXEOCrjbKCOktDwCjSelBAe/rt0DABUYoMw69c8qZ1toAIz1x6oN5y58ImMpVsPK/CkbmbeK0QtDbWYZK8V1SZ6cZlF6kngpGWnAcEilIHqCVjM1HZMI+mZz86h86ZHcbxp9twuENu3DHi3nIZRzILrRIsjWAkruSDw7W/jXseGmVeBj/22xbKSybZmXawFGM59k3U5fE+1WvudOfVzwFyVAxDxispF", code) + } }