diff --git a/.gitignore b/.gitignore
index 24cdadc..769ad1a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,3 +96,4 @@ local.properties
.scala_dependencies
.worksheet
+.idea
diff --git a/Jenkinsfile b/Jenkinsfile
index 3b19af3..8ad0292 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,186 +1,101 @@
-#!groovy
+pipeline {
+ agent any
-// Required Jenkins plugins:
-// https://wiki.jenkins-ci.org/display/JENKINS/Timestamper
-// https://wiki.jenkins-ci.org/display/JENKINS/Static+Code+Analysis+Plug-ins
-// https://wiki.jenkins-ci.org/display/JENKINS/Checkstyle+Plugin ?
-// https://wiki.jenkins-ci.org/display/JENKINS/FindBugs+Plugin
-// https://wiki.jenkins-ci.org/display/JENKINS/PMD+Plugin ?
-// https://wiki.jenkins-ci.org/display/JENKINS/DRY+Plugin ?
-// https://wiki.jenkins-ci.org/display/JENKINS/Task+Scanner+Plugin
-// https://wiki.jenkins-ci.org/display/JENKINS/Javadoc+Plugin
-// https://wiki.jenkins-ci.org/display/JENKINS/JaCoCo+Plugin ?
+ options {
+ buildDiscarder(logRotator(numToKeepStr: '5'))
+ disableConcurrentBuilds()
+ skipDefaultCheckout(true)
+ timestamps()
+ }
-
-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
- branch_name_docker = branch_name.replaceAll(/\//,'.')
- persist = "/var/lib/jenkins/PERSIST/${branch_name_docker}"
-
- // 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('hotfix')==0) build_type='hotfix'
- if (branch_name.indexOf('bugfix')==0) build_type='bugfix'
- if (branch_name.indexOf('master')==0) build_type='master'
-
- // common pipeline elements
- node('master') {
- Initialize()
- SetVersion(build_type)
- print_vars() // after SetVersion - all variables now defined
- set_result('INPROGRESS')
- Build() // builds database via flyway migration
- }
-
- if (branch_name.indexOf('develop')==0) {
- node('master') {
- Deploy();
- }
- } else if (branch_name.indexOf('release/')==0) {
- node('master') {
- Deploy();
+ stages {
+ stage('Checkout') {
+ steps {
+ checkout scm
}
}
- node('master') {
- set_result('SUCCESS')
+ stage('Set version') {
+ steps {
+ script {
+ String branch = env.BRANCH_NAME
+ if (!branch) {
+ error('BRANCH_NAME is unavailable; this pipeline must run as a multibranch job')
+ }
+
+ env.DEPLOY_BUILD = 'false'
+
+ // A release branch supplies major.minor; Jenkins supplies the patch
+ // number so release/2.0 build 1 becomes 2.0.1-RELEASE.
+ def releaseMatch = branch =~ /^release\/(\d+\.\d+)$/
+ if (releaseMatch.matches()) {
+ env.BUILD_VERSION = "${releaseMatch[0][1]}.${env.BUILD_NUMBER}-RELEASE"
+ env.DEPLOY_BUILD = 'true'
+ } else if (branch == 'develop') {
+ env.BUILD_VERSION = "develop.${env.BUILD_NUMBER}-SNAPSHOT"
+ env.DEPLOY_BUILD = 'true'
+ } else {
+ String safeBranch = branch.replaceAll(/[^A-Za-z0-9._-]/, '-')
+ env.BUILD_VERSION = "0.0.${env.BUILD_NUMBER}-${safeBranch}-SNAPSHOT"
+ }
+ currentBuild.displayName = env.BUILD_VERSION
+ }
+
+ withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') {
+ sh 'mvn -B org.codehaus.mojo:versions-maven-plugin:2.21.0:set -DgenerateBackupPoms=false -DnewVersion="$BUILD_VERSION"'
+ }
+ }
}
- } catch (err) {
- node() {
- set_result('FAILURE')
+ stage('Prepare OWASP cache') {
+ steps {
+ lock(resource: 'owasp-nvd-cache') {
+ withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') {
+ sh 'mkdir -p "$HOME/.cache/dependency-check"'
+ sh 'mvn -B org.owasp:dependency-check-maven:13.0.0:update-only -DdataDirectory="$HOME/.cache/dependency-check"'
+ }
+ }
+ }
}
- throw err
- }
-}
-def Build() {
- stage ('build') {
- mvn "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() {
- stage ('deploy') {
- mvn "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(args) {
- withMaven(
- maven: 'maven-3.6.1',
- globalMavenSettingsConfig: 'locusworks-settings'
- ) {
-
- sh "mvn ${args}"
-
- }
-}
-
-def mvn_initial(args) {
- 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")
- }
-
- // save in persistence file for access the status page
- // make sure the directory exists first
- sh "mkdir -p $persist && echo $status > $persist/build.result"
-}
-
-def notify_bitbucket(state) {
-}
-
-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]
- if ( v == '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";
- //version = branch_name.substring('release/'.length()) + "." + build_number
- currentBuild.displayName = version
- } else if (v == 'develop') {
- version = branch_name_base + "." + build_number + "-SNAPSHOT";
- currentBuild.displayName = version
- } else {
- // 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
- // #101 - feature/user/foo
- //version = '0.' + build_number
- version = branch_name_base + "." + build_number
- currentBuild.displayName = "#" + build_number + " - " + branch_name_base
+ stage('Build and test') {
+ steps {
+ withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') {
+ sh 'mvn -B clean verify -DdataDirectory="$HOME/.cache/dependency-check" -DautoUpdate=false -Dbuild.revision="$GIT_COMMIT"'
+ }
+ }
+ post {
+ always {
+ junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml'
+ }
+ success {
+ archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
+ archiveArtifacts artifacts: '**/target/site/jacoco/**', fingerprint: true
+ }
+ }
+ }
+
+ stage('Deploy') {
+ when {
+ expression { env.DEPLOY_BUILD == 'true' }
+ }
+ steps {
+ withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') {
+ sh 'mvn -B deploy -DskipTests -Ddependency-check.skip=true -Dbuild.number="$BUILD_NUMBER" -Dbuild.revision="$GIT_COMMIT"'
+ }
+ }
+ }
+ }
+
+ post {
+ success {
+ echo "Published build ${env.BUILD_VERSION} successfully"
+ }
+ failure {
+ echo "Build ${env.BUILD_VERSION ?: env.BUILD_NUMBER} failed"
+ }
+ cleanup {
+ deleteDir()
}
- display_name = currentBuild.displayName
- mvn_initial "versions:set -DnewVersion=${version}"
}
}
-
-return this
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 825861a..4404d99 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,26 +19,70 @@
- 1.8
- 1.8
- 4.1
+ 21
+ 21
+ UTF-8
https://nexus.locusworks.net
+ 5.1.2
+ 6.1.3
+ 5.23.0
+ 0.8.15
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+
+ prepare-agent
+
+
+
+ report
+ verify
+
+ report
+
+
+
+ coverage-check
+ verify
+
+ check
+
+
+
+
+ BUNDLE
+
+
+ LINE
+ COVEREDRATIO
+ 0.75
+
+
+
+
+
+
+
+
org.apache.maven.plugins
maven-surefire-plugin
- 3.0.0-M5
+ 3.5.6
- always
+ @{argLine} -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar
org.apache.maven.plugins
maven-compiler-plugin
- 3.8.1
+ 3.15.0
${maven.compiler.source}
${maven.compiler.target}
@@ -51,7 +95,14 @@
org.owasp
dependency-check-maven
- 6.3.1
+ 13.0.0
+
+ true
+ true
+ true
+ true
+ e2eb1036-9b5b-4df9-95e7-0888be947011
+
@@ -90,42 +141,43 @@
-
- org.junit.jupiter
- junit-jupiter-api
- 5.8.0
- test
-
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
- commons-io
- commons-io
- 2.11.0
-
+ commons-io
+ commons-io
+ 2.22.0
+
- commons-codec
- commons-codec
- 1.15
-
+ commons-codec
+ commons-codec
+ 1.22.1
+ test
+
- com.google.guava
- guava
- 30.1.1-jre
-
+ com.google.guava
+ guava
+ 33.7.1-jre
+
- org.apache.commons
- commons-lang3
- 3.12.0
-
+ org.apache.commons
+ commons-lang3
+ 3.20.0
+
@@ -168,4 +220,4 @@
-
\ No newline at end of file
+
diff --git a/src/main/java/net/locusworks/crypto/AESKeySpec.java b/src/main/java/net/locusworks/crypto/AESKeySpec.java
index 9ccb7f6..3daea62 100644
--- a/src/main/java/net/locusworks/crypto/AESKeySpec.java
+++ b/src/main/java/net/locusworks/crypto/AESKeySpec.java
@@ -32,15 +32,11 @@ 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 java.util.Base64;
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;
+import static com.google.common.base.Preconditions.checkArgument;
public class AESKeySpec extends EncodedKeySpec {
@@ -54,10 +50,10 @@ public class AESKeySpec extends EncodedKeySpec {
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[] parts = IOUtils.toString(stream, StandardCharsets.UTF_8).split(" ");
+
+ checkArgument(parts.length == 2 && AES_MARKER.equals(parts[0]), "Bad format, should be: aes-seed AAB3...");
+ stream = new ByteArrayInputStream(Base64.getDecoder().decode(parts[1]));
String marker = IOUtils.toString(stream, StandardCharsets.UTF_8);
return new AESKey(marker);
} catch (Exception ex) {
diff --git a/src/main/java/net/locusworks/crypto/EncryptionKeyFactory.java b/src/main/java/net/locusworks/crypto/EncryptionKeyFactory.java
index 31ea1f6..1e5e522 100644
--- a/src/main/java/net/locusworks/crypto/EncryptionKeyFactory.java
+++ b/src/main/java/net/locusworks/crypto/EncryptionKeyFactory.java
@@ -36,11 +36,7 @@ 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 {
+public class EncryptionKeyFactory extends KeyFactory {
protected EncryptionKeyFactory(KeyFactorySpi keyFacSpi, Provider provider, String algorithm) {
super(keyFacSpi, provider, algorithm);
@@ -56,11 +52,24 @@ public class EncryptionKeyFactory extends KeyFactory {
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);
- }
+ }
+
+ public static EncryptionKeyFactory getInstance(String algorithm) throws NoSuchAlgorithmException {
+ KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
+ Provider provider = keyFactory.getProvider();
+ Provider.Service service = provider.getService("KeyFactory", algorithm);
+ if (service == null) {
+ throw new NoSuchAlgorithmException(
+ String.format("KeyFactory %s is not available from provider %s", algorithm, provider.getName()));
+ }
+
+ Object implementation = service.newInstance(null);
+ if (!(implementation instanceof KeyFactorySpi)) {
+ throw new NoSuchAlgorithmException(
+ String.format("Provider %s returned an invalid KeyFactory implementation for %s",
+ provider.getName(), algorithm));
+ }
+ return new EncryptionKeyFactory((KeyFactorySpi)implementation, provider, algorithm);
+ }
}
diff --git a/src/main/java/net/locusworks/crypto/KeyFile.java b/src/main/java/net/locusworks/crypto/KeyFile.java
index 684eaec..85d90c3 100644
--- a/src/main/java/net/locusworks/crypto/KeyFile.java
+++ b/src/main/java/net/locusworks/crypto/KeyFile.java
@@ -28,7 +28,6 @@
package net.locusworks.crypto;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
@@ -96,14 +95,15 @@ public class KeyFile implements AutoCloseable {
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);
+ String contentStr = Files.readString(keyFile.toPath(), StandardCharsets.UTF_8);
- boolean rsaFormat = !contentStr.startsWith("ssh-rsa") && !contentStr.startsWith("aes-seed");
- if (rsaFormat) {
- contentStr = contentStr.replace("-----.*", "");
- }
-
- contentStr = contentStr.replace("\\r?\\n", "");
+ boolean rsaFormat = !contentStr.startsWith("ssh-rsa") && !contentStr.startsWith("aes-seed");
+ if (rsaFormat) {
+ contentStr = contentStr.replaceAll("-----BEGIN RSA .*?-----|-----END RSA .*?-----", "");
+ contentStr = contentStr.replaceAll("\\s", "");
+ } else {
+ contentStr = contentStr.trim();
+ }
byte[] content = rsaFormat ? Base64.getDecoder().decode(contentStr): contentStr.getBytes(StandardCharsets.UTF_8);
@@ -144,7 +144,7 @@ public class KeyFile implements AutoCloseable {
dos.writeInt(item.length);
dos.write(item);
}
- data = String.format("ssh-rsa", dos.base64Encoded(), this.description);
+ data = String.format("ssh-rsa %s %s", dos.base64Encoded(), getDescription());
IOUtils.write(data, Files.newOutputStream(Paths.get(fileName)), StandardCharsets.UTF_8);
}
break;
diff --git a/src/main/java/net/locusworks/crypto/RSA.java b/src/main/java/net/locusworks/crypto/RSA.java
index 2ddeb59..32ea943 100644
--- a/src/main/java/net/locusworks/crypto/RSA.java
+++ b/src/main/java/net/locusworks/crypto/RSA.java
@@ -51,7 +51,7 @@ 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 ENCRYPTION_ALGORITHM = "RSA/ECB/PKCS1Padding";
private static final String PROVIDER = "SunJCE";
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
diff --git a/src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java b/src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java
index 929d388..7300af5 100644
--- a/src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java
+++ b/src/main/java/net/locusworks/crypto/SSHEncodedKeySpec.java
@@ -35,15 +35,11 @@ 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 java.util.Base64;
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;
+import static com.google.common.base.Preconditions.checkArgument;
public class SSHEncodedKeySpec extends EncodedKeySpec {
@@ -58,10 +54,10 @@ public class SSHEncodedKeySpec extends EncodedKeySpec {
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[] parts = IOUtils.toString(stream, StandardCharsets.UTF_8).split(" ");
+
+ checkArgument(parts.length >= 2 && SSH_MARKER.equals(parts[0]), "Bad format, should be: ssh-rsa AAB3...");
+ stream = new ByteArrayInputStream(Base64.getDecoder().decode(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));
diff --git a/src/test/java/net/locusworks/crypto/tests/ConfigurationManagerTest.java b/src/test/java/net/locusworks/crypto/tests/ConfigurationManagerTest.java
new file mode 100644
index 0000000..dccda1f
--- /dev/null
+++ b/src/test/java/net/locusworks/crypto/tests/ConfigurationManagerTest.java
@@ -0,0 +1,99 @@
+/*
+ * Project: Crypto, File: ConfigurationManagerTest.java
+ * Copyright 2019 Locusworks LLC. All rights reserved.
+ */
+package net.locusworks.crypto.tests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import net.locusworks.crypto.configuration.ConfigurationManager;
+import net.locusworks.crypto.configuration.PersistableRequest;
+
+class ConfigurationManagerTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void initializationCreatesConfigurationFromDefaultsAndReportsProgress() throws Exception {
+ TestManager manager = new TestManager();
+ List messages = new java.util.ArrayList<>();
+ manager.initialize(tempDir, messages);
+
+ assertEquals("localhost", manager.getPropertyValue("dbHost"));
+ assertEquals("fallback", manager.getPropertyValue("missing", "fallback"));
+ assertEquals(null, manager.getPropertyValue("missing"));
+ assertEquals(4, manager.getConfiguration().size());
+ assertTrue(Files.exists(tempDir.resolve("test.properties")));
+ assertTrue(messages.stream().anyMatch(message -> message.startsWith("Loading config file:")));
+ assertTrue(messages.stream().anyMatch(message -> message.contains("Added new configuration items")));
+ }
+
+ @Test
+ void saveAndReloadReconcilesAddedAndRemovedConfiguration() throws Exception {
+ TestManager manager = new TestManager();
+ List messages = new java.util.ArrayList<>();
+ manager.initialize(tempDir, messages);
+
+ Properties changed = new Properties();
+ changed.setProperty("dbHost", "example.test");
+ changed.setProperty("obsolete", "remove me");
+ manager.saveToConf(changed);
+
+ assertEquals("example.test", manager.getPropertyValue("dbHost"));
+ assertFalse(manager.getConfiguration().containsKey("obsolete"));
+ assertEquals(4, manager.getConfiguration().size());
+ assertTrue(messages.stream().anyMatch(message -> message.startsWith("Saved config file:")));
+ }
+
+ @Test
+ void persistableRequestUpdatesPlainAndEncryptedValues() throws Exception {
+ TestManager manager = new TestManager();
+ manager.initialize(tempDir, new java.util.ArrayList<>());
+
+ Request request = new Request("database.test", "secret", "ignored");
+ manager.saveConfiguration(request, Set.of("dbHost", "logLevel"), Set.of("logLevel"));
+
+ assertEquals("database.test", manager.getPropertyValue("dbHost"));
+ assertFalse("secret".equals(manager.getPropertyValue("logLevel")));
+ assertEquals("secret", manager.decrypt(manager.getPropertyValue("logLevel")));
+ assertThrows(IOException.class,
+ () -> manager.saveConfiguration(request, new HashSet<>(), null));
+ }
+
+ private static final class TestManager extends ConfigurationManager {
+ void initialize(Path baseDir, List messages) throws IOException {
+ init(baseDir.toString(), "test.properties", "fixed test seed".getBytes(), messages::add);
+ }
+
+ String decrypt(String value) {
+ return aes.decrypt(value);
+ }
+ }
+
+ private static final class Request implements PersistableRequest {
+ private final String dbHost;
+ private final String logLevel;
+ private final String ignored;
+
+ Request(String dbHost, String logLevel, String ignored) {
+ this.dbHost = dbHost;
+ this.logLevel = logLevel;
+ this.ignored = ignored;
+ }
+ }
+}
diff --git a/src/test/java/net/locusworks/crypto/tests/KeyAndRsaTest.java b/src/test/java/net/locusworks/crypto/tests/KeyAndRsaTest.java
new file mode 100644
index 0000000..db304cf
--- /dev/null
+++ b/src/test/java/net/locusworks/crypto/tests/KeyAndRsaTest.java
@@ -0,0 +1,140 @@
+/*
+ * Project: Crypto, File: KeyAndRsaTest.java
+ * Copyright 2019 Locusworks LLC. All rights reserved.
+ */
+package net.locusworks.crypto.tests;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Base64;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import net.locusworks.crypto.AESKey;
+import net.locusworks.crypto.AESKeySpec;
+import net.locusworks.crypto.EncryptionKeyFactory;
+import net.locusworks.crypto.KeyFile;
+import net.locusworks.crypto.RSA;
+import net.locusworks.crypto.SSHEncodedKeySpec;
+import net.locusworks.crypto.utils.DataOutputStreamHelper;
+
+class KeyAndRsaTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void aesKeyAndSpecRoundTrip() throws Exception {
+ AESKey key = new AESKey("repeatable seed");
+ assertEquals("aes", key.getAlgorithm());
+ assertEquals("aes-seed", key.getFormat());
+ assertArrayEquals("repeatable seed".getBytes(StandardCharsets.UTF_8), key.getEncoded());
+
+ String encoded = "aes-seed " + Base64.getEncoder().encodeToString(key.getEncoded());
+ AESKey decoded = new AESKeySpec(encoded.getBytes(StandardCharsets.UTF_8)).generateKey();
+ assertArrayEquals(key.getEncoded(), decoded.getEncoded());
+ assertEquals("aes", new AESKeySpec(encoded.getBytes(StandardCharsets.UTF_8)).getFormat());
+ assertThrows(InvalidKeySpecException.class,
+ () -> new AESKeySpec("invalid".getBytes(StandardCharsets.UTF_8)).generateKey());
+ }
+
+ @Test
+ void encryptionKeyFactoryGeneratesRsaKeysAndRejectsUnknownAlgorithms() throws Exception {
+ KeyPair pair = RSA.generateKeyPair(1024);
+ EncryptionKeyFactory factory = EncryptionKeyFactory.getInstance("RSA");
+ assertEquals(pair.getPrivate(), factory.generatePrivateKey(
+ new java.security.spec.PKCS8EncodedKeySpec(pair.getPrivate().getEncoded())));
+ assertEquals(pair.getPublic(), factory.generatePublicKey(
+ new java.security.spec.X509EncodedKeySpec(pair.getPublic().getEncoded())));
+ assertThrows(NoSuchAlgorithmException.class,
+ () -> EncryptionKeyFactory.getInstance("not-an-algorithm"));
+ }
+
+ @Test
+ void rsaEncryptsDecryptsAndValidatesKeySize() {
+ KeyPair pair = RSA.generateKeyPair(1024);
+ String encrypted = RSA.encrypt(pair.getPublic(), "hello RSA");
+ assertEquals("hello RSA", RSA.decrypt(pair.getPrivate(), encrypted));
+ assertEquals(("hello RSA".getBytes(StandardCharsets.UTF_8).length + 11) * 8,
+ RSA.calculateRequiredKeyLength("hello RSA"));
+ assertThrows(RuntimeException.class,
+ () -> RSA.encrypt(pair.getPublic(), "x".repeat(200)));
+ }
+
+ @Test
+ void pemKeyFilesRoundTripAndExposeDescriptions() {
+ KeyPair pair = RSA.generateKeyPair(1024);
+ Path privateFile = tempDir.resolve("private.pem");
+ Path publicFile = tempDir.resolve("public.pem");
+
+ assertTrue(RSA.writePrivateKey(pair, privateFile.toString(), "PRIVATE KEY"));
+ assertTrue(RSA.writePublicKey(pair, publicFile.toString(), "PUBLIC KEY"));
+ assertEquals(pair.getPrivate(), KeyFile.read(privateFile.toString()).getKey());
+ assertEquals(pair.getPublic(), KeyFile.read(publicFile.toString()).getKey());
+
+ KeyFile privateKey = new KeyFile(pair.getPrivate());
+ KeyFile publicKey = new KeyFile(pair.getPublic(), "server key");
+ assertEquals("PRIVATE KEY", privateKey.getDescription());
+ assertEquals("server key", publicKey.getDescription());
+ publicKey.setDescription("changed");
+ assertEquals("changed", publicKey.getDescription());
+ assertEquals(pair.getPublic(), publicKey.getKey());
+ assertThrows(RuntimeException.class, () -> KeyFile.read(tempDir.resolve("missing").toString()));
+ }
+
+ @Test
+ void aesAndSshKeyFilesRoundTrip() throws Exception {
+ AESKey aesKey = new AESKey("file seed");
+ Path aesFile = tempDir.resolve("aes.key");
+ try (KeyFile keyFile = new KeyFile(aesKey)) {
+ keyFile.write(aesFile.toString());
+ }
+ assertArrayEquals(aesKey.getEncoded(), KeyFile.read(aesFile.toString()).getKey().getEncoded());
+
+ KeyPair pair = RSA.generateKeyPair(1024);
+ Path sshFile = tempDir.resolve("id_rsa.pub");
+ assertTrue(RSA.writePublicKey(pair, sshFile.toString(), "test key", true));
+ assertEquals(pair.getPublic(), KeyFile.read(sshFile.toString()).getKey());
+ String sshText = Files.readString(sshFile, StandardCharsets.UTF_8);
+ assertTrue(sshText.startsWith("ssh-rsa "));
+ assertTrue(sshText.endsWith(" test key"));
+ }
+
+ @Test
+ void dataOutputHelperProvidesBytesTextAndBase64() throws Exception {
+ try (DataOutputStreamHelper output = new DataOutputStreamHelper()) {
+ output.write("data".getBytes(StandardCharsets.UTF_8));
+ assertArrayEquals("data".getBytes(StandardCharsets.UTF_8), output.toByteArray());
+ assertEquals("data", output.toString());
+ assertEquals(Base64.getEncoder().encodeToString("data".getBytes(StandardCharsets.UTF_8)),
+ output.base64Encoded());
+ }
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ DataOutputStreamHelper output = new DataOutputStreamHelper(bytes);
+ output.writeByte(7);
+ assertFalse(output.toByteArray().length == 0);
+ output.close();
+ assertArrayEquals(new byte[0], output.toByteArray());
+ output.close();
+ }
+
+ @Test
+ void invalidSshKeyIsRejected() {
+ SSHEncodedKeySpec spec = new SSHEncodedKeySpec("invalid ssh key".getBytes(StandardCharsets.UTF_8));
+ assertEquals(null, spec.getFormat());
+ assertThrows(InvalidKeySpecException.class, spec::convertToRSAPubKeySpec);
+ }
+}