From 313b11b1eacb806f28ac3b59f0c0f14d726c0e7d Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 2 Sep 2022 11:33:49 -0500 Subject: [PATCH 1/4] Update dependencies. --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f05ff65e..da0a72cd 100644 --- a/pom.xml +++ b/pom.xml @@ -165,7 +165,7 @@ ch.qos.logback logback-classic - 1.3.0-beta0 + 1.3.0 test @@ -177,7 +177,7 @@ org.scalatest scalatest_2.13 - 3.2.12 + 3.2.13 test @@ -610,7 +610,7 @@ com.google.errorprone error_prone_core - 2.13.1 + 2.15.0 From d080323da5245970cd3eed964f20d2d649b85710 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 2 Sep 2022 11:42:37 -0500 Subject: [PATCH 2/4] When connections fail due to an algorithm negotiation failure, throw a JSchAlgoNegoFailException that extends JSchException. The new JSchAlgoNegoFailException details which specific algorithm negotiation failed, along with what both JSch and the server proposed. --- ChangeLog.md | 3 + .../jsch/JSchAlgoNegoFailException.java | 69 +++++++++++ .../java/com/jcraft/jsch/KeyExchange.java | 6 +- src/main/java/com/jcraft/jsch/Session.java | 3 - .../jsch/JSchAlgoNegoFailExceptionIT.java | 112 ++++++++++++++++++ 5 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java create mode 100644 src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java diff --git a/ChangeLog.md b/ChangeLog.md index fe6bf0dd..1a56919d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,6 @@ +* [0.2.4](https://github.com/mwiede/jsch/releases/tag/jsch-0.2.4) + * When connections fail due to an algorithm negotiation failure, throw a `JSchAlgoNegoFailException` that extends `JSchException`. + * The new `JSchAlgoNegoFailException` details which specific algorithm negotiation failed, along with what both JSch and the server proposed. * [0.2.3](https://github.com/mwiede/jsch/releases/tag/jsch-0.2.3) * #188 fix private key length checks for ssh-ed25519 & ssh-ed448. by @norrisjeremy in https://github.com/mwiede/jsch/pull/189 * [0.2.2](https://github.com/mwiede/jsch/releases/tag/jsch-0.2.2) diff --git a/src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java b/src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java new file mode 100644 index 00000000..fbdbf446 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java @@ -0,0 +1,69 @@ +package com.jcraft.jsch; + +/** + * Extension of {@link JSchException} to indicate when a connection fails during algorithm + * negotiation. + */ +public class JSchAlgoNegoFailException extends JSchException { + + private static final long serialVersionUID = -1L; + + private final String algorithmName; + private final String jschProposal; + private final String serverProposal; + + JSchAlgoNegoFailException(int algorithmIndex, String jschProposal, String serverProposal) { + super(failString(algorithmIndex, jschProposal, serverProposal)); + algorithmName = algorithmNameFromIndex(algorithmIndex); + this.jschProposal = jschProposal; + this.serverProposal = serverProposal; + } + + /** Get the algorithm name. */ + public String getAlgorithmName() { + return algorithmName; + } + + /** Get the JSch algorithm proposal. */ + public String getJSchProposal() { + return jschProposal; + } + + /** Get the server algorithm proposal. */ + public String getServerProposal() { + return serverProposal; + } + + private static String failString(int algorithmIndex, String jschProposal, String serverProposal) { + return String.format( + "Algorithm negotiation fail: algorithmName=\"%s\" jschProposal=\"%s\" serverProposal=\"%s\"", + algorithmNameFromIndex(algorithmIndex), jschProposal, serverProposal); + } + + private static String algorithmNameFromIndex(int algorithmIndex) { + switch (algorithmIndex) { + case KeyExchange.PROPOSAL_KEX_ALGS: + return "kex"; + case KeyExchange.PROPOSAL_SERVER_HOST_KEY_ALGS: + return "server_host_key"; + case KeyExchange.PROPOSAL_ENC_ALGS_CTOS: + return "cipher.c2s"; + case KeyExchange.PROPOSAL_ENC_ALGS_STOC: + return "cipher.s2c"; + case KeyExchange.PROPOSAL_MAC_ALGS_CTOS: + return "mac.c2s"; + case KeyExchange.PROPOSAL_MAC_ALGS_STOC: + return "mac.s2c"; + case KeyExchange.PROPOSAL_COMP_ALGS_CTOS: + return "compression.c2s"; + case KeyExchange.PROPOSAL_COMP_ALGS_STOC: + return "compression.s2c"; + case KeyExchange.PROPOSAL_LANG_CTOS: + return "lang.c2s"; + case KeyExchange.PROPOSAL_LANG_STOC: + return "lang.s2c"; + default: + return ""; + } + } +} diff --git a/src/main/java/com/jcraft/jsch/KeyExchange.java b/src/main/java/com/jcraft/jsch/KeyExchange.java index 6e1c0eac..251b312d 100644 --- a/src/main/java/com/jcraft/jsch/KeyExchange.java +++ b/src/main/java/com/jcraft/jsch/KeyExchange.java @@ -123,13 +123,13 @@ protected static String[] guess(Session session, byte[]I_S, byte[]I_C) throws Ex loop: while(j sshd = + new GenericContainer<>( + new ImageFromDockerfile() + .withFileFromClasspath("ssh_host_rsa_key", "docker/ssh_host_rsa_key") + .withFileFromClasspath("ssh_host_rsa_key.pub", "docker/ssh_host_rsa_key.pub") + .withFileFromClasspath("ssh_host_ecdsa256_key", "docker/ssh_host_ecdsa256_key") + .withFileFromClasspath( + "ssh_host_ecdsa256_key.pub", "docker/ssh_host_ecdsa256_key.pub") + .withFileFromClasspath("ssh_host_ecdsa384_key", "docker/ssh_host_ecdsa384_key") + .withFileFromClasspath( + "ssh_host_ecdsa384_key.pub", "docker/ssh_host_ecdsa384_key.pub") + .withFileFromClasspath("ssh_host_ecdsa521_key", "docker/ssh_host_ecdsa521_key") + .withFileFromClasspath( + "ssh_host_ecdsa521_key.pub", "docker/ssh_host_ecdsa521_key.pub") + .withFileFromClasspath("ssh_host_ed25519_key", "docker/ssh_host_ed25519_key") + .withFileFromClasspath( + "ssh_host_ed25519_key.pub", "docker/ssh_host_ed25519_key.pub") + .withFileFromClasspath("ssh_host_dsa_key", "docker/ssh_host_dsa_key") + .withFileFromClasspath("ssh_host_dsa_key.pub", "docker/ssh_host_dsa_key.pub") + .withFileFromClasspath("sshd_config", "docker/sshd_config") + .withFileFromClasspath("authorized_keys", "docker/authorized_keys") + .withFileFromClasspath("Dockerfile", "docker/Dockerfile")) + .withExposedPorts(22); + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "kex|curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group18-sha512,diffie-hellman-group16-sha512,diffie-hellman-group14-sha256,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1", + "server_host_key|ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,ssh-rsa,rsa-sha2-512,rsa-sha2-256,ssh-dss", + "cipher.c2s|chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc,3des-cbc,blowfish-cbc,arcfour,arcfour256,arcfour128,rijndael-cbc@lysator.liu.se,cast128-cbc", + "cipher.s2c|chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc,3des-cbc,blowfish-cbc,arcfour,arcfour256,arcfour128,rijndael-cbc@lysator.liu.se,cast128-cbc", + "mac.c2s|hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1,hmac-sha1-96-etm@openssh.com,hmac-sha1-96,hmac-md5-etm@openssh.com,hmac-md5,hmac-md5-96-etm@openssh.com,hmac-md5-96,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-ripemd160-etm@openssh.com", + "mac.s2c|hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1,hmac-sha1-96-etm@openssh.com,hmac-sha1-96,hmac-md5-etm@openssh.com,hmac-md5,hmac-md5-96-etm@openssh.com,hmac-md5-96,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-ripemd160-etm@openssh.com", + "compression.c2s|none,zlib@openssh.com", + "compression.s2c|none,zlib@openssh.com", + "lang.c2s|''", + "lang.s2c|''" + }) + public void testJSchAlgoNegoFailException(String algorithmName, String serverProposal) + throws Exception { + String jschProposal = "foo"; + JSch ssh = createRSAIdentity(); + Session session = createSession(ssh); + session.setConfig(algorithmName, jschProposal); + session.setTimeout(timeout); + + JSchAlgoNegoFailException e = assertThrows(JSchAlgoNegoFailException.class, session::connect); + + if (algorithmName.equals("kex")) { + jschProposal += ",ext-info-c"; + } + String message = + String.format( + "Algorithm negotiation fail: algorithmName=\"%s\" jschProposal=\"%s\" serverProposal=\"%s\"", + algorithmName, jschProposal, serverProposal); + + assertEquals(message, e.getMessage()); + assertEquals(algorithmName, e.getAlgorithmName()); + assertEquals(jschProposal, e.getJSchProposal()); + assertEquals(serverProposal, e.getServerProposal()); + } + + private JSch createRSAIdentity() throws Exception { + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.addIdentity(getResourceFile("docker/id_rsa"), getResourceFile("docker/id_rsa.pub"), null); + ssh.getHostKeyRepository().add(hostKey, null); + return ssh; + } + + private HostKey readHostKey(String fileName) throws Exception { + List lines = Files.readAllLines(Paths.get(fileName), UTF_8); + String[] split = lines.get(0).split("\\s+"); + String hostname = String.format("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort()); + return new HostKey(hostname, decodeBase64(split[1])); + } + + private Session createSession(JSch ssh) throws Exception { + Session session = ssh.getSession("root", sshd.getHost(), sshd.getFirstMappedPort()); + session.setConfig("StrictHostKeyChecking", "yes"); + session.setConfig("PreferredAuthentications", "publickey"); + return session; + } + + private String getResourceFile(String fileName) { + return ResourceUtil.getResourceFile(getClass(), fileName); + } +} From 9a6fd56b5cac16823b827075ca7146280c43d08e Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 2 Sep 2022 14:07:42 -0500 Subject: [PATCH 3/4] #201 add missing JumpHosts.java, pulled from http://www.jcraft.com/jsch/examples/JumpHosts.java. --- examples/JumpHosts.java | 168 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 examples/JumpHosts.java diff --git a/examples/JumpHosts.java b/examples/JumpHosts.java new file mode 100644 index 00000000..ad7b0b62 --- /dev/null +++ b/examples/JumpHosts.java @@ -0,0 +1,168 @@ +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/** + * This program will demonstrate SSH through jump hosts. + * Suppose that you don't have direct accesses to host2 and host3. + * $ CLASSPATH=.:../build javac JumpHosts.java + * $ CLASSPATH=.:../build java JumpHosts usr1@host1 usr2@host2 usr3@host3 + * You will be asked passwords for those destinations, + * and if everything works fine, you will get file lists of your home-directory + * at host3. + * + */ +import com.jcraft.jsch.*; +import java.awt.*; +import javax.swing.*; + +public class JumpHosts { + public static void main(String[] arg){ + + try{ + JSch jsch = new JSch(); + + if(arg.length <= 1){ + System.out.println("This program expects more arguments."); + System.exit(-1); + } + + Session session = null; + Session[] sessions = new Session[arg.length]; + + String host = arg[0]; + String user = host.substring(0, host.indexOf('@')); + host = host.substring(host.indexOf('@')+1); + + sessions[0] = session = jsch.getSession(user, host, 22); + session.setUserInfo(new MyUserInfo()); + session.connect(); + System.out.println("The session has been established to "+user+"@"+host); + + for(int i = 1; i < arg.length; i++){ + host = arg[i]; + user = host.substring(0, host.indexOf('@')); + host = host.substring(host.indexOf('@')+1); + + int assinged_port = session.setPortForwardingL(0, host, 22); + System.out.println("portforwarding: "+ + "localhost:"+assinged_port+" -> "+host+":"+22); + sessions[i] = session = + jsch.getSession(user, "127.0.0.1", assinged_port); + + session.setUserInfo(new MyUserInfo()); + session.setHostKeyAlias(host); + session.connect(); + System.out.println("The session has been established to "+ + user+"@"+host); + } + + ChannelSftp sftp = (ChannelSftp)session.openChannel("sftp"); + + sftp.connect(); + sftp.ls(".", + new ChannelSftp.LsEntrySelector() { + public int select(ChannelSftp.LsEntry le) { + System.out.println(le); + return ChannelSftp.LsEntrySelector.CONTINUE; + } + }); + sftp.disconnect(); + + for(int i = sessions.length-1; i >= 0; i--){ + sessions[i].disconnect(); + } + } + catch(Exception e){ + System.out.println(e); + } + } + + public static class MyUserInfo implements UserInfo, UIKeyboardInteractive{ + public String getPassword(){ return passwd; } + public boolean promptYesNo(String str){ + Object[] options={ "yes", "no" }; + int foo=JOptionPane.showOptionDialog(null, + str, + "Warning", + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, options, options[0]); + return foo==0; + } + + String passwd; + JTextField passwordField=(JTextField)new JPasswordField(20); + + public String getPassphrase(){ return null; } + public boolean promptPassphrase(String message){ return true; } + public boolean promptPassword(String message){ + Object[] ob={passwordField}; + int result= + JOptionPane.showConfirmDialog(null, ob, message, + JOptionPane.OK_CANCEL_OPTION); + if(result==JOptionPane.OK_OPTION){ + passwd=passwordField.getText(); + return true; + } + else{ return false; } + } + public void showMessage(String message){ + JOptionPane.showMessageDialog(null, message); + } + final GridBagConstraints gbc = + new GridBagConstraints(0,0,1,1,1,1, + GridBagConstraints.NORTHWEST, + GridBagConstraints.NONE, + new Insets(0,0,0,0),0,0); + private Container panel; + public String[] promptKeyboardInteractive(String destination, + String name, + String instruction, + String[] prompt, + boolean[] echo){ + panel = new JPanel(); + panel.setLayout(new GridBagLayout()); + + gbc.weightx = 1.0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.gridx = 0; + panel.add(new JLabel(instruction), gbc); + gbc.gridy++; + + gbc.gridwidth = GridBagConstraints.RELATIVE; + + JTextField[] texts=new JTextField[prompt.length]; + for(int i=0; i Date: Fri, 2 Sep 2022 14:14:24 -0500 Subject: [PATCH 4/4] Use spaces as indentation consistently --- examples/JumpHosts.java | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/JumpHosts.java b/examples/JumpHosts.java index ad7b0b62..6f4c91ac 100644 --- a/examples/JumpHosts.java +++ b/examples/JumpHosts.java @@ -79,35 +79,35 @@ public static class MyUserInfo implements UserInfo, UIKeyboardInteractive{ public String getPassword(){ return passwd; } public boolean promptYesNo(String str){ Object[] options={ "yes", "no" }; - int foo=JOptionPane.showOptionDialog(null, + int foo=JOptionPane.showOptionDialog(null, str, - "Warning", - JOptionPane.DEFAULT_OPTION, + "Warning", + JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); return foo==0; } - + String passwd; JTextField passwordField=(JTextField)new JPasswordField(20); public String getPassphrase(){ return null; } public boolean promptPassphrase(String message){ return true; } public boolean promptPassword(String message){ - Object[] ob={passwordField}; + Object[] ob={passwordField}; int result= - JOptionPane.showConfirmDialog(null, ob, message, - JOptionPane.OK_CANCEL_OPTION); + JOptionPane.showConfirmDialog(null, ob, message, + JOptionPane.OK_CANCEL_OPTION); if(result==JOptionPane.OK_OPTION){ - passwd=passwordField.getText(); - return true; + passwd=passwordField.getText(); + return true; } else{ return false; } } public void showMessage(String message){ JOptionPane.showMessageDialog(null, message); } - final GridBagConstraints gbc = + final GridBagConstraints gbc = new GridBagConstraints(0,0,1,1,1,1, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, @@ -149,7 +149,7 @@ public String[] promptKeyboardInteractive(String destination, gbc.gridy++; } - if(JOptionPane.showConfirmDialog(null, panel, + if(JOptionPane.showConfirmDialog(null, panel, destination+": "+name, JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) @@ -158,7 +158,7 @@ public String[] promptKeyboardInteractive(String destination, for(int i=0; i