Updated to jdk21, updated libraries and added unit tests
Locusworks Team/crypto/pipeline/head This commit looks good
Locusworks Team/crypto/pipeline/head This commit looks good
This commit is contained in:
1 parent
eb5035cb11
commit
00f0229906
10 files changed
+453
-245
No files matched your search
@@ -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<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[] 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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<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[] 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));
|
||||
|
||||
@@ -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<String> 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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user