diff --git a/pom.xml b/pom.xml index 1e24d4e..00c4a5a 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,14 @@ + + + junit + junit + 4.12 + test + + commons-io @@ -61,6 +69,14 @@ 2.6 + + + commons-codec + commons-codec + 1.13 + + + com.google.guava diff --git a/src/main/java/net/locusworks/crypto/AES.java b/src/main/java/net/locusworks/crypto/AES.java index 35049ab..f054c1e 100644 --- a/src/main/java/net/locusworks/crypto/AES.java +++ b/src/main/java/net/locusworks/crypto/AES.java @@ -9,13 +9,14 @@ import javax.crypto.spec.SecretKeySpec; import org.apache.commons.lang3.StringUtils; +import net.locusworks.crypto.utils.RandomString; + import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; -import java.util.Random; /** @@ -79,7 +80,7 @@ public class AES { try { this.cipher = Cipher.getInstance(ENCRYPTION_ALGORITH, PROVIDER); this.secretKeySpec = new SecretKeySpec(key, ENCRYPTION_TYPE); - this.ivParamSpec = new IvParameterSpec(getRandomString(16, sr).getBytes(StandardCharsets.UTF_8)); + this.ivParamSpec = new IvParameterSpec(RandomString.getBytes(16, sr)); } catch (Exception ex) { System.err.println(ex); throw new IllegalArgumentException("Unable to initalize encryption:", ex); @@ -135,7 +136,7 @@ public class AES { } public static AES createInstance() { - return createInstance(getRandomString(16, null)); + return createInstance(RandomString.getString(16)); } public static AES createInstance(byte[] byteSeed) { @@ -149,15 +150,6 @@ public class AES { return aes; } - private static String getRandomString(int size, Random randomizer) { - byte[] array = new byte[size]; - if (randomizer == null) { - randomizer = new Random(System.currentTimeMillis()); - } - randomizer.nextBytes(array); - return new String(array, StandardCharsets.UTF_8); - } - public static void main(String[] args) throws NoSuchAlgorithmException { if (args == null || !(args.length > 0)) { throw new IllegalArgumentException("No args provided. Need password as argument"); diff --git a/src/main/java/net/locusworks/crypto/utils/HashUtils.java b/src/main/java/net/locusworks/crypto/utils/HashUtils.java new file mode 100644 index 0000000..448934d --- /dev/null +++ b/src/main/java/net/locusworks/crypto/utils/HashUtils.java @@ -0,0 +1,188 @@ +package net.locusworks.crypto.utils; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; + +/** + * Wrapper class that leverages java's MessageDigest to hash files + * @author Isaac Parenteau + * + */ +public class HashUtils { + + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * Size of the streaming buffer + */ + public static final Integer STREAM_BUFFER_LENGTH = 1024; + + /** + * Hash a string literal + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @param data String to hash + * @return hash value of the string literal + */ + public static String hash(String hashType, String data) { + return hash(hashType, data, true); + } + + /** + * Hash a string literal + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @param data String to hash + * @param toLower True to output the hash in lower case. False to output in upper case + * @return hash value of the string literal + */ + public static String hash(String hashType, String data, boolean toLower) { + byte[] stringData = data.getBytes(UTF_8); + return hash(hashType, stringData, toLower); + } + + @Deprecated + public static String hash(String hashType, File data) { + return hash(hashType, data.toPath()); + } + + /** + * Hash a file + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @param data File to hash + * @return hash value of the file + */ + public static String hash(String hashType, Path data) { + return hash(hashType, data, true); + } + + @Deprecated + public static String hash(String hashType, File data, boolean toLower) { + return hash(hashType, data.toPath(), toLower); + } + + /** + * Hash a file + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @param data File to hash + * @param toLower True to output the hash in lower case. False to output in upper case + * @return hash value of the file + */ + public static String hash(String hashType, Path data, boolean toLower) { + try (InputStream stream = Files.newInputStream(data)) { + return hash(stream, hashType, toLower); + } catch (IOException ex) { + throw new IllegalArgumentException(ex.getMessage()); + } + } + + /** + * Hash a byte array + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @param data data to hash + * @return hash value of the data + */ + public static String hash(String hashType, byte[] data) { + return hash(hashType, data, true); + } + + /** + * Hash a byte array + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @param data data to hash + * @param toLower True to output the hash in lower case. False to output in upper case + * @return hash value of the data + */ + public static String hash(String hashType, byte[] data, boolean toLower) { + return hash(new BufferedInputStream(new ByteArrayInputStream(data)), hashType, toLower); + } + + /** + * Hash an input stream + * @param stream Stream with the data to hash + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @return Hash value of the input stream + */ + public static String hash(InputStream stream, String hashType) { + return hash(stream, hashType, true); + } + + /** + * Hash an input stream + * @param stream Stream with the data to hash + * @param hashType Hash types supported by MessageDigest (i.e MD5, SHA-1, SHA-512) + * @param toLower True to output the hash in lower case. False to output in upper case + * @return Hash value of the input stream + */ + public static String hash(InputStream stream, String hashType, boolean toLower) { + MessageDigest digest = null; + try(InputStream is = stream) { + digest = MessageDigest.getInstance(hashType); + + byte[] buffer = new byte[STREAM_BUFFER_LENGTH]; + int read = is.read(buffer, 0, STREAM_BUFFER_LENGTH); + + while (read > -1) { + digest.update(buffer, 0, read); + read = is.read(buffer, 0, STREAM_BUFFER_LENGTH); + } + + return encodeHexString(digest.digest(), toLower); + } catch (Exception ex) { + throw new IllegalArgumentException(ex.getMessage()); + } + } + + /** + * Encode the hash data back to a string + * @param data Data to encode + * @param toLower output to lower case + * @return + */ + private static String encodeHexString(byte[] data, boolean toLower) { + return new String(encodeHex(data, toLower)); + } + + /** + * Encode the hash data to a character array + * @param data Data to encode + * @param toLower output to lower case + * @return + */ + private static char[] encodeHex(byte[] data, boolean toLower) { + return encodeHex(data, toLower ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * Encode the hex to a character array + * @param data Data to encode + * @param toDigits digits to use + * @return + */ + private static char[] encodeHex(byte[] data, char[] toDigits) { + int l = data.length; + char[] out = new char[l << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return out; + } +} diff --git a/src/main/java/net/locusworks/crypto/utils/RandomString.java b/src/main/java/net/locusworks/crypto/utils/RandomString.java new file mode 100644 index 0000000..2466f75 --- /dev/null +++ b/src/main/java/net/locusworks/crypto/utils/RandomString.java @@ -0,0 +1,78 @@ +package net.locusworks.crypto.utils; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.Random; + +public class RandomString { + + public static final String LOWER = "abcdefghijklmnopqrstuvwxyz"; + public static final String UPPER = LOWER.toUpperCase(); + public static final String DIGITS = "0123456789"; + + public static final String ALPHA_NUMERIC = LOWER + UPPER + DIGITS; + + private Random random; + + private char[] symbols; + private int length; + + private static RandomString instance; + + private RandomString(Integer length) { + this(length, new SecureRandom()); + } + + private RandomString(Integer length, Random random) { + this(length, random, ALPHA_NUMERIC); + } + + private RandomString(Integer length, Random random, String symbols) { + if (length < 1) throw new IllegalArgumentException("Length has to be greater than 1"); + if (symbols.length() < 2) throw new IllegalArgumentException("Symbols need to be greater than 2"); + this.random = Objects.requireNonNull(random); + this.symbols = symbols.toCharArray(); + } + + private synchronized final void setRandom(Random random) { + this.random = random; + } + + private synchronized final void setLength(int length) { + this.length = length; + } + + public String nextString() { + char[] buffer = new char[length]; + for (int index = 0; index < buffer.length; index++) { + buffer[index] = symbols[random.nextInt(symbols.length)]; + } + return new String(buffer); + } + + public static String getString(Integer length) { + if (instance == null) { + instance = new RandomString(length); + } + instance.setLength(length); + return instance.nextString(); + } + + public static String getString(Integer length, Random random) { + if (instance == null) { + instance = new RandomString(length); + } + instance.setLength(length); + instance.setRandom(random); + return instance.nextString(); + } + + public static byte[] getBytes(Integer length) { + return getString(length).getBytes(StandardCharsets.UTF_8); + } + + public static byte[] getBytes(Integer length, Random random) { + return getString(length, random).getBytes(StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/net/locusworks/crypto/tests/AESEncryptionTest.java b/src/test/java/net/locusworks/crypto/tests/AESEncryptionTest.java new file mode 100644 index 0000000..6a5b183 --- /dev/null +++ b/src/test/java/net/locusworks/crypto/tests/AESEncryptionTest.java @@ -0,0 +1,41 @@ +package net.locusworks.crypto.tests; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import net.locusworks.crypto.AES; + +public class AESEncryptionTest { + + @Test + public void testEncryption() { + try { + String encrypted = AES.createInstance().encrypt("hello world"); + Assert.assertTrue(String.format("Encrypted String is not blank? :%s", encrypted), !StringUtils.isBlank(encrypted)); + } catch (Exception ex) { + ex.printStackTrace(System.err); + Assert.fail(); + } + } + + @Test + public void testDecryption() { + String testString ="hello world"; + try { + AES aes = AES.createInstance(); + String encrypted = aes.encrypt(testString); + Assert.assertTrue(String.format("Encrypted String is not blank? :%s", encrypted), !StringUtils.isBlank(encrypted)); + + String decrypted = aes.decrypt(encrypted); + Assert.assertTrue(String.format("Decrypted String is not blank? :%s", decrypted), !StringUtils.isBlank(encrypted)); + + Assert.assertTrue("Test String and Original String the same? :%s", testString.equals(decrypted)); + + } catch (Exception ex) { + ex.printStackTrace(System.err); + Assert.fail(); + } + } + +} diff --git a/src/test/java/net/locusworks/crypto/tests/HashSaltTest.java b/src/test/java/net/locusworks/crypto/tests/HashSaltTest.java new file mode 100644 index 0000000..2d3c822 --- /dev/null +++ b/src/test/java/net/locusworks/crypto/tests/HashSaltTest.java @@ -0,0 +1,35 @@ +package net.locusworks.crypto.tests; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import net.locusworks.crypto.HashSalt; + + +public class HashSaltTest { + + private static String samplePassword="Hello World"; + + @Test + public void testEncryption() { + try { + String hashSalt = HashSalt.createHash(samplePassword); + Assert.assertTrue(String.format("Encrypted String is not blank? :%s", hashSalt), !StringUtils.isBlank(hashSalt)); + } catch(Exception ex) { + Assert.fail(); + } + } + + @Test + public void testDecryption() { + try { + String hashSalt = HashSalt.createHash(samplePassword); + boolean decrypted = HashSalt.validatePassword(samplePassword, hashSalt); + Assert.assertTrue("Test String and Original String the same? :%s", decrypted); + } catch(Exception ex) { + Assert.fail(); + } + } + +} diff --git a/src/test/java/net/locusworks/crypto/tests/HashUtilsTest.java b/src/test/java/net/locusworks/crypto/tests/HashUtilsTest.java new file mode 100644 index 0000000..660eefa --- /dev/null +++ b/src/test/java/net/locusworks/crypto/tests/HashUtilsTest.java @@ -0,0 +1,38 @@ +package net.locusworks.crypto.tests; + +import static org.junit.Assert.*; + +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.Test; + +import net.locusworks.crypto.utils.HashUtils; + +public class HashUtilsTest { + + private static final String TEST_STRING = "Hello World"; + + @Test + public void testMD5() throws Exception { + String digestUtilsMD5 = DigestUtils.md5Hex(TEST_STRING.getBytes()); + String hashUtilsMD5 = HashUtils.hash("MD5", TEST_STRING); + + assertTrue(digestUtilsMD5.equals(hashUtilsMD5)); + } + + @Test + public void testSHA1() throws Exception { + String digestUtilsMD5 = DigestUtils.sha1Hex(TEST_STRING.getBytes()); + String hashUtilsMD5 = HashUtils.hash("SHA-1", TEST_STRING); + + assertTrue(digestUtilsMD5.equals(hashUtilsMD5)); + } + + @Test + public void testSHA512() throws Exception { + String digestUtilsMD5 = DigestUtils.sha512Hex(TEST_STRING.getBytes()); + String hashUtilsMD5 = HashUtils.hash("SHA-512", TEST_STRING); + + assertTrue(digestUtilsMD5.equals(hashUtilsMD5)); + } + +} diff --git a/src/test/java/net/locusworks/crypto/tests/RandomStringTest.java b/src/test/java/net/locusworks/crypto/tests/RandomStringTest.java new file mode 100644 index 0000000..6769134 --- /dev/null +++ b/src/test/java/net/locusworks/crypto/tests/RandomStringTest.java @@ -0,0 +1,26 @@ +package net.locusworks.crypto.tests; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import net.locusworks.crypto.utils.RandomString; + +public class RandomStringTest { + + @Test + public void testStaticBytes() { + for (Integer length = 3; length < 50; length++) { + assertTrue(RandomString.getBytes(length).length == length); + } + } + + @Test + public void testStaticString() { + for (Integer length = 3; length < 50; length++) { + String random = RandomString.getString(length); + assertTrue(random.length() == length); + } + } + +}