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

Apr 27, 2025 - 15:20
 0
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(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

  1. Build the JAR:
   mvn clean package

The JAR will be in target/secure-sftp-client-1.0.0.jar.

  1. Distribute the JAR to other teams (e.g., via a shared folder).

  2. Add the JAR to the consuming project’s lib/ directory.

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

  1. Install the library to your local Maven cache:
   mvn install
  1. 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