diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java index 1ef137af..64a77632 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java @@ -26,6 +26,11 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -48,6 +53,7 @@ import com.kingsrook.qqq.backend.module.filesystem.sftp.model.metadata.SFTPBacke import com.kingsrook.qqq.backend.module.filesystem.sftp.model.metadata.SFTPBackendVariantSetting; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.sftp.client.SftpClient; import org.apache.sshd.sftp.client.SftpClientFactory; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -60,6 +66,8 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction 0) + { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + PublicKey publicKey = KeyUtils.recoverPublicKey(privateKey); + clientSession.addPublicKeyIdentity(new KeyPair(publicKey, privateKey)); + } + + ////////////////////////////////////////////////// + // if we have a password, add password identity // + ////////////////////////////////////////////////// + if(StringUtils.hasContent(password)) + { + clientSession.addPasswordIdentity(password); + } + clientSession.auth().verify(); this.sftpClient = SftpClientFactory.instance().createSftpClient(clientSession); diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionAction.java index 53b93ccf..d31fbcbe 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionAction.java @@ -37,7 +37,7 @@ public class SFTPTestConnectionAction extends AbstractSFTPAction ***************************************************************************/ public SFTPTestConnectionTestOutput testConnection(SFTPTestConnectionTestInput input) { - try(SftpClient sftpClient = super.makeConnection(input.getUsername(), input.getHostName(), input.getPort(), input.getPassword())) + try(SftpClient sftpClient = super.makeConnection(input.getUsername(), input.getHostName(), input.getPort(), input.getPassword(), input.getPrivateKey())) { SFTPTestConnectionTestOutput output = new SFTPTestConnectionTestOutput().withIsConnectionSuccess(true); @@ -80,6 +80,7 @@ public class SFTPTestConnectionAction extends AbstractSFTPAction private Integer port; private String password; private String basePath; + private byte[] privateKey; @@ -251,6 +252,39 @@ public class SFTPTestConnectionAction extends AbstractSFTPAction return (this); } + + + /******************************************************************************* + ** Getter for privateKey + ** + *******************************************************************************/ + public byte[] getPrivateKey() + { + return privateKey; + } + + + + /******************************************************************************* + ** Setter for privateKey + ** + *******************************************************************************/ + public void setPrivateKey(byte[] privateKey) + { + this.privateKey = privateKey; + } + + + + /******************************************************************************* + ** Fluent setter for privateKey + ** + *******************************************************************************/ + public SFTPTestConnectionTestInput withPrivateKey(byte[] privateKey) + { + this.privateKey = privateKey; + return (this); + } } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendMetaData.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendMetaData.java index c423f99e..28ed667f 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendMetaData.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendMetaData.java @@ -34,6 +34,7 @@ public class SFTPBackendMetaData extends AbstractFilesystemBackendMetaData private String username; private String password; private String hostName; + private byte[] privateKey; private Integer port; @@ -195,4 +196,35 @@ public class SFTPBackendMetaData extends AbstractFilesystemBackendMetaData return (this); } + + + /******************************************************************************* + ** Getter for privateKey + *******************************************************************************/ + public byte[] getPrivateKey() + { + return (this.privateKey); + } + + + + /******************************************************************************* + ** Setter for privateKey + *******************************************************************************/ + public void setPrivateKey(byte[] privateKey) + { + this.privateKey = privateKey; + } + + + + /******************************************************************************* + ** Fluent setter for privateKey + *******************************************************************************/ + public SFTPBackendMetaData withPrivateKey(byte[] privateKey) + { + this.privateKey = privateKey; + return (this); + } + } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendVariantSetting.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendVariantSetting.java index b5205cb9..2a702fc8 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendVariantSetting.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/model/metadata/SFTPBackendVariantSetting.java @@ -34,5 +34,6 @@ public enum SFTPBackendVariantSetting implements BackendVariantSetting PASSWORD, HOSTNAME, PORT, - BASE_PATH + BASE_PATH, + PRIVATE_KEY } diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/BaseSFTPTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/BaseSFTPTest.java index 6c71ea42..e7093402 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/BaseSFTPTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/BaseSFTPTest.java @@ -67,6 +67,17 @@ public class BaseSFTPTest extends BaseTest grantUploadFilesDirWritePermission(); + /////////////////////////////////////////////////// + // add our test-only public key to the container // + /////////////////////////////////////////////////// + String sshDir = "/home/" + USERNAME + "/.ssh"; + sftpContainer.execInContainer("mkdir", sshDir); + sftpContainer.execInContainer("chmod", "700", sshDir); + sftpContainer.execInContainer("chown", USERNAME, sshDir); + copyFileToContainer("test-only-key.pub", sshDir + "/authorized_keys"); + sftpContainer.execInContainer("chmod", "600", sshDir + "/authorized_keys"); + sftpContainer.execInContainer("chown", USERNAME, sshDir + "/authorized_keys"); + currentPort = sftpContainer.getMappedPort(22); } diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionActionTest.java index b1439cbf..5b023735 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/SFTPTestConnectionActionTest.java @@ -22,7 +22,12 @@ package com.kingsrook.qqq.backend.module.filesystem.sftp.actions; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.stream.Collectors; import com.kingsrook.qqq.backend.module.filesystem.sftp.BaseSFTPTest; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -157,6 +162,7 @@ class SFTPTestConnectionActionTest extends BaseSFTPTest } + /******************************************************************************* ** *******************************************************************************/ @@ -175,4 +181,31 @@ class SFTPTestConnectionActionTest extends BaseSFTPTest assertNull(output.getListBasePathErrorMessage()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConnectViaPublicKey() throws Exception + { + try(InputStream resourceAsStream = getClass().getResourceAsStream("/test-only-key")) + { + String pem = IOUtils.readLines(resourceAsStream, StandardCharsets.UTF_8).stream() + .filter(s -> !s.startsWith("----")) + .collect(Collectors.joining("")); + + byte[] privateKeyBytes = Base64.getDecoder().decode(pem); + + SFTPTestConnectionAction.SFTPTestConnectionTestInput input = new SFTPTestConnectionAction.SFTPTestConnectionTestInput() + .withUsername(BaseSFTPTest.USERNAME) + .withPrivateKey(privateKeyBytes) + .withPort(BaseSFTPTest.getCurrentPort()) + .withHostName(BaseSFTPTest.HOST_NAME); + SFTPTestConnectionAction.SFTPTestConnectionTestOutput output = new SFTPTestConnectionAction().testConnection(input); + assertTrue(output.getIsConnectionSuccess()); + assertNull(output.getConnectionErrorMessage()); + } + } + } \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/resources/README.md b/qqq-backend-module-filesystem/src/test/resources/README.md new file mode 100644 index 00000000..abaabf7a --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/resources/README.md @@ -0,0 +1,11 @@ +The `test-only-key` / `test-only-key.pub` key pair in this directory was generated via: + +```shell +ssh-keygen -t rsa -b 4096 -m PEM -f test-only-key +openssl pkcs8 -topk8 -inform PEM -in test-only-key -outform PEM -nocrypt -out test-only-key-kpcs8.pem +cp test-only-key-kpcs8.pem .../src/test/resources/test-only-key +``` + +It is NOT meant to be used as a secure key in ANY environment. + +It is included in this repo ONLY to be used for basic unit testing. \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/resources/test-only-key b/qqq-backend-module-filesystem/src/test/resources/test-only-key new file mode 100644 index 00000000..c3ca2a4d --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/resources/test-only-key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +sNTITZbKrQB4hwHdawpMHxh+5mRXLk0= +-----END PRIVATE KEY----- diff --git a/qqq-backend-module-filesystem/src/test/resources/test-only-key.pub b/qqq-backend-module-filesystem/src/test/resources/test-only-key.pub new file mode 100644 index 00000000..02a25a74 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/resources/test-only-key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGEYxHZMPM6A+4OtayotUrB94AqYow1PcK5HzY8psKuHYejurugwFBbrFHq+dOso6EkLKcSpjvEX+XSe+m3CDIC6qVuLBS6fmWe6wo73fhlqyEP6rslz//v5+Di8fhCPSfa8y4X5RLbkDRyrL/ilmIEHUcmCU82SFFp9C10W+Ir9SZbjAoiRHT+X/WWmXYnMrfLHl38xxdWr8OqXEmLaVxmSaLnWAmMXRr4Nqrk7r7VVq899CZSJMVLSfqeiJPqyLkwOPcYjzOKft0hYAhIXCUsEK4Wv7R1t9SooC7nqE2mY/htBcrPPyDhTT4Kl0zAD2MNloNUUQsTnict3FCUy0pB9zr26bZ4YHBIvdReTWcx6IlH2U28V9ktxWRJe/ESqay4RTfif24aWyaWzD9+yTowTdPJbMaQZI5XGYJBfFtXr8aC8Di+VuILbriiuLk1iPuj1p2R3JXepONsVRDBm10SmHdETcsZXB3glqqiV+mFGmHguPXSo1WZmP79+kj/phwtn2wFKRLwi6kQNeqOxLbamRtj7y4M2sphEC7Cih1w4J+CzjZE8PC7+Bck+ldvmdtW6+wkkhLgli9SvDwe2pM8eTWTznzs1Qj9My5p1v0uvIo0xADtpITzb3kQtl9+2C7uqscjJLIpc0gVM5q55bFgxUp32G+HKUT8TI98ZrbEw== test-only-key