Add support for public-key based authentication

This commit is contained in:
2025-02-24 19:57:07 -06:00
parent b984959aa7
commit 21c4434831
9 changed files with 219 additions and 12 deletions

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -34,5 +34,6 @@ public enum SFTPBackendVariantSetting implements BackendVariantSetting
PASSWORD,
HOSTNAME,
PORT,
BASE_PATH
BASE_PATH,
PRIVATE_KEY
}

View File

@ -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);
}

View File

@ -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());
}
}
}

View 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.

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
sNTITZbKrQB4hwHdawpMHxh+5mRXLk0=
-----END PRIVATE KEY-----

View File

@ -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