diff --git a/.gitignore b/.gitignore
index 71c41f2..24cdadc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,6 +72,9 @@ local.properties
# sbteclipse plugin
.target
+.classpath
+.project
+.settings
# Tern plugin
.tern-project
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..155a059
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,180 @@
+#!groovy
+
+
+init()
+
+def branch_name
+def branch_name_base
+def build_number
+def build_url
+def git_commit
+def job_name
+def tag
+def version
+def build_type
+def display_name
+
+def init() {
+
+ // Keep the 5 most recent builds
+ properties([[$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', numToKeepStr: '5']]])
+
+ build_number = env.BUILD_NUMBER
+ build_url = env.BUILD_URL
+ job_name = "${env.JOB_NAME}"
+ branch_name = env.BRANCH_NAME
+ persist = branch_name
+
+ // execute the branch type specific pipeline code
+ try {
+
+ if (branch_name.indexOf('release/')==0) build_type='release'
+ if (branch_name.indexOf('feature/')==0) build_type='feature'
+ if (branch_name.indexOf('develop')==0) build_type='develop'
+ if (branch_name.indexOf('master')==0) build_type='master'
+ if (branch_name.indexOf('hotfix/')==0) build_type='hotfix'
+ if (branch_name.indexOf('bugfix/')==0) build_type='bugfix'
+
+ switch(build_type) {
+ case ~/feature/:
+ case ~/hotfix/:
+ case ~/bugfix/:
+ case ~/master/:
+ case ~/develop/:
+ CommonBuild();
+ break;
+ case ~/release/:
+ CommonBuild();
+ Deploy();
+ break;
+ default:
+ throw "unsupported branch type: ${branch_name}"
+ }
+
+ node('master') {
+ set_result('SUCCESS')
+ }
+
+ } catch (err) {
+ node() {
+ set_result('FAILURE')
+ }
+ throw err
+ }
+}
+
+def CommonBuild() {
+ // common pipeline elements
+ node('master') {
+ Initialize()
+ SetVersion(build_type)
+ print_vars() // after SetVersion - all variables now defined
+ set_result('INPROGRESS')
+ Build()
+ }
+}
+
+def Build() {
+ stage ('build') {
+ mvn_alt("install -DskipTests=true -Dbuild.revision=${git_commit}")
+ step([$class: 'ArtifactArchiver', artifacts: '**/target/*.jar', fingerprint: true])
+ }
+}
+
+def Initialize() {
+ stage ('initialize') {
+
+ // get new code
+ checkout scm
+
+ git_commit = getSha1()
+ }
+}
+
+def Deploy() {
+ node('master') {
+ stage ('deploy') {
+ mvn_alt("deploy -DskipTests=true -Dbuild.number=${build_number} -Dbuild.revision=${git_commit}")
+ }
+ }
+}
+
+def getSha1() {
+ sha1 = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
+ echo "sha1 is ${sha1}"
+ return sha1
+}
+
+def mvn_initial(args) {
+ mvn_alt(args)
+}
+
+def mvn_alt(args) {
+ // add maven tools to path before calling maven
+ withMaven(maven: 'maven-3.6.1', jdk: 'jdk1.8.0_221', mavenSettingsConfig: 'maven_settings') {
+ writeFile file: '.skip-task-scanner', text: ''
+ writeFile file: '.skip-publish-junit-results', text: ''
+ sh "mvn ${args}"
+ }
+}
+
+def set_result(status) {
+ if ( status == 'SUCCESS' ) {
+ currentBuild.result = status
+ notify_bitbucket('SUCCESSFUL')
+ } else if ( status == 'FAILURE' ) {
+ currentBuild.result = status
+ notify_bitbucket('FAILED')
+ } else if ( status == 'INPROGRESS' ) {
+ notify_bitbucket('INPROGRESS')
+ } else {
+ error ("unknown status")
+ }
+}
+
+def notify_bitbucket(state) {
+ echo "notify bitbucket, state = $state, commit: ${git_commit}"
+}
+
+def print_vars() {
+ echo "build_number = ${build_number}"
+ echo "build_url = ${build_url}"
+ echo "job_name = ${job_name}"
+ echo "branch_name = ${branch_name}"
+ echo "branch_name_base = ${branch_name_base}"
+ echo "build_type = ${build_type}"
+ echo "display_name = ${currentBuild.displayName}"
+ echo "version = ${version}"
+ echo "git_commit = ${git_commit}"
+}
+
+def SetVersion( v ) {
+ stage ('set version') {
+ echo "set version ${v}"
+ branch_name_base = (branch_name =~ /([^\/]+$)/)[0][0]
+
+ switch(build_type) {
+ case ~/develop/:
+ version = build_number + '-SNAPSHOT'
+ currentBuild.displayName = version
+ break;
+ case ~/release/:
+ // for release branches, where the branch is named "release/1.2.3",
+ // derive the version and display name derive from the numeric suffix and append the build number
+ // 3.2.1.100
+ version = branch_name_base + '.' + build_number + '-RELEASE'
+ currentBuild.displayName = version
+ break;
+ default:
+ // for all other branches the version number is 0 with an appended build number
+ // and for the display name use the jenkins default #n and add the branch name
+ version = branch_name_base + "." + build_number
+ currentBuild.displayName = "#" + build_number + " - " + branch_name_base
+ }
+
+ display_name = currentBuild.displayName
+ mvn_initial("versions:set -DnewVersion=${version}")
+ }
+}
+
+return this
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..1e24d4e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,82 @@
+
+ 4.0.0
+ net.locusworks
+ crypto
+ 0.0.1-SNAPSHOT
+ Crypto
+ Crypto library
+
+
+ 1.8
+ 1.8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.20.1
+
+ always
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.7.0
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+
+
+
+ org.owasp
+ dependency-check-maven
+ 5.2.2
+
+ true
+ true
+ false
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+
+
+ commons-io
+ commons-io
+ 2.6
+
+
+
+
+ com.google.guava
+ guava
+ 28.1-jre
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.9
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/net/locusworks/crypto/AES.java b/src/main/java/net/locusworks/crypto/AES.java
new file mode 100644
index 0000000..35049ab
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/AES.java
@@ -0,0 +1,172 @@
+package net.locusworks.crypto;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.lang3.StringUtils;
+
+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;
+
+
+/**
+ * AES encryption/decryption class
+ * This class will encrypt/decrypt data. The encryption key is never known.
+ * Instead it is is generated by the provided seed. As long as the seed stays the same
+ * the key will remain the same and the encryption/decryption will work. This
+ * provides and added security.
+ * @author Isaac Parenteau
+ * @version 1.0.0
+ * @date 02/15/2018
+ *
+ */
+public class AES {
+
+ private static final String ENCRYPTION_TYPE = "AES";
+ private static final String ENCRYPTION_ALGORITH = "AES/CBC/PKCS5Padding";
+ private static final String PROVIDER = "SunJCE";
+ private static final String ALGORITHM = "SHA1PRNG";
+
+ private Cipher cipher;
+
+ private SecretKeySpec secretKeySpec;
+
+ private IvParameterSpec ivParamSpec;
+
+ private String seed;
+
+ private void initSecureKey(String seed) {
+ try {
+ SecureRandom sr = getSecureRandom(seed);
+ KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_TYPE);
+ generator.init(128, sr);
+ init(generator.generateKey().getEncoded(), sr);
+ } catch (Exception ex) {
+ System.err.println(ex);
+ throw new IllegalArgumentException("Unable to initalize encryption:", ex);
+ }
+ }
+
+ /**
+ * Initializes the secure random object to be the same across all platforms
+ * with regard to provider and algorithm used
+ * @param seed Seed to initialize SecureRandom with
+ * @return SecureRandom object
+ * @throws NoSuchAlgorithmException thrown when algorithm can't be used
+ * @throws NoSuchProviderException thrown when the provider cant be found
+ */
+ private static SecureRandom getSecureRandom(String seed) throws NoSuchAlgorithmException {
+ SecureRandom sr = SecureRandom.getInstance(ALGORITHM);
+ sr.setSeed(seed.getBytes(StandardCharsets.UTF_8));
+ return sr;
+ }
+
+ /**
+ * Initialize the aes engine
+ * @param key secret key to use
+ * @param sr
+ */
+ private void init(final byte[] key, SecureRandom sr) {
+ 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));
+ } catch (Exception ex) {
+ System.err.println(ex);
+ throw new IllegalArgumentException("Unable to initalize encryption:", ex);
+ }
+ }
+
+ /***
+ * Encrypt a text string
+ * @param plainText String to encrypt
+ * @return encrypted string
+ */
+ public String encrypt(String plainText) {
+ if (StringUtils.isBlank(plainText)) {
+ plainText = "";
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, this.secretKeySpec, this.ivParamSpec);
+ byte[] cypherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+ return new String(Base64.getEncoder().encode(cypherText), StandardCharsets.UTF_8);
+ } catch (Exception ex) {
+ throw new IllegalArgumentException(ex.getMessage(), ex);
+ }
+ }
+
+ /***
+ * Decrypt an encrypted string
+ * @param cipherString encrypted string to decrypt
+ * @return unecrypted string
+ */
+ public String decrypt(String cipherString) {
+ if (StringUtils.isBlank(cipherString)) {
+ return "";
+ }
+ byte[] cipherText = Base64.getDecoder().decode(cipherString.getBytes(StandardCharsets.UTF_8));
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, this.secretKeySpec, this.ivParamSpec);
+ return new String(cipher.doFinal(cipherText), StandardCharsets.UTF_8);
+ } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new IllegalArgumentException(e.getMessage(), e);
+ }
+ }
+
+ public AES setSeed(String seed) {
+ if (this.seed == null || !this.seed.equals(seed)) {
+ initSecureKey(seed);
+ }
+ this.seed = seed;
+ return this;
+ }
+
+ public final String getSeed() {
+ return this.seed;
+ }
+
+ public static AES createInstance() {
+ return createInstance(getRandomString(16, null));
+ }
+
+ public static AES createInstance(byte[] byteSeed) {
+ String seed = new String(byteSeed, StandardCharsets.UTF_8);
+ return createInstance(seed);
+ }
+
+ public static AES createInstance(String seed) {
+ AES aes = new AES();
+ aes.setSeed(seed);
+ 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");
+ }
+ if (args.length % 2 == 0) {
+ System.out.println(AES.createInstance(String.valueOf(args[1])).decrypt(String.valueOf(args[0])));
+ } else {
+ System.out.println(AES.createInstance().encrypt(String.valueOf(args[0])));
+ }
+ }
+
+}
diff --git a/src/main/java/net/locusworks/crypto/AESKey.java b/src/main/java/net/locusworks/crypto/AESKey.java
new file mode 100644
index 0000000..85900de
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/AESKey.java
@@ -0,0 +1,29 @@
+package net.locusworks.crypto;
+
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+
+public class AESKey implements PrivateKey {
+
+ private static final long serialVersionUID = -8452357427706386362L;
+ private String seed;
+
+ public AESKey(String seed) {
+ this.seed = seed;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return "aes";
+ }
+
+ @Override
+ public String getFormat() {
+ return "aes-seed";
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return this.seed.getBytes(StandardCharsets.UTF_8);
+ }
+}
diff --git a/src/main/java/net/locusworks/crypto/AESKeySpec.java b/src/main/java/net/locusworks/crypto/AESKeySpec.java
new file mode 100644
index 0000000..4c31efc
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/AESKeySpec.java
@@ -0,0 +1,46 @@
+package net.locusworks.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Iterator;
+
+import org.apache.commons.io.IOUtils;
+
+import static com.google.common.collect.Iterators.get;
+import static com.google.common.collect.Iterators.size;
+import static com.google.common.base.Preconditions.checkArgument;
+
+public class AESKeySpec extends EncodedKeySpec {
+
+ private static final String AES_MARKER = "aes-seed";
+
+ public AESKeySpec(byte[] encodedKey) {
+ super(encodedKey);
+ }
+
+ public AESKey generateKey() throws InvalidKeySpecException {
+ try {
+ byte[] data = this.getEncoded();
+ InputStream stream = new ByteArrayInputStream(data);
+ Iterator parts = Arrays.asList(IOUtils.toString(stream, StandardCharsets.UTF_8).split(" ")).iterator();
+
+ checkArgument(size(parts) == 2 && AES_MARKER.equals(get(parts, 0)), "Bad format, should be: aes-seed AAB3...");
+ stream = new ByteArrayInputStream(Base64.getDecoder().decode(String.valueOf(get(parts, 1))));
+ String marker = IOUtils.toString(stream, StandardCharsets.UTF_8);
+ return new AESKey(marker);
+ } catch (Exception ex) {
+ throw new InvalidKeySpecException(ex);
+ }
+ }
+
+ @Override
+ public String getFormat() {
+ return "aes";
+ }
+
+}
diff --git a/src/main/java/net/locusworks/crypto/EncryptionKeyFactory.java b/src/main/java/net/locusworks/crypto/EncryptionKeyFactory.java
new file mode 100644
index 0000000..8d33f44
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/EncryptionKeyFactory.java
@@ -0,0 +1,39 @@
+package net.locusworks.crypto;
+
+import java.security.KeyFactory;
+import java.security.KeyFactorySpi;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+
+import sun.security.jca.GetInstance;
+import sun.security.jca.GetInstance.Instance;
+
+@SuppressWarnings("restriction")
+public class EncryptionKeyFactory extends KeyFactory {
+
+ protected EncryptionKeyFactory(KeyFactorySpi keyFacSpi, Provider provider, String algorithm) {
+ super(keyFacSpi, provider, algorithm);
+ }
+
+ public PrivateKey generatePrivateKey(KeySpec keySpec) throws InvalidKeySpecException {
+ if (keySpec instanceof AESKeySpec) {
+ return ((AESKeySpec)keySpec).generateKey();
+ }
+ return super.generatePrivate(keySpec);
+ }
+
+ public PublicKey generatePublicKey(KeySpec keySpec) throws InvalidKeySpecException {
+ keySpec = keySpec instanceof SSHEncodedKeySpec ? ((SSHEncodedKeySpec)keySpec).convertToRSAPubKeySpec() : keySpec;
+ return super.generatePublic(keySpec);
+ }
+
+ public static EncryptionKeyFactory getInstance(String algorithm) throws NoSuchAlgorithmException {
+ Instance instance = GetInstance.getInstance("KeyFactory", KeyFactorySpi.class, algorithm);
+ return new EncryptionKeyFactory((KeyFactorySpi)instance.impl, instance.provider, algorithm);
+ }
+
+}
diff --git a/src/main/java/net/locusworks/crypto/HashSalt.java b/src/main/java/net/locusworks/crypto/HashSalt.java
new file mode 100644
index 0000000..5c65f82
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/HashSalt.java
@@ -0,0 +1,189 @@
+package net.locusworks.crypto;
+
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+/**
+ * The type Hash salt.
+ * @author Isaac Parenteau
+ * @version 1.0.0
+ * @date 02/15/2018
+ */
+public class HashSalt {
+ /**
+ * The constant PBKDF2_ALGORITHM.
+ */
+ private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";
+
+ /**
+ * The constant SALT_BYTE_SIZE.
+ */
+ private static final int SALT_BYTE_SIZE = 24;
+
+ /**
+ * The constant HASH_BYTE_SIZE.
+ */
+ private static final int HASH_BYTE_SIZE = 24;
+
+ /**
+ * The constant PBKDF2_ITERATIONS.
+ */
+ private static final int PBKDF2_ITERATIONS = 1000;
+
+ /**
+ * The constant ITERATION_INDEX.
+ */
+ private static final int ITERATION_INDEX = 0;
+ /**
+ * The constant SALT_INDEX.
+ */
+ private static final int SALT_INDEX = 1;
+ /**
+ * The constant PBKDF2_INDEX.
+ */
+ private static final int PBKDF2_INDEX = 2;
+
+ /**
+ * Returns a salted PBKDF2 hash of the password.
+ *
+ * @param password the password to hash
+ *
+ * @return a salted PBKDF2 hash of the password
+ * @throws NoSuchAlgorithmException the no such algorithm exception
+ * @throws InvalidKeySpecException the invalid key spec exception
+ */
+ public static String createHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ return createHash(password.toCharArray());
+ }
+
+ /**
+ * Returns a salted PBKDF2 hash of the password.
+ *
+ * @param password the password to hash
+ *
+ * @return a salted PBKDF2 hash of the password
+ * @throws NoSuchAlgorithmException the no such algorithm exception
+ * @throws InvalidKeySpecException the invalid key spec exception
+ */
+ public static String createHash(char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ // Generate a random salt
+ SecureRandom random = new SecureRandom();
+ byte[] salt = new byte[SALT_BYTE_SIZE];
+ random.nextBytes(salt);
+
+ // Hash the password
+ byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
+ // format iterations:salt:hash
+ return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash);
+ }
+
+ /**
+ * Validates a password using a hash.
+ *
+ * @param password the password to check
+ * @param correctHash the hash of the valid password
+ *
+ * @return true if the password is correct, false if not
+ * @throws NoSuchAlgorithmException the no such algorithm exception
+ * @throws InvalidKeySpecException the invalid key spec exception
+ */
+ public static boolean validatePassword(String password, String correctHash) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ return validatePassword(password.toCharArray(), correctHash);
+ }
+
+ /**
+ * Validates a password using a hash.
+ *
+ * @param password the password to check
+ * @param correctHash the hash of the valid password
+ *
+ * @return true if the password is correct, false if not
+ * @throws NoSuchAlgorithmException the no such algorithm exception
+ * @throws InvalidKeySpecException the invalid key spec exception
+ */
+ public static boolean validatePassword(char[] password, String correctHash) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ // Decode the hash into its parameters
+ String[] params = correctHash.split(":");
+ int iterations = Integer.parseInt(params[ITERATION_INDEX]);
+ byte[] salt = fromHex(params[SALT_INDEX]);
+ byte[] hash = fromHex(params[PBKDF2_INDEX]);
+ // Compute the hash of the provided password, using the same salt,
+ // iteration count, and hash length
+ byte[] testHash = pbkdf2(password, salt, iterations, hash.length);
+ // Compare the hashes in constant time. The password is correct if
+ // both hashes match.
+ return slowEquals(hash, testHash);
+ }
+
+ /**
+ * Compares two byte arrays in length-constant time. This comparison method
+ * is used so that password hashes cannot be extracted from an on-line
+ * system using a timing attack and then attacked off-line.
+ *
+ * @param a the first byte array
+ * @param b the second byte array
+ * @return true if both byte arrays are the same, false if not
+ */
+ private static boolean slowEquals(byte[] a, byte[] b) {
+ int diff = a.length ^ b.length;
+ for(int i = 0; i < a.length && i < b.length; i++)
+ diff |= a[i] ^ b[i];
+ return diff == 0;
+ }
+
+ /**
+ * 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
+ */
+ private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
+ SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
+ return skf.generateSecret(spec).getEncoded();
+ }
+
+ /**
+ * Converts a string of hexadecimal characters into a byte array.
+ *
+ * @param hex the hex string
+ * @return the hex string decoded into a byte array
+ */
+ private static byte[] fromHex(String hex) {
+ byte[] binary = new byte[hex.length() / 2];
+ for(int i = 0; i < binary.length; i++) {
+ binary[i] = (byte)Integer.parseInt(hex.substring(2*i, 2*i+2), 16);
+ }
+ return binary;
+ }
+
+ /**
+ * Converts a byte array into a hexadecimal string.
+ *
+ * @param array the byte array to convert
+ * @return a length*2 character string encoding the byte array
+ */
+ private static String toHex(byte[] array) {
+ BigInteger bi = new BigInteger(1, array);
+ String hex = bi.toString(16);
+ int paddingLength = (array.length * 2) - hex.length();
+ if(paddingLength > 0)
+ return String.format("%0" + paddingLength + "d", 0) + hex;
+ else
+ return hex;
+ }
+
+ public static void main (String[] args) throws Exception {
+ if (args == null || !(args.length > 0)) {
+ throw new IllegalArgumentException("No args provided. Need password as argument");
+ }
+ System.out.println(HashSalt.createHash(String.valueOf(args[0])));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/locusworks/crypto/KeyFile.java b/src/main/java/net/locusworks/crypto/KeyFile.java
new file mode 100644
index 0000000..af0f84d
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/KeyFile.java
@@ -0,0 +1,216 @@
+package net.locusworks.crypto;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.Key;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import net.locusworks.crypto.utils.DataOutputStreamHelper;
+
+import static java.lang.String.join;
+
+public class KeyFile implements AutoCloseable {
+
+ public enum EncryptionType {
+ RSA,
+ AES,
+ SSH
+ }
+
+ private Key key;
+ private String description;
+ private Writer writer;
+ private EncryptionType encryptionType;
+
+ public KeyFile(Key key) {
+ this(key, null);
+ }
+
+ public KeyFile(Key key, String description) {
+ this(key, description, EncryptionType.valueOf(key.getAlgorithm().toUpperCase()));
+ }
+
+ public KeyFile(Key key, String description, EncryptionType encryptionType) {
+ this.key = key;
+ this.description = description;
+ this.encryptionType = encryptionType;
+ }
+
+ private KeyFile() {}
+
+ private void loadFromFile(String fileName) {
+ if (StringUtils.isBlank(fileName)) return;
+
+ this.key = null;
+ try {
+ File keyFile = new File(fileName);
+ if (!keyFile.exists()) {
+ throw new IllegalArgumentException(String.format("Unable to find file with name %s. Please check path", fileName));
+ }
+
+ String contentStr = IOUtils.toString(new FileInputStream(keyFile), StandardCharsets.UTF_8);
+
+ boolean rsaFormat = !contentStr.startsWith("ssh-rsa") && !contentStr.startsWith("aes-seed");
+ if (rsaFormat) {
+ contentStr = contentStr.replace("-----.*", "");
+ }
+
+ contentStr = contentStr.replace("\\r?\\n", "");
+
+ byte[] content = rsaFormat ? Base64.getDecoder().decode(contentStr): contentStr.getBytes(StandardCharsets.UTF_8);
+
+ EncryptionKeyFactory kf = EncryptionKeyFactory.getInstance("RSA");
+
+ List keySpecs = Lists.newArrayList(
+ new KeySpecHelper(new AESKeySpec(content), true, EncryptionType.AES),
+ new KeySpecHelper(new SSHEncodedKeySpec(content), false, EncryptionType.SSH),
+ new KeySpecHelper(new PKCS8EncodedKeySpec(content), true, EncryptionType.RSA),
+ new KeySpecHelper(new X509EncodedKeySpec(content), false, EncryptionType.RSA)
+ );
+
+ for (KeySpecHelper ksh : keySpecs) {
+ try {
+ this.key = ksh.isPrivate() ? kf.generatePrivateKey(ksh.getKeySpec()) : kf.generatePublicKey(ksh.getKeySpec());
+ this.encryptionType = ksh.getEncryptionType();
+ return;
+ } catch (NullPointerException | InvalidKeySpecException ikse) { continue; }
+ }
+
+ throw new InvalidKeySpecException(String.format("Unable to determine if file %s is a private or public key. Not type of PKCS8, X509, SSH or AES spec", fileName));
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public void write(String fileName) {
+ try {
+ String data;
+ switch(this.encryptionType) {
+ case AES:
+ data = String.format("%s %s", this.key.getFormat(), Base64.getEncoder().encodeToString(this.key.getEncoded()));
+ IOUtils.write(data, Files.newOutputStream(Paths.get(fileName)), StandardCharsets.UTF_8);
+ break;
+ case SSH:
+ try(DataOutputStreamHelper dos = new DataOutputStreamHelper()) {
+ for(byte[] item : getSSHPubKeyBytes(this.key)) {
+ dos.writeInt(item.length);
+ dos.write(item);
+ }
+ data = String.format("ssh-rsa", dos.base64Encoded(), this.description);
+ IOUtils.write(data, Files.newOutputStream(Paths.get(fileName)), StandardCharsets.UTF_8);
+ }
+ break;
+ default:
+ writePem(fileName);
+ }
+ } catch (Exception ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ if (StringUtils.isBlank(this.description)) {
+ return this.key instanceof PrivateKey ? "PRIVATE KEY" : "PUBLIC KEY";
+ }
+ return this.description;
+ }
+
+ public Key getKey() {
+ return this.key;
+ }
+
+ @Override
+ public void close() {
+ try {
+ if (this.writer != null) {
+ this.writer.flush();
+ this.writer.close();
+ this.writer = null;
+ }
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static KeyFile read(String fileName) {
+ try (KeyFile kf = new KeyFile()) {
+ kf.loadFromFile(fileName);
+ return kf;
+ }
+ }
+
+ private void writePem(String fileName) {
+ try {
+ String desc = getDescription();
+ this.writer = new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8);
+ this.writer.write(String.format("-----BEGIN RSA %s-----", desc));
+
+ String encoded = Base64.getEncoder().encodeToString(this.key.getEncoded());
+ String out = join("\n", Splitter.fixedLength(60).split(encoded));
+
+ this.writer.write(out);
+ this.writer.write(String.format("-----END RSA %s-----", desc));
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private List getSSHPubKeyBytes(Key key) {
+ RSAPublicKey rpk = (RSAPublicKey)key;
+ return Arrays.asList("ssh-rsa".getBytes(StandardCharsets.UTF_8),
+ rpk.getPublicExponent().toByteArray(),
+ rpk.getModulus().toByteArray()
+ );
+ }
+
+ private static class KeySpecHelper {
+ private KeySpec keySpec;
+ private boolean isPrivate;
+ private EncryptionType encryptionType;
+
+ public KeySpecHelper(KeySpec keySpec, boolean isPrivate, EncryptionType encryptionType) {
+ super();
+ this.keySpec = keySpec;
+ this.isPrivate = isPrivate;
+ this.encryptionType = encryptionType;
+ }
+
+ public synchronized final KeySpec getKeySpec() {
+ return keySpec;
+ }
+
+ public synchronized final boolean isPrivate() {
+ return isPrivate;
+ }
+
+ public synchronized final EncryptionType getEncryptionType() {
+ return encryptionType;
+ }
+ }
+
+}
diff --git a/src/main/java/net/locusworks/crypto/RSA.java b/src/main/java/net/locusworks/crypto/RSA.java
new file mode 100644
index 0000000..2416181
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/RSA.java
@@ -0,0 +1,157 @@
+package net.locusworks.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.interfaces.RSAKey;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.IllegalBlockSizeException;
+
+import org.apache.commons.io.IOUtils;
+
+import net.locusworks.crypto.KeyFile.EncryptionType;
+
+public class RSA {
+
+ private static final String ENCRYPTION_TYPE = "RSA";
+ private static final String ENCRYPTION_ALGORITHM = "RSA/ECB/PKCS10PADDING";
+ private static final String PROVIDER = "SunJCE";
+ private static final String RANDOM_ALGORITHM = "SHA1PRNG";
+
+ private static final int PADDING_LENGTH = 11;
+ private static final int DEFAULT_KEY_LENGTH = 2048;
+
+ public static KeyPair generateKeyPair() {
+ return generateKeyPair(DEFAULT_KEY_LENGTH);
+ }
+
+ public static KeyPair generateKeyPair(int keyLength) {
+ try {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(ENCRYPTION_TYPE);
+ SecureRandom sr = SecureRandom.getInstance(RANDOM_ALGORITHM);
+ kpg.initialize(keyLength, sr);
+ return kpg.genKeyPair();
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static KeyPair loadPrivateKey(String privateKeyFileName) {
+ return loadKeyPair(null, privateKeyFileName);
+ }
+
+ public static KeyPair loadPublicKey(String publicKeyFileName) {
+ return loadKeyPair(publicKeyFileName, null);
+ }
+
+ public static KeyPair loadKeyPair(String publicKey, String privateKey) {
+ KeyFile pubKey = KeyFile.read(publicKey);
+ KeyFile prvKey = KeyFile.read(privateKey);
+ return new KeyPair((PublicKey)pubKey.getKey(), (PrivateKey)prvKey.getKey());
+ }
+
+ public static boolean generateAndWriteSSHKeys() {
+ KeyPair kp = generateKeyPair();
+ return writePrivateKey(kp) && writePublicKey(kp, true);
+ }
+
+ public static boolean generateAndWriteKeyPair() {
+ return generateAndWriteKeyPair(DEFAULT_KEY_LENGTH);
+ }
+
+ public static boolean generateAndWriteKeyPair(String keyPairName) {
+ return generateAndWriteKeyPair(keyPairName, DEFAULT_KEY_LENGTH);
+ }
+
+ public static boolean generateAndWriteKeyPair(int keyLength) {
+ return generateAndWriteKeyPair("id_rsa", keyLength);
+ }
+
+ public static boolean generateAndWriteKeyPair(String keyPairName, int keyLength) {
+ KeyPair kp = generateKeyPair(keyLength);
+ return writePrivateKey(kp, keyPairName, "PRIVATE KEY") && writePublicKey(kp, keyPairName + ".pub", "PUBLIC KEY");
+ }
+
+ public static boolean writePrivateKey(KeyPair kp) {
+ return writePrivateKey(kp, "id_rsa", "PRIVATE KEY");
+ }
+
+ public static boolean writePrivateKey(KeyPair kp, String fileName, String description) {
+ return writePemFile(kp.getPrivate(), fileName, description);
+ }
+
+ public static boolean writePublicKey(KeyPair kp) {
+ return writePublicKey(kp, false);
+ }
+
+ public static boolean writePublicKey(KeyPair kp, boolean sshFormat) {
+ return writePublicKey(kp, "id_rsa.pub", "PUBLIC KEY", sshFormat);
+ }
+
+ public static boolean writePublicKey(KeyPair kp, String fileName, String description) {
+ return writePemFile(kp.getPublic(), fileName, description, false);
+ }
+
+ public static boolean writePublicKey(KeyPair kp, String fileName, String description, boolean sshFormat) {
+ return writePemFile(kp.getPublic(), fileName, description, sshFormat);
+ }
+
+ public static int calculateRequiredKeyLength(String message) {
+ return (message.getBytes(StandardCharsets.UTF_8).length + PADDING_LENGTH) * 8;
+ }
+
+ public static String encrypt(Key key, String message) {
+ try {
+ calculateKeyLength(key, message);
+ Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ CipherInputStream cis = new CipherInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)), cipher);
+ byte[] encrypted = IOUtils.toByteArray(cis);
+ return Base64.getEncoder().encodeToString(encrypted);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static String decrypt(Key key, String message) {
+ try {
+ Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM, PROVIDER);
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ byte[] decoded = Base64.getDecoder().decode(message);
+ byte[] plainTextArray = cipher.doFinal(decoded);
+ return new String(plainTextArray, StandardCharsets.UTF_8);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private static boolean writePemFile(Key key, String fileName, String description) {
+ return writePemFile(key, fileName, description, false);
+ }
+
+ private static boolean writePemFile(Key key, String fileName, String description, boolean sshFormat) {
+ try(KeyFile kf = sshFormat ? new KeyFile(key, description, EncryptionType.SSH) : new KeyFile(key, description)) {
+ kf.write(fileName);
+ }
+ return Files.exists(Paths.get(fileName));
+ }
+
+ private static void calculateKeyLength(Key key, String message) throws IllegalBlockSizeException {
+ int keyLength = ((RSAKey)key).getModulus().bitLength();
+ int requiredKeyLength = calculateRequiredKeyLength(message);
+ if (keyLength < requiredKeyLength) {
+ throw new IllegalBlockSizeException(String.format("RSA key size of %d is not large enough to encrypt message of length %d. "
+ + "Increase key size to a minimum of %d and re-encrypt with new key", keyLength, message.length(), requiredKeyLength));
+ }
+ }
+}
diff --git a/src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java b/src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java
new file mode 100644
index 0000000..c6e6bb6
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java
@@ -0,0 +1,67 @@
+package net.locusworks.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Iterator;
+
+import org.apache.commons.io.IOUtils;
+
+import static com.google.common.collect.Iterators.get;
+import static com.google.common.collect.Iterators.size;
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+public class SSHEncodedKeySpec extends EncodedKeySpec {
+
+ private static final String SSH_MARKER = "ssh-rsa";
+
+ public SSHEncodedKeySpec(byte[] encodedKey) {
+ super(encodedKey);
+ }
+
+ public RSAPublicKeySpec convertToRSAPubKeySpec() throws InvalidKeySpecException {
+ try {
+ byte[] data = this.getEncoded();
+ InputStream stream = new ByteArrayInputStream(data);
+ Iterator parts = Arrays.asList(IOUtils.toString(stream, StandardCharsets.UTF_8).split(" ")).iterator();
+
+ checkArgument(size(parts) >= 2 && SSH_MARKER.equals(get(parts, 0)), "Bad format, should be: ssh-rsa AAB3...");
+ stream = new ByteArrayInputStream(Base64.getDecoder().decode(String.valueOf(get(parts, 1))));
+ String marker = new String(readLengthFirst(stream));
+ checkArgument(SSH_MARKER.equals(marker), "Looking for marker %s but received %s", SSH_MARKER, marker);
+ BigInteger publicExponent = new BigInteger(readLengthFirst(stream));
+ BigInteger modulus = new BigInteger(readLengthFirst(stream));
+ RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
+ return keySpec;
+ } catch (Exception ex) {
+ throw new InvalidKeySpecException(ex);
+ }
+ }
+
+ @Override
+ public String getFormat() {
+ return null;
+ }
+
+ private static byte[] readLengthFirst(InputStream in) throws IOException {
+ int[] bytes = new int[] {in.read(), in.read(), in.read(), in.read()};
+ int length = 0;
+ int shift = 24;
+ for (int i = 0; i < bytes.length; i++) {
+ length += bytes[i] << shift;
+ shift -= 8;
+ }
+ byte[] val = new byte[length];
+ in.read(val);
+ return val;
+ }
+
+}
diff --git a/src/main/java/net/locusworks/crypto/package-info.java b/src/main/java/net/locusworks/crypto/package-info.java
new file mode 100644
index 0000000..6c7de80
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Package contains classes that help encrypting, decrypting, salting and hashing objects
+ * @author Isaac Parenteau
+ */
+package net.locusworks.crypto;
\ No newline at end of file
diff --git a/src/main/java/net/locusworks/crypto/utils/DataOutputStreamHelper.java b/src/main/java/net/locusworks/crypto/utils/DataOutputStreamHelper.java
new file mode 100644
index 0000000..f3a3dc4
--- /dev/null
+++ b/src/main/java/net/locusworks/crypto/utils/DataOutputStreamHelper.java
@@ -0,0 +1,49 @@
+package net.locusworks.crypto.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public class DataOutputStreamHelper extends DataOutputStream implements AutoCloseable{
+
+ public DataOutputStreamHelper() {
+ this(new ByteArrayOutputStream());
+ }
+
+ public DataOutputStreamHelper(OutputStream out) {
+ super(out);
+ }
+
+ public byte[] toByteArray() {
+ if (super.out == null) {
+ return new byte[0];
+ }
+ if (super.out instanceof ByteArrayOutputStream) {
+ return ((ByteArrayOutputStream)super.out).toByteArray();
+ }
+ return super.out.toString().getBytes(StandardCharsets.UTF_8);
+ }
+
+ public String base64Encoded() {
+ return Base64.getEncoder().encodeToString(this.toByteArray());
+ }
+
+ @Override
+ public String toString() {
+ return new String(this.toByteArray(), StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (super.out != null) {
+ try {
+ super.out.close();
+ super.out = null;
+ } catch (Exception ex) {}
+ }
+ }
+
+}