From Legacy FTP to Secure SFTP: Building a Reusable Java Library with Factory Pattern
Migrating from legacy FTP to SFTP is a crucial step in modernizing Java applications and ensuring data security. SFTP (SSH File Transfer Protocol) encrypts all data and credentials, protecting sensitive information and meeting compliance requirements. In this post, we’ll build a reusable, internal SFTP client library using the open-source JSch library and the Factory Pattern-now enhanced to support custom SSH algorithms (MACs, key exchange, ciphers), timeouts, and custom ports for maximum compatibility and flexibility. Why Use an Internal, Configurable SFTP Library? Consistent security practices across teams Reduced code duplication Centralized maintenance and upgrades Easy adaptation to diverse server policies No need to publish to a public Maven repository Library Name SecureSftpClient Project Structure secure-sftp-client/ ├── src/main/java/com/yourcompany/sftp/ │ ├── SftpConfig.java │ ├── SftpClient.java │ └── SftpClientFactory.java └── pom.xml Step 1: Enhanced SftpConfig.java package com.yourcompany.sftp; public class SftpConfig { private String host; private int port = 22; // Default SFTP port private String username; private String password; private int timeout = 0; // Timeout in seconds (0 = no timeout) // SSH Algorithm configuration private String kexAlgorithms; // Key exchange algorithms private String ciphersC2S; // Client-to-server ciphers private String ciphersS2C; // Server-to-client ciphers private String macsC2S; // Client-to-server MACs private String macsS2C; // Server-to-client MACs // Getters and setters public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public String getKexAlgorithms() { return kexAlgorithms; } public void setKexAlgorithms(String kexAlgorithms) { this.kexAlgorithms = kexAlgorithms; } public String getCiphersC2S() { return ciphersC2S; } public void setCiphersC2S(String ciphersC2S) { this.ciphersC2S = ciphersC2S; } public String getCiphersS2C() { return ciphersS2C; } public void setCiphersS2C(String ciphersS2C) { this.ciphersS2C = ciphersS2C; } public String getMacsC2S() { return macsC2S; } public void setMacsC2S(String macsC2S) { this.macsC2S = macsC2S; } public String getMacsS2C() { return macsS2C; } public void setMacsS2C(String macsS2C) { this.macsS2C = macsS2C; } } Step 2: Enhanced SftpClient.java package com.yourcompany.sftp; import com.jcraft.jsch.*; import java.util.Properties; public class SftpClient { private final SftpConfig config; private Session session; private ChannelSftp channelSftp; public SftpClient(SftpConfig config) { this.config = config; } public void connect() throws JSchException { JSch jsch = new JSch(); session = jsch.getSession(config.getUsername(), config.getHost(), config.getPort()); session.setPassword(config.getPassword()); Properties properties = new Properties(); properties.put("StrictHostKeyChecking", "no"); // Apply custom algorithms if provided if (config.getKexAlgorithms() != null) { properties.put("kex", config.getKexAlgorithms()); } if (config.getCiphersC2S() != null) { properties.put("cipher.c2s", config.getCiphersC2S()); } if (config.getCiphersS2C() != null) { properties.put("cipher.s2c", config.getCiphersS2C()); } if (config.getMacsC2S() != null) { properties.put("mac.c2s", config.getMacsC2S()); } if (config.getMacsS2C() != null) { properties.put("mac.s2c", config.getMacsS2C()); } session.setConfig(properties); if (config.getTimeout() > 0) { session.setTimeout(config.getTimeout() * 1000); // Convert seconds to milliseconds } session.connect(); Channel channel = session.openChannel("sftp"); channel.connect(); channelSftp = (ChannelSftp) channel; } public void upload(String localFile, String remoteDir) throws SftpException { validateConnection(); channelSftp.cd(remoteDir); channelSftp.put(localFile, "."); } public void download(String remoteFile, String localDir) throws SftpException { validateConnection(); channelSftp.get(remot

Migrating from legacy FTP to SFTP is a crucial step in modernizing Java applications and ensuring data security. SFTP (SSH File Transfer Protocol) encrypts all data and credentials, protecting sensitive information and meeting compliance requirements. In this post, we’ll build a reusable, internal SFTP client library using the open-source JSch library and the Factory Pattern-now enhanced to support custom SSH algorithms (MACs, key exchange, ciphers), timeouts, and custom ports for maximum compatibility and flexibility.
Why Use an Internal, Configurable SFTP Library?
- Consistent security practices across teams
- Reduced code duplication
- Centralized maintenance and upgrades
- Easy adaptation to diverse server policies
- No need to publish to a public Maven repository
Library Name
SecureSftpClient
Project Structure
secure-sftp-client/
├── src/main/java/com/yourcompany/sftp/
│ ├── SftpConfig.java
│ ├── SftpClient.java
│ └── SftpClientFactory.java
└── pom.xml
Step 1: Enhanced SftpConfig.java
package com.yourcompany.sftp;
public class SftpConfig {
private String host;
private int port = 22; // Default SFTP port
private String username;
private String password;
private int timeout = 0; // Timeout in seconds (0 = no timeout)
// SSH Algorithm configuration
private String kexAlgorithms; // Key exchange algorithms
private String ciphersC2S; // Client-to-server ciphers
private String ciphersS2C; // Server-to-client ciphers
private String macsC2S; // Client-to-server MACs
private String macsS2C; // Server-to-client MACs
// Getters and setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
public String getKexAlgorithms() { return kexAlgorithms; }
public void setKexAlgorithms(String kexAlgorithms) { this.kexAlgorithms = kexAlgorithms; }
public String getCiphersC2S() { return ciphersC2S; }
public void setCiphersC2S(String ciphersC2S) { this.ciphersC2S = ciphersC2S; }
public String getCiphersS2C() { return ciphersS2C; }
public void setCiphersS2C(String ciphersS2C) { this.ciphersS2C = ciphersS2C; }
public String getMacsC2S() { return macsC2S; }
public void setMacsC2S(String macsC2S) { this.macsC2S = macsC2S; }
public String getMacsS2C() { return macsS2C; }
public void setMacsS2C(String macsS2C) { this.macsS2C = macsS2C; }
}
Step 2: Enhanced SftpClient.java
package com.yourcompany.sftp;
import com.jcraft.jsch.*;
import java.util.Properties;
public class SftpClient {
private final SftpConfig config;
private Session session;
private ChannelSftp channelSftp;
public SftpClient(SftpConfig config) {
this.config = config;
}
public void connect() throws JSchException {
JSch jsch = new JSch();
session = jsch.getSession(config.getUsername(), config.getHost(), config.getPort());
session.setPassword(config.getPassword());
Properties properties = new Properties();
properties.put("StrictHostKeyChecking", "no");
// Apply custom algorithms if provided
if (config.getKexAlgorithms() != null) {
properties.put("kex", config.getKexAlgorithms());
}
if (config.getCiphersC2S() != null) {
properties.put("cipher.c2s", config.getCiphersC2S());
}
if (config.getCiphersS2C() != null) {
properties.put("cipher.s2c", config.getCiphersS2C());
}
if (config.getMacsC2S() != null) {
properties.put("mac.c2s", config.getMacsC2S());
}
if (config.getMacsS2C() != null) {
properties.put("mac.s2c", config.getMacsS2C());
}
session.setConfig(properties);
if (config.getTimeout() > 0) {
session.setTimeout(config.getTimeout() * 1000); // Convert seconds to milliseconds
}
session.connect();
Channel channel = session.openChannel("sftp");
channel.connect();
channelSftp = (ChannelSftp) channel;
}
public void upload(String localFile, String remoteDir) throws SftpException {
validateConnection();
channelSftp.cd(remoteDir);
channelSftp.put(localFile, ".");
}
public void download(String remoteFile, String localDir) throws SftpException {
validateConnection();
channelSftp.get(remoteFile, localDir);
}
public void disconnect() {
if (channelSftp != null && channelSftp.isConnected()) {
channelSftp.exit();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
private void validateConnection() throws SftpException {
if (channelSftp == null || !channelSftp.isConnected()) {
throw new SftpException(ChannelSftp.SSH_FX_CONNECTION_LOST, "Not connected to SFTP server");
}
}
}
Step 3: SftpClientFactory.java
package com.yourcompany.sftp;
public class SftpClientFactory {
public static SftpClient create(SftpConfig config) {
validateConfig(config);
return new SftpClient(config);
}
private static void validateConfig(SftpConfig config) {
if(config.getHost() == null || config.getHost().isEmpty()) {
throw new IllegalArgumentException("Host cannot be empty");
}
if(config.getPort() 65535) {
throw new IllegalArgumentException("Invalid port number");
}
}
}
Step 4: pom.xml
Add the JSch dependency:
com.jcraft
jsch
0.1.55
Sharing the Library Internally
Since you don’t want to publish to a Maven repository, use one of these approaches:
Option 1: Share the JAR Directly
- Build the JAR:
mvn clean package
The JAR will be in target/secure-sftp-client-1.0.0.jar
.
Distribute the JAR to other teams (e.g., via a shared folder).
Add the JAR to the consuming project’s
lib/
directory.Reference the JAR in the consuming project’s
pom.xml
:
com.yourcompany.sftp
secure-sftp-client
1.0.0
system
${project.basedir}/lib/secure-sftp-client-1.0.0.jar
Option 2: Install to Local Maven Repository
- Install the library to your local Maven cache:
mvn install
-
Add the dependency in other projects’
pom.xml
:
com.yourcompany.sftp
secure-sftp-client
1.0.0
Usage Examples
1. Basic Usage with Default Port and Timeout
import com.yourcompany.sftp.*;
SftpConfig config = new SftpConfig();
config.setHost("sftp.example.com");
config.setUsername("user");
config.setPassword("secret");
config.setTimeout(30); // 30 seconds
SftpClient client = SftpClientFactory.create(config);
try {
client.connect();
client.upload("local/path/file.txt", "/remote/dir");
client.download("/remote/dir/file.txt", "local/path/");
} finally {
client.disconnect();
}
2. Custom Port and Timeout
config.setPort(2222); // Use a non-standard port
config.setTimeout(10); // 10-second timeout
3. Custom SSH Algorithms (KEX, Ciphers, MACs)
// Example: match server's supported algorithms for compatibility or compliance
config.setKexAlgorithms("diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1");
config.setCiphersC2S("aes256-ctr,aes192-ctr,aes128-ctr");
config.setCiphersS2C("aes256-ctr,aes192-ctr,aes128-ctr");
config.setMacsC2S("hmac-sha2-256,hmac-sha1");
config.setMacsS2C("hmac-sha2-256,hmac-sha1");
Why Custom Algorithm Configuration Matters
When connecting to SFTP servers, especially legacy or highly secured environments, you may encounter errors like "Algorithm negotiation fail" due to mismatched supported algorithms between the client and server[5][6]. Configuring your client to use the correct key exchange, cipher, and MAC algorithms ensures compatibility and strengthens your security posture.
“The error typically occurs when there is a mismatch in encryption or key exchange algorithms between the client (JSch) and the server. Understanding the server's algorithm preferences and configuring the JSch client to match is often the key to solving this problem.”[5]
Best Practices
- Always set a timeout to avoid hanging connections.
- Use non-standard ports where possible for security.
- Set only strong, required algorithms to comply with security policies.
- Document and version your library for internal use.
- Never hardcode credentials; use environment variables or a secret manager.
Conclusion
By building and sharing the SecureSftpClient library internally, your teams can standardize secure file transfers, reduce duplication, and adapt to any SFTP server’s requirements. With support for custom ports, timeouts, and SSH algorithms, your Java SFTP solutions will be robust, secure, and future-proof-without the need for a public Maven repository.
References:
- [Java JSch SFTP Connection Issue: Resolving Algorithm][5]
- [Setting SFTP Algorithms][6]
Citations:
[1] https://www.north-47.com/sftp-file-transfers-with-java/
[2] https://docs.ifs.com/techdocs/23r2/030_administration/030_integration/300_ifs_connect/010_configure_connect/060_transport_connectors/configure_sftp_transport_connector/
[3] https://stackoverflow.com/questions/75020056/sshj-java-library-set-cipher-suit-for-sftp-connection
[4] https://help.salesforce.com/s/articleView?id=001121369&language=fi&type=1
[5] https://www.tempmail.us.com/en/jsch/java-jsch-sftp-connection-issue-resolving-algorithm-negotiation-fail
[6] https://www.jscape.com/blog/setting-sftp-algorithms
[7] https://docs.spring.io/spring-integration/docs/6.1.5/reference/html/sftp.html
[8] https://stackoverflow.com/questions/15108923/sftp-file-transfer-using-java-jsch