Added initial crypto library
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -72,6 +72,9 @@ local.properties
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
180
Jenkinsfile
vendored
Normal file
180
Jenkinsfile
vendored
Normal file
@ -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
|
82
pom.xml
Normal file
82
pom.xml
Normal file
@ -0,0 +1,82 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>net.locusworks</groupId>
|
||||
<artifactId>crypto</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>Crypto</name>
|
||||
<description>Crypto library</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.20.1</version>
|
||||
<configuration>
|
||||
<forkMode>always</forkMode>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.7.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.owasp</groupId>
|
||||
<artifactId>dependency-check-maven</artifactId>
|
||||
<version>5.2.2</version>
|
||||
<configuration>
|
||||
<skipProvidedScope>true</skipProvidedScope>
|
||||
<skipTestScope>true</skipTestScope>
|
||||
<failOnError>false</failOnError>
|
||||
<versionCheckEnabled>true</versionCheckEnabled>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>28.1-jre</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.9</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
172
src/main/java/net/locusworks/crypto/AES.java
Normal file
172
src/main/java/net/locusworks/crypto/AES.java
Normal file
@ -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])));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
29
src/main/java/net/locusworks/crypto/AESKey.java
Normal file
29
src/main/java/net/locusworks/crypto/AESKey.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
46
src/main/java/net/locusworks/crypto/AESKeySpec.java
Normal file
46
src/main/java/net/locusworks/crypto/AESKeySpec.java
Normal file
@ -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<String> 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";
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
189
src/main/java/net/locusworks/crypto/HashSalt.java
Normal file
189
src/main/java/net/locusworks/crypto/HashSalt.java
Normal file
@ -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])));
|
||||
}
|
||||
}
|
216
src/main/java/net/locusworks/crypto/KeyFile.java
Normal file
216
src/main/java/net/locusworks/crypto/KeyFile.java
Normal file
@ -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<KeySpecHelper> 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<byte[]> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
157
src/main/java/net/locusworks/crypto/RSA.java
Normal file
157
src/main/java/net/locusworks/crypto/RSA.java
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
67
src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java
Normal file
67
src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java
Normal file
@ -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<String> 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;
|
||||
}
|
||||
|
||||
}
|
5
src/main/java/net/locusworks/crypto/package-info.java
Normal file
5
src/main/java/net/locusworks/crypto/package-info.java
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Package contains classes that help encrypting, decrypting, salting and hashing objects
|
||||
* @author Isaac Parenteau
|
||||
*/
|
||||
package net.locusworks.crypto;
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user