mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-17 20:50:44 +00:00
Add support for public-key based authentication
This commit is contained in:
@ -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<SFTPDirEntr
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AbstractSFTPAction.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** singleton implementing Initialization-on-Demand Holder idiom
|
||||
** to help ensure only a single SshClient object exists in a server.
|
||||
@ -91,8 +99,6 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// open clientSessionFirst, then sftpClient //
|
||||
// and close them in reverse (sftpClient, then clientSession) //
|
||||
@ -120,10 +126,11 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
|
||||
{
|
||||
SFTPBackendMetaData sftpBackendMetaData = getBackendMetaData(SFTPBackendMetaData.class, backendMetaData);
|
||||
|
||||
String username = sftpBackendMetaData.getUsername();
|
||||
String password = sftpBackendMetaData.getPassword();
|
||||
String hostName = sftpBackendMetaData.getHostName();
|
||||
Integer port = sftpBackendMetaData.getPort();
|
||||
String username = sftpBackendMetaData.getUsername();
|
||||
String password = sftpBackendMetaData.getPassword();
|
||||
String hostName = sftpBackendMetaData.getHostName();
|
||||
Integer port = sftpBackendMetaData.getPort();
|
||||
byte[] privateKey = sftpBackendMetaData.getPrivateKey();
|
||||
|
||||
if(backendMetaData.getUsesVariants())
|
||||
{
|
||||
@ -144,6 +151,11 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
|
||||
password = variantRecord.getValueString(fieldNameMap.get(SFTPBackendVariantSetting.PASSWORD));
|
||||
}
|
||||
|
||||
if(fieldNameMap.containsKey(SFTPBackendVariantSetting.PRIVATE_KEY))
|
||||
{
|
||||
privateKey = variantRecord.getValueByteArray(fieldNameMap.get(SFTPBackendVariantSetting.PRIVATE_KEY));
|
||||
}
|
||||
|
||||
if(fieldNameMap.containsKey(SFTPBackendVariantSetting.HOSTNAME))
|
||||
{
|
||||
hostName = variantRecord.getValueString(fieldNameMap.get(SFTPBackendVariantSetting.HOSTNAME));
|
||||
@ -155,9 +167,9 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
|
||||
}
|
||||
}
|
||||
|
||||
makeConnection(username, hostName, port, password);
|
||||
makeConnection(username, hostName, port, password, privateKey);
|
||||
}
|
||||
catch(IOException e)
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error setting up SFTP connection", e));
|
||||
}
|
||||
@ -195,10 +207,30 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction<SFTPDirEntr
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected SftpClient makeConnection(String username, String hostName, Integer port, String password) throws IOException
|
||||
protected SftpClient makeConnection(String username, String hostName, Integer port, String password, byte[] privateKeyBytes) throws Exception
|
||||
{
|
||||
this.clientSession = SshClientManager.getInstance().connect(username, hostName, port).verify().getSession();
|
||||
clientSession.addPasswordIdentity(password);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// if we have private key bytes, use them to add publicKey identity //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
if(privateKeyBytes != null && privateKeyBytes.length > 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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,5 +34,6 @@ public enum SFTPBackendVariantSetting implements BackendVariantSetting
|
||||
PASSWORD,
|
||||
HOSTNAME,
|
||||
PORT,
|
||||
BASE_PATH
|
||||
BASE_PATH,
|
||||
PRIVATE_KEY
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
11
qqq-backend-module-filesystem/src/test/resources/README.md
Normal file
11
qqq-backend-module-filesystem/src/test/resources/README.md
Normal file
@ -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.
|
@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
sNTITZbKrQB4hwHdawpMHxh+5mRXLk0=
|
||||
-----END PRIVATE KEY-----
|
@ -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
|
Reference in New Issue
Block a user