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) {} + } + } + +}