diff --git a/Readme.md b/Readme.md
index abc6269a..39035536 100644
--- a/Readme.md
+++ b/Readme.md
@@ -64,6 +64,7 @@ As I explained in a [blog post](http://www.matez.de/index.php/2020/06/22/the-fut
* This library is a Multi-Release-jar, which means that you can only use certain features when a more recent Java version is used.
* In order to use ssh-ed25519 & ssh-ed448, you must use at least Java 15.
* In order to use curve25519-sha256, curve448-sha512 & chacha20-poly1305@openssh.com, you must use at least Java 11.
+ * As of the [0.1.66](https://github.com/mwiede/jsch/releases/tag/jsch-0.1.66) release, these algorithms can now be used with older Java releases if [Bouncy Castle](https://www.bouncycastle.org/) (bcprov-jdk15on) is added to the classpath.
## Changes since fork:
* [0.1.66](https://github.com/mwiede/jsch/releases/tag/jsch-0.1.66)
@@ -99,6 +100,13 @@ As I explained in a [blog post](http://www.matez.de/index.php/2020/06/22/the-fut
* See `examples/JSchWithAgentProxy.java` for simple example
* ssh-agent support requires either [Java 16's JEP 380](https://openjdk.java.net/jeps/380) or the addition of [junixsocket](https://github.com/kohlschutter/junixsocket) to classpath
* Pageant support is untested & requires the addition of [JNA](https://github.com/java-native-access/jna) to classpath
+ * Added support for the following algorithms with older Java releases by using [Bouncy Castle](https://www.bouncycastle.org/):
+ * ssh-ed25519
+ * ssh-ed448
+ * curve25519-sha256
+ * curve25519-sha256@libssh.org
+ * curve448-sha512
+ * chacha20-poly1305@openssh.com
* [0.1.65](https://github.com/mwiede/jsch/releases/tag/jsch-0.1.65)
* Added system properties to allow manipulation of various crypto algorithms used by default
* Integrated JZlib, allowing use of zlib@openssh.com & zlib compressions without the need to provide the JZlib jar-file
diff --git a/pom.xml b/pom.xml
index 19812b80..4f197228 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,12 @@
5.9.0
+
+ org.bouncycastle
+ bcprov-jdk15on
+ 1.69
+ true
+
com.kohlschutter.junixsocket
junixsocket-common
diff --git a/src/main/java/com/jcraft/jsch/DHXEC.java b/src/main/java/com/jcraft/jsch/DHXEC.java
index 4bbeeeac..b1100207 100644
--- a/src/main/java/com/jcraft/jsch/DHXEC.java
+++ b/src/main/java/com/jcraft/jsch/DHXEC.java
@@ -89,7 +89,7 @@ public void init(Session session,
Q_C = xdh.getQ();
buf.putString(Q_C);
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
throw new JSchException(e.toString(), e);
}
diff --git a/src/main/java/com/jcraft/jsch/JSch.java b/src/main/java/com/jcraft/jsch/JSch.java
index 6a83e70d..e6f6569e 100644
--- a/src/main/java/com/jcraft/jsch/JSch.java
+++ b/src/main/java/com/jcraft/jsch/JSch.java
@@ -106,13 +106,9 @@ public class JSch{
config.put("ecdh-sha2-nistp", "com.jcraft.jsch.jce.ECDHN");
- config.put("ssh-ed25519", "com.jcraft.jsch.jce.SignatureEd25519");
- config.put("ssh-ed448", "com.jcraft.jsch.jce.SignatureEd448");
-
config.put("curve25519-sha256", "com.jcraft.jsch.DH25519");
config.put("curve25519-sha256@libssh.org", "com.jcraft.jsch.DH25519");
config.put("curve448-sha512", "com.jcraft.jsch.DH448");
- config.put("xdh", "com.jcraft.jsch.jce.XDH");
config.put("dh", "com.jcraft.jsch.jce.DH");
config.put("3des-cbc", "com.jcraft.jsch.jce.TripleDESCBC");
@@ -156,12 +152,10 @@ public class JSch{
config.put("keypairgen.dsa", "com.jcraft.jsch.jce.KeyPairGenDSA");
config.put("keypairgen.rsa", "com.jcraft.jsch.jce.KeyPairGenRSA");
config.put("keypairgen.ecdsa", "com.jcraft.jsch.jce.KeyPairGenECDSA");
- config.put("keypairgen.eddsa", "com.jcraft.jsch.jce.KeyPairGenEdDSA");
config.put("random", "com.jcraft.jsch.jce.Random");
config.put("none", "com.jcraft.jsch.CipherNone");
- config.put("chacha20-poly1305@openssh.com", "com.jcraft.jsch.jce.ChaCha20Poly1305");
config.put("aes128-gcm@openssh.com", "com.jcraft.jsch.jce.AES128GCM");
config.put("aes256-gcm@openssh.com", "com.jcraft.jsch.jce.AES256GCM");
@@ -189,6 +183,26 @@ public class JSch{
config.put("pbkdf", "com.jcraft.jsch.jce.PBKDF");
+ if(JavaVersion.getVersion()>=11){
+ config.put("chacha20-poly1305@openssh.com", "com.jcraft.jsch.jce.ChaCha20Poly1305");
+ config.put("xdh", "com.jcraft.jsch.jce.XDH");
+ }
+ else{
+ config.put("chacha20-poly1305@openssh.com", "com.jcraft.jsch.bc.ChaCha20Poly1305");
+ config.put("xdh", "com.jcraft.jsch.bc.XDH");
+ }
+
+ if(JavaVersion.getVersion()>=15){
+ config.put("keypairgen.eddsa", "com.jcraft.jsch.jce.KeyPairGenEdDSA");
+ config.put("ssh-ed25519", "com.jcraft.jsch.jce.SignatureEd25519");
+ config.put("ssh-ed448", "com.jcraft.jsch.jce.SignatureEd448");
+ }
+ else{
+ config.put("keypairgen.eddsa", "com.jcraft.jsch.bc.KeyPairGenEdDSA");
+ config.put("ssh-ed25519", "com.jcraft.jsch.bc.SignatureEd25519");
+ config.put("ssh-ed448", "com.jcraft.jsch.bc.SignatureEd448");
+ }
+
config.put("StrictHostKeyChecking", "ask");
config.put("HashKnownHosts", "no");
diff --git a/src/main/java/com/jcraft/jsch/JavaVersion.java b/src/main/java/com/jcraft/jsch/JavaVersion.java
new file mode 100644
index 00000000..b6184b14
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/JavaVersion.java
@@ -0,0 +1,8 @@
+package com.jcraft.jsch;
+
+final class JavaVersion {
+
+ static int getVersion() {
+ return 8;
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/KeyExchange.java b/src/main/java/com/jcraft/jsch/KeyExchange.java
index cd2e86f9..cb611909 100644
--- a/src/main/java/com/jcraft/jsch/KeyExchange.java
+++ b/src/main/java/com/jcraft/jsch/KeyExchange.java
@@ -143,18 +143,25 @@ else if(guess[i]==null){
}
}
- Class> _s2cclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_STOC]));
- Cipher _s2ccipher=(Cipher)(_s2cclazz.getDeclaredConstructor().newInstance());
- boolean _s2cAEAD=_s2ccipher.isAEAD();
- if(_s2cAEAD){
- guess[PROPOSAL_MAC_ALGS_STOC]=null;
- }
+ boolean _s2cAEAD=false;
+ boolean _c2sAEAD=false;
+ try{
+ Class> _s2cclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_STOC]));
+ Cipher _s2ccipher=(Cipher)(_s2cclazz.getDeclaredConstructor().newInstance());
+ _s2cAEAD=_s2ccipher.isAEAD();
+ if(_s2cAEAD){
+ guess[PROPOSAL_MAC_ALGS_STOC]=null;
+ }
- Class> _c2sclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_CTOS]));
- Cipher _c2scipher=(Cipher)(_c2sclazz.getDeclaredConstructor().newInstance());
- boolean _c2sAEAD=_c2scipher.isAEAD();
- if(_c2sAEAD){
- guess[PROPOSAL_MAC_ALGS_CTOS]=null;
+ Class> _c2sclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_CTOS]));
+ Cipher _c2scipher=(Cipher)(_c2sclazz.getDeclaredConstructor().newInstance());
+ _c2sAEAD=_c2scipher.isAEAD();
+ if(_c2sAEAD){
+ guess[PROPOSAL_MAC_ALGS_CTOS]=null;
+ }
+ }
+ catch(Exception | NoClassDefFoundError e){
+ throw new JSchException(e.toString(), e);
}
if(JSch.getLogger().isEnabled(Logger.INFO)){
@@ -361,7 +368,7 @@ else if(alg.equals("ssh-ed25519") ||
sig=(SignatureEdDSA)(c.getDeclaredConstructor().newInstance());
sig.init();
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
System.err.println(e);
}
diff --git a/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java b/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java
index 585a5be5..92d6a43f 100644
--- a/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java
+++ b/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java
@@ -57,7 +57,7 @@ void generate(int key_size) throws JSchException{
keypairgen=null;
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
//System.err.println("KeyPairEdDSA: "+e);
throw new JSchException(e.toString(), e);
}
@@ -134,7 +134,7 @@ public byte[] getSignature(byte[] data, String alg){
tmp[1] = sig;
return Buffer.fromBytes(tmp).buffer;
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
}
return null;
}
@@ -160,7 +160,7 @@ public Signature getVerifier(String alg){
eddsa.setPubKey(pub_array);
return eddsa;
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
}
return null;
}
diff --git a/src/main/java/com/jcraft/jsch/Session.java b/src/main/java/com/jcraft/jsch/Session.java
index 69e1bf34..80d807a7 100644
--- a/src/main/java/com/jcraft/jsch/Session.java
+++ b/src/main/java/com/jcraft/jsch/Session.java
@@ -610,7 +610,7 @@ private KeyExchange receive_kexinit(Buffer buf) throws Exception {
Class> c=Class.forName(getConfig(guess[KeyExchange.PROPOSAL_KEX_ALGS]));
kex=(KeyExchange)(c.getDeclaredConstructor().newInstance());
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
throw new JSchException(e.toString(), e);
}
@@ -1528,7 +1528,7 @@ private void updateKeys(KeyExchange kex) throws Exception{
method=guess[KeyExchange.PROPOSAL_COMP_ALGS_STOC];
initInflater(method);
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
if(e instanceof JSchException)
throw e;
throw new JSchException(e.toString(), e);
@@ -2574,9 +2574,6 @@ private void initDeflater(String method) throws JSchException{
catch(Exception ee){ }
deflater.init(Compression.DEFLATER, level);
}
- catch(NoClassDefFoundError ee){
- throw new JSchException(ee.toString(), ee);
- }
catch(Exception ee){
throw new JSchException(ee.toString(), ee);
//System.err.println(foo+" isn't accessible.");
@@ -2855,7 +2852,7 @@ static boolean checkCipher(String cipher){
new byte[_c.getIVSize()]);
return true;
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
return false;
}
}
@@ -2904,7 +2901,7 @@ static boolean checkMac(String mac){
_c.init(new byte[_c.getBlockSize()]);
return true;
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
return false;
}
}
@@ -2947,7 +2944,7 @@ static boolean checkKex(Session s, String kex){
_c.init(s ,null, null, null, null);
return true;
}
- catch(Exception e){ return false; }
+ catch(Exception | NoClassDefFoundError e){ return false; }
}
private String[] checkSignatures(String sigs){
@@ -2967,7 +2964,7 @@ private String[] checkSignatures(String sigs){
final Signature sig=(Signature)(c.getDeclaredConstructor().newInstance());
sig.init();
}
- catch(Exception e){
+ catch(Exception | NoClassDefFoundError e){
result.addElement(_sigs[i]);
}
}
diff --git a/src/main/java/com/jcraft/jsch/bc/ChaCha20Poly1305.java b/src/main/java/com/jcraft/jsch/bc/ChaCha20Poly1305.java
new file mode 100644
index 00000000..8ab84dc9
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/bc/ChaCha20Poly1305.java
@@ -0,0 +1,138 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2008-2018 ymnk, JCraft,Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the distribution.
+
+ 3. The names of the authors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package com.jcraft.jsch.bc;
+
+import com.jcraft.jsch.Cipher;
+import com.jcraft.jsch.openjax.Poly1305;
+import java.nio.ByteBuffer;
+import javax.crypto.AEADBadTagException;
+import org.bouncycastle.crypto.engines.ChaChaEngine;
+import org.bouncycastle.crypto.params.*;
+
+public class ChaCha20Poly1305 implements Cipher{
+ //Actually the block size, not IV size
+ private static final int ivsize=8;
+ //Actually the key size, not block size
+ private static final int bsize=64;
+ private static final int tagsize=16;
+ private ChaChaEngine header_cipher;
+ private ChaChaEngine main_cipher;
+ private KeyParameter K_1_spec;
+ private KeyParameter K_2_spec;
+ private int mode;
+ private Poly1305 poly1305;
+ @Override
+ public int getIVSize(){return ivsize;}
+ @Override
+ public int getBlockSize(){return bsize;}
+ @Override
+ public int getTagSize(){return tagsize;}
+ @Override
+ public void init(int mode, byte[] key, byte[] iv) throws Exception{
+ byte[] tmp;
+ if(key.length>bsize){
+ tmp=new byte[bsize];
+ System.arraycopy(key, 0, tmp, 0, tmp.length);
+ key=tmp;
+ }
+ byte[] K_1=new byte[bsize/2];
+ byte[] K_2=new byte[bsize/2];
+ System.arraycopy(key, bsize/2, K_1, 0, bsize/2);
+ System.arraycopy(key, 0, K_2, 0, bsize/2);
+ this.mode=mode;
+ try{
+ K_1_spec=new KeyParameter(K_1);
+ K_2_spec=new KeyParameter(K_2);
+ header_cipher=new ChaChaEngine();
+ main_cipher=new ChaChaEngine();
+ }
+ catch(Exception e){
+ header_cipher=null;
+ main_cipher=null;
+ K_1_spec=null;
+ K_2_spec=null;
+ throw e;
+ }
+ }
+ @Override
+ public void update(int foo) throws Exception{
+ ByteBuffer nonce=ByteBuffer.allocate(8);
+ nonce.putLong(0, foo);
+ header_cipher.init(this.mode==ENCRYPT_MODE, new ParametersWithIV(K_1_spec, nonce.array()));
+ main_cipher.init(this.mode==ENCRYPT_MODE, new ParametersWithIV(K_2_spec, nonce.array()));
+ // Trying to reinit the cipher again with same nonce results in InvalidKeyException
+ // So just read entire first 64-byte block, which should increment global counter from 0->1
+ byte[] poly_key = new byte[32];
+ byte[] discard = new byte[32];
+ main_cipher.processBytes(poly_key, 0, 32, poly_key, 0);
+ main_cipher.processBytes(discard, 0, 32, discard, 0);
+ poly1305 = new Poly1305(poly_key);
+ }
+ @Override
+ public void update(byte[] foo, int s1, int len, byte[] bar, int s2) throws Exception{
+ header_cipher.processBytes(foo, s1, len, bar, s2);
+ }
+ @Override
+ public void updateAAD(byte[] foo, int s1, int len) throws Exception{
+ }
+ @Override
+ public void doFinal(byte[] foo, int s1, int len, byte[] bar, int s2) throws Exception{
+ if(this.mode==DECRYPT_MODE){
+ byte[] actual_tag = new byte[tagsize];
+ System.arraycopy(foo, len, actual_tag, 0, tagsize);
+ byte[] expected_tag = new byte[tagsize];
+ poly1305.update(foo, s1, len).finish(expected_tag, 0);
+ if(!arraysequals(actual_tag, expected_tag)){
+ throw new AEADBadTagException("Tag mismatch");
+ }
+ }
+
+ main_cipher.processBytes(foo, s1+4, len-4, bar, s2+4);
+
+ if(this.mode==ENCRYPT_MODE){
+ poly1305.update(bar, s2, len).finish(bar, len);
+ }
+ }
+ @Override
+ public boolean isCBC(){return false; }
+ @Override
+ public boolean isAEAD(){return true; }
+ @Override
+ public boolean isChaCha20(){return true; }
+
+ private static boolean arraysequals(byte[] a, byte[] b){
+ if(a.length!=b.length) return false;
+ int res=0;
+ for(int i=0; iclient cipher: %s.*", cipher);
+ String expectedC2S = String.format("kex: client->server cipher: %s.*", cipher);
+ checkLogs(expectedS2C);
+ checkLogs(expectedC2S);
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ "chacha20-poly1305@openssh.com,none",
+ "chacha20-poly1305@openssh.com,zlib@openssh.com"
+ })
+ public void testBCCiphers(String cipher, String compression) throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("chacha20-poly1305@openssh.com", "com.jcraft.jsch.bc.ChaCha20Poly1305");
session.setConfig("cipher.s2c", cipher);
session.setConfig("cipher.c2s", cipher);
session.setConfig("compression.s2c", compression);