diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 8e023621..77cdc4f7 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -34,6 +34,12 @@ jobs:
check-latest: true
- name: Build with Maven
run: mvn -B -DskipTests=true package --file pom.xml
+ - uses: actions/upload-artifact@v3
+ with:
+ name: java-${{ matrix.java }}-jars
+ path: |
+ **/target/*.jar
+ if: always()
- name: Set up test JDK ${{ matrix.java }}
uses: actions/setup-java@v3
with:
@@ -49,4 +55,5 @@ jobs:
**/target/surefire-reports/
**/target/failsafe-reports/
**/target/site/jacoco/
+ **/target/site/jacoco-it/
if: always()
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index dc3affce..ca5ab4ba 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -14,5 +14,5 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
diff --git a/ChangeLog.md b/ChangeLog.md
index 43a3ffb1..245b5387 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,23 @@
+* [0.2.7](https://github.com/mwiede/jsch/releases/tag/jsch-0.2.7)
+ * Fix exception logging in Log4j2Logger.
+ * [#265](https://github.com/mwiede/jsch/issues/265) change buffer_margin computation to be dynamic based upon the MAC to allow connections that advertise small maximum packet sizes.
+ * [#266](https://github.com/mwiede/jsch/issues/266) fix PuTTY key parsing to work with unix line endings.
+ * Add support for ECDSA & EdDSA type PuTTY keys.
+ * [#71](https://github.com/mwiede/jsch/issues/71) add support for PuTTY version 3 format keys.
+ * Encrypted PuTTY version 3 format keys requires [Bouncy Castle](https://www.bouncycastle.org/java.html) (bcprov-jdk18on).
+ * Eliminate KeyPairDeferred and instead change handling of OpenSSH V1 type keys to be more like other KeyPair types.
+ * Be more vigilant about clearing private key data.
+ * Improve PKCS8 key handling and add support for PKCS5 2.1 encryption.
+ * Add support for ECDSA type PKCS8 keys.
+ * Add support for SCrypt type KDF for PKCS8 keys.
+ * PKCS8 keys using SCrypt requires [Bouncy Castle](https://www.bouncycastle.org/java.html) (bcprov-jdk18on).
+ * Add support for EdDSA type PKCS8 keys.
+ * EdDSA type PKCS8 keys requires [Bouncy Castle](https://www.bouncycastle.org/java.html) (bcprov-jdk18on).
+ * Attempt to authenticate using other signature algorithms supported by the same public key.
+ * Allow this behavior to be disabled via `try_additional_pubkey_algorithms` config option.
+ * Some servers incorrectly respond with `SSH_MSG_USERAUTH_PK_OK` to an initial auth query that they don't actually support for RSA keys.
+ * Add a new config option `enable_pubkey_auth_query` to allow skipping auth queries and proceed directly to attempting full `SSH_MSG_USERAUTH_REQUEST`'s.
+ * Add a new config option `enable_auth_none` to control whether an initial auth request for the method `none` is sent to detect all supported auth methods available on the server.
* [0.2.6](https://github.com/mwiede/jsch/releases/tag/jsch-0.2.6)
* Include host alias instead of the real host in messages and exceptions by @ShadelessFox in https://github.com/mwiede/jsch/pull/257
* Fix missing keySize set when loading V1 RSA keys by @Alex-Vol-Amz in https://github.com/mwiede/jsch/pull/258
diff --git a/pom.xml b/pom.xml
index b2a9b915..44e699c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,7 @@
true
2.6.1
5.12.1
+ 2.19.0
@@ -118,7 +119,7 @@
org.apache.logging.log4j
log4j-api
- 2.19.0
+ ${log4j.version}
true
@@ -163,33 +164,28 @@
test
- ch.qos.logback
- logback-classic
- 1.3.4
+ com.google.guava
+ guava
+ 31.1-jre
test
- org.scala-lang
- scala-library
- 2.13.6
+ org.apache.logging.log4j
+ log4j-core
+ ${log4j.version}
test
- org.scalatest
- scalatest_2.13
- 3.2.13
+ org.apache.logging.log4j
+ log4j-core
+ ${log4j.version}
+ test-jar
test
-
-
- org.scala-lang
- scala-library
-
-
- co.helmethair
- scalatest-junit-runner
- 0.2.0
+ com.github.valfirst
+ slf4j-test
+ 2.8.1
test
@@ -282,7 +278,7 @@
true
true
- -Xlint:all
+ -Xlint:all,-processing
-Werror
@@ -373,40 +369,12 @@
-
- net.alchim31.maven
- scala-maven-plugin
- 4.8.0
-
- all
- false
-
- -deprecation
- -feature
- -unchecked
- -Xfatal-warnings
-
-
-
-
-
- testCompile
-
-
-
-
org.apache.maven.plugins
maven-surefire-plugin
2.22.2
false
-
-
- java.util.logging.config.file
- ${project.basedir}/src/test/resources/logging.properties
-
-
@@ -415,12 +383,6 @@
2.22.2
false
-
-
- java.util.logging.config.file
- ${project.basedir}/src/test/resources/logging.properties
-
-
@@ -594,12 +556,24 @@
prepare-agent
+
+ default-prepare-agent-integration
+
+ prepare-agent-integration
+
+
default-report
report
+
+ default-report-integration
+
+ report-integration
+
+
@@ -630,7 +604,7 @@
com.google.errorprone
error_prone_core
- 2.15.0
+ 2.17.0
@@ -639,4 +613,4 @@
-
\ No newline at end of file
+
diff --git a/src/main/java/com/jcraft/jsch/Argon2.java b/src/main/java/com/jcraft/jsch/Argon2.java
new file mode 100644
index 00000000..be0c6876
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/Argon2.java
@@ -0,0 +1,40 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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;
+
+public interface Argon2 extends KDF{
+ public static final int ARGON2D = 0;
+ public static final int ARGON2I = 1;
+ public static final int ARGON2ID = 2;
+ public static final int V10 = 0x10;
+ public static final int V13 = 0x13;
+
+ void init(byte[] salt, int iteration, int type, byte[] additional, byte[] secret, int memory, int parallelism, int version) throws Exception;
+}
diff --git a/src/main/java/com/jcraft/jsch/BCrypt.java b/src/main/java/com/jcraft/jsch/BCrypt.java
new file mode 100644
index 00000000..e3e6e51b
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/BCrypt.java
@@ -0,0 +1,34 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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;
+
+public interface BCrypt extends KDF{
+ void init(byte[] salt, int iteration) throws Exception;
+}
diff --git a/src/main/java/com/jcraft/jsch/Buffer.java b/src/main/java/com/jcraft/jsch/Buffer.java
index 0f75f7b7..f6f4e067 100644
--- a/src/main/java/com/jcraft/jsch/Buffer.java
+++ b/src/main/java/com/jcraft/jsch/Buffer.java
@@ -212,8 +212,13 @@ byte getCommand(){
return buffer[5];
}
+ // Hardcode this since we can't use dynamic Session value
+ private static final int buffer_margin = 32 + // maximum padding length
+ 64 + // maximum mac length
+ 32; // margin for deflater; deflater may inflate data
+
void checkFreeSize(int n){
- int size = index+n+Session.buffer_margin;
+ int size = index+n+buffer_margin;
if(buffer.length0){
- int _l=l;
- if(l>_bufl-(14+dataLen)-Session.buffer_margin){
- _l=_bufl-(14+dataLen)-Session.buffer_margin;
- }
-
- if(_l<=0){
- flush();
- continue;
+ try{
+ while(l>0){
+ int _l=l;
+ int buffer_margin=getSession().getBufferMargin();
+ if(l>_bufl-(14+dataLen)-buffer_margin){
+ _l=_bufl-(14+dataLen)-buffer_margin;
+ }
+
+ if(_l<=0){
+ flush();
+ continue;
+ }
+
+ System.arraycopy(buf, s, _buf, 14+dataLen, _l);
+ dataLen+=_l;
+ s+=_l;
+ l-=_l;
}
-
- System.arraycopy(buf, s, _buf, 14+dataLen, _l);
- dataLen+=_l;
- s+=_l;
- l-=_l;
+ }
+ catch(JSchException e){
+ throw new IOException(e.toString(), e);
}
}
diff --git a/src/main/java/com/jcraft/jsch/ChannelDirectStreamLocal.java b/src/main/java/com/jcraft/jsch/ChannelDirectStreamLocal.java
index 7ace8095..0751adf0 100644
--- a/src/main/java/com/jcraft/jsch/ChannelDirectStreamLocal.java
+++ b/src/main/java/com/jcraft/jsch/ChannelDirectStreamLocal.java
@@ -47,7 +47,7 @@ protected Packet genChannelOpenPacket() {
Buffer buf = new Buffer(50 +
socketPath.length() +
- Session.buffer_margin);
+ session.getBufferMargin());
Packet packet = new Packet(buf);
packet.reset();
buf.putByte((byte) SSH_MSG_CHANNEL_OPEN);
diff --git a/src/main/java/com/jcraft/jsch/ChannelDirectTCPIP.java b/src/main/java/com/jcraft/jsch/ChannelDirectTCPIP.java
index f4c127be..5be68d8a 100644
--- a/src/main/java/com/jcraft/jsch/ChannelDirectTCPIP.java
+++ b/src/main/java/com/jcraft/jsch/ChannelDirectTCPIP.java
@@ -104,7 +104,7 @@ void run(){
i=io.in.read(buf.buffer,
14,
buf.buffer.length-14
- -Session.buffer_margin
+ -_session.getBufferMargin()
);
if(i<=0){
eof();
@@ -154,7 +154,7 @@ public void setOutputStream(OutputStream out){
protected Packet genChannelOpenPacket(){
Buffer buf = new Buffer(50 + // 6 + 4*8 + 12
host.length() + originator_IP_address.length() +
- Session.buffer_margin);
+ session.getBufferMargin());
Packet packet = new Packet(buf);
// byte SSH_MSG_CHANNEL_OPEN(90)
// string channel type //
diff --git a/src/main/java/com/jcraft/jsch/ChannelForwardedTCPIP.java b/src/main/java/com/jcraft/jsch/ChannelForwardedTCPIP.java
index 0210e059..c3115e47 100644
--- a/src/main/java/com/jcraft/jsch/ChannelForwardedTCPIP.java
+++ b/src/main/java/com/jcraft/jsch/ChannelForwardedTCPIP.java
@@ -96,14 +96,14 @@ public void run(){
Packet packet=new Packet(buf);
int i=0;
try{
- Session _session = getSession();
+ Session _session=getSession();
while(thread!=null &&
io!=null &&
io.in!=null){
i=io.in.read(buf.buffer,
14,
buf.buffer.length-14
- -Session.buffer_margin
+ -_session.getBufferMargin()
);
if(i<=0){
eof();
diff --git a/src/main/java/com/jcraft/jsch/ChannelSession.java b/src/main/java/com/jcraft/jsch/ChannelSession.java
index 7e45ead8..046b436c 100644
--- a/src/main/java/com/jcraft/jsch/ChannelSession.java
+++ b/src/main/java/com/jcraft/jsch/ChannelSession.java
@@ -241,6 +241,7 @@ void run(){
Packet packet=new Packet(buf);
int i=-1;
try{
+ Session _session=getSession();
while(isConnected() &&
thread!=null &&
io!=null &&
@@ -248,7 +249,7 @@ void run(){
i=io.in.read(buf.buffer,
14,
buf.buffer.length-14
- -Session.buffer_margin
+ -_session.getBufferMargin()
);
if(i==0)continue;
if(i==-1){
@@ -262,7 +263,7 @@ void run(){
buf.putInt(recipient);
buf.putInt(i);
buf.skip(i);
- getSession().write(packet, this, i);
+ _session.write(packet, this, i);
}
}
catch(Exception e){
diff --git a/src/main/java/com/jcraft/jsch/ChannelSftp.java b/src/main/java/com/jcraft/jsch/ChannelSftp.java
index 0157752a..5c368333 100644
--- a/src/main/java/com/jcraft/jsch/ChannelSftp.java
+++ b/src/main/java/com/jcraft/jsch/ChannelSftp.java
@@ -593,9 +593,10 @@ public void _put(InputStream src, String dst,
boolean dontcopy=true;
+ int buffer_margin=session.getBufferMargin();
if(!dontcopy){ // This case will not work anymore.
data=new byte[obuf.buffer.length
- -(5+13+21+handle.length+Session.buffer_margin
+ -(5+13+21+handle.length+buffer_margin
)
];
}
@@ -616,7 +617,7 @@ public void _put(InputStream src, String dst,
else{
data=obuf.buffer;
_s=5+13+21+handle.length;
- _datalen=obuf.buffer.length-_s-Session.buffer_margin;
+ _datalen=obuf.buffer.length-_s-buffer_margin;
}
int bulk_requests = rq.size();
@@ -664,7 +665,7 @@ public void _put(InputStream src, String dst,
foo-=sendWRITE(handle, offset, data, 0, foo);
if(data!=obuf.buffer){
data=obuf.buffer;
- _datalen=obuf.buffer.length-_s-Session.buffer_margin;
+ _datalen=obuf.buffer.length-_s-buffer_margin;
}
}
else {
@@ -2581,8 +2582,10 @@ private int sendWRITE(byte[] handle, long offset,
byte[] data, int start, int length) throws Exception{
int _length=length;
opacket.reset();
- if(obuf.buffer.length=11){
config.put("xdh", "com.jcraft.jsch.jce.XDH");
@@ -222,12 +230,16 @@ public class JSch{
config.put("ssh-ed25519", "com.jcraft.jsch.bc.SignatureEd25519");
config.put("ssh-ed448", "com.jcraft.jsch.bc.SignatureEd448");
}
+ config.put("keypairgen_fromprivate.eddsa", "com.jcraft.jsch.bc.KeyPairGenEdDSA");
config.put("StrictHostKeyChecking", "ask");
config.put("HashKnownHosts", "no");
config.put("PreferredAuthentications", Util.getSystemProperty("jsch.preferred_authentications", "gssapi-with-mic,publickey,keyboard-interactive,password"));
config.put("PubkeyAcceptedAlgorithms", Util.getSystemProperty("jsch.client_pubkey", "ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256"));
+ config.put("enable_pubkey_auth_query", Util.getSystemProperty("jsch.enable_pubkey_auth_query", "yes"));
+ config.put("try_additional_pubkey_algorithms", Util.getSystemProperty("jsch.try_additional_pubkey_algorithms", "yes"));
+ config.put("enable_auth_none", Util.getSystemProperty("jsch.enable_auth_none", "yes"));
config.put("CheckCiphers", Util.getSystemProperty("jsch.check_ciphers", "chacha20-poly1305@openssh.com"));
config.put("CheckMacs", Util.getSystemProperty("jsch.check_macs", ""));
diff --git a/src/main/java/com/jcraft/jsch/JulLogger.java b/src/main/java/com/jcraft/jsch/JulLogger.java
index 9316d2fa..2984bdf0 100644
--- a/src/main/java/com/jcraft/jsch/JulLogger.java
+++ b/src/main/java/com/jcraft/jsch/JulLogger.java
@@ -5,16 +5,9 @@
public class JulLogger implements com.jcraft.jsch.Logger {
- private static final Logger stlogger = Logger.getLogger(JSch.class.getName());
- private final Logger logger;
-
- public JulLogger() {
- this(stlogger);
- }
+ private static final Logger logger = Logger.getLogger(JSch.class.getName());
- JulLogger(Logger logger) {
- this.logger = logger;
- }
+ public JulLogger() {}
@Override
public boolean isEnabled(int level) {
diff --git a/src/main/java/com/jcraft/jsch/KDF.java b/src/main/java/com/jcraft/jsch/KDF.java
new file mode 100644
index 00000000..e9361121
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/KDF.java
@@ -0,0 +1,34 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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;
+
+public interface KDF {
+ byte[] getKey(byte[] pass, int size);
+}
diff --git a/src/main/java/com/jcraft/jsch/KeyPair.java b/src/main/java/com/jcraft/jsch/KeyPair.java
index e53640a0..6d2d5019 100644
--- a/src/main/java/com/jcraft/jsch/KeyPair.java
+++ b/src/main/java/com/jcraft/jsch/KeyPair.java
@@ -30,12 +30,15 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
package com.jcraft.jsch;
import java.io.*;
+import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Hashtable;
-import java.util.Vector;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
public abstract class KeyPair{
-
+
+ /** DEFERRED should not be be used. */
public static final int DEFERRED = -1;
public static final int ERROR=0;
public static final int DSA=1;
@@ -50,6 +53,7 @@ public abstract class KeyPair{
static final int VENDOR_PUTTY=2;
static final int VENDOR_PKCS8=3;
static final int VENDOR_OPENSSH_V1 = 4;
+ static final int VENDOR_PUTTY_V3 = 5;
int vendor=VENDOR_OPENSSH;
@@ -97,14 +101,13 @@ public void setPublicKeyComment(String publicKeyComment){
JSch jsch=null;
protected Cipher cipher;
+ private KDF kdf;
+ private HASH sha1;
private HASH hash;
private Random random;
private byte[] passphrase;
- protected String kdfName;
- protected byte[] kdfOptions;
-
public KeyPair(JSch jsch){
this.jsch=jsch;
}
@@ -168,6 +171,9 @@ public void writePrivateKey(OutputStream out, byte[] passphrase){
//out.close();
}
catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to write private key", e);
+ }
}
}
@@ -211,6 +217,9 @@ public void writePublicKey(OutputStream out, String comment){
out.write(cr);
}
catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to write public key", e);
+ }
}
}
@@ -248,6 +257,9 @@ public void writeSECSHPublicKey(OutputStream out, String comment){
out.write(Util.str2byte("---- END SSH2 PUBLIC KEY ----")); out.write(cr);
}
catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to write public key", e);
+ }
}
}
@@ -326,7 +338,9 @@ private byte[] encrypt(byte[] plain, byte[][] _iv, byte[] passphrase){
cipher.update(encoded, 0, encoded.length, encoded, 0);
}
catch(Exception e){
- //System.err.println(e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to encrypt key", e);
+ }
}
Util.bzero(key);
return encoded;
@@ -345,7 +359,9 @@ private byte[] decrypt(byte[] data, byte[] passphrase, byte[] iv){
return plain;
}
catch(Exception e){
- //System.err.println(e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to decrypt key", e);
+ }
}
return null;
}
@@ -411,7 +427,11 @@ private Random genRandom(){
Class extends Random> c=Class.forName(JSch.getConfig("random")).asSubclass(Random.class);
random=c.getDeclaredConstructor().newInstance();
}
- catch(Exception e){ System.err.println("connect: random "+e); }
+ catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to create random", e);
+ }
+ }
}
return random;
}
@@ -423,6 +443,9 @@ private HASH genHash(){
hash.init();
}
catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to create hash", e);
+ }
}
return hash;
}
@@ -432,6 +455,9 @@ private Cipher genCipher(){
cipher=c.getDeclaredConstructor().newInstance();
}
catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to create cipher", e);
+ }
}
return cipher;
}
@@ -463,6 +489,12 @@ synchronized byte[] genKey(byte[] passphrase, byte[] iv){
}
System.arraycopy(hn, 0, key, 0, key.length);
}
+ else if(vendor==VENDOR_OPENSSH_V1){
+ tmp=kdf.getKey(passphrase, cipher.getBlockSize()+cipher.getIVSize());
+ System.arraycopy(tmp, 0, key, 0, key.length);
+ System.arraycopy(tmp, key.length, iv, 0, iv.length);
+ Util.bzero(tmp);
+ }
else if(vendor==VENDOR_FSECURE){
for(int index=0; index+hsize<=hn.length;){
if(tmp!=null){ hash.update(tmp, 0, tmp.length); }
@@ -474,21 +506,32 @@ else if(vendor==VENDOR_FSECURE){
System.arraycopy(hn, 0, key, 0, key.length);
}
else if(vendor==VENDOR_PUTTY){
- Class extends HASH> c=Class.forName(JSch.getConfig("sha-1")).asSubclass(HASH.class);
- HASH sha1=c.getDeclaredConstructor().newInstance();
- tmp = new byte[4];
- key = new byte[20*2];
- for(int i = 0; i < 2; i++){
- sha1.init();
- tmp[3]=(byte)i;
- sha1.update(tmp, 0, tmp.length);
- sha1.update(passphrase, 0, passphrase.length);
- System.arraycopy(sha1.digest(), 0, key, i*20, 20);
- }
+ byte[] i=new byte[4];
+
+ sha1.update(i, 0, i.length);
+ sha1.update(passphrase, 0, passphrase.length);
+ tmp=sha1.digest();
+ System.arraycopy(tmp, 0, key, 0, tmp.length);
+ Util.bzero(tmp);
+
+ i[3]=(byte)1;
+ sha1.update(i, 0, i.length);
+ sha1.update(passphrase, 0, passphrase.length);
+ tmp=sha1.digest();
+ System.arraycopy(tmp, 0, key, tmp.length, key.length-tmp.length);
+ Util.bzero(tmp);
+ }
+ else if(vendor==VENDOR_PUTTY_V3){
+ tmp=kdf.getKey(passphrase, cipher.getBlockSize()+cipher.getIVSize()+32);
+ System.arraycopy(tmp, 0, key, 0, key.length);
+ System.arraycopy(tmp, key.length, iv, 0, iv.length);
+ Util.bzero(tmp);
}
}
catch(Exception e){
- System.err.println(e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to generate key from passphrase", e);
+ }
}
return key;
}
@@ -539,10 +582,17 @@ public boolean decrypt(byte[] _passphrase){
byte[] bar=new byte[_passphrase.length];
System.arraycopy(_passphrase, 0, bar, 0, bar.length);
_passphrase=bar;
- byte[] foo=decrypt(data, _passphrase, iv);
- Util.bzero(_passphrase);
- if(parse(foo)){
- encrypted=false;
+ byte[] foo=null;
+ try{
+ foo=decrypt(data, _passphrase, iv);
+ if(parse(foo)){
+ encrypted=false;
+ Util.bzero(data);
+ }
+ }
+ finally{
+ Util.bzero(_passphrase);
+ Util.bzero(foo);
}
return !encrypted;
}
@@ -600,8 +650,6 @@ public static KeyPair load(JSch jsch, byte[] prvkey, byte[] pubkey) throws JSchE
int vendor=VENDOR_OPENSSH;
String publicKeyComment = "";
Cipher cipher=null;
- String kdfName = null;
- byte[] kdfOptions = null;
// prvkey from "ssh-add" command on the remote.
if(pubkey==null &&
@@ -635,7 +683,7 @@ else if(_type.equals("ssh-ed448")){
kpair=KeyPairEd448.fromSSHAgent(jsch, buf);
}
else{
- throw new JSchException("privatekey: invalid key "+Util.byte2str(prvkey, 4, 7));
+ throw new JSchException("privatekey: invalid key "+_type);
}
return kpair;
}
@@ -691,11 +739,12 @@ else if(i+8 < len &&
type=UNKNOWN;
vendor=VENDOR_PKCS8;
i+=5;
-
- } else if (isOpenSSHPrivateKey(buf, i, len)) {
- type = UNKNOWN;
- vendor = VENDOR_OPENSSH_V1;
- } else {
+ }
+ else if (isOpenSSHPrivateKey(buf, i, len)){
+ type = UNKNOWN;
+ vendor = VENDOR_OPENSSH_V1;
+ }
+ else{
throw new JSchException("invalid privatekey");
}
i+=3;
@@ -750,20 +799,20 @@ else if(i+8 < len &&
}
continue;
}
- if(buf[i]==0x0d && i+14 && // FSecure
data[0]==(byte)0x3f &&
data[1]==(byte)0x6f &&
@@ -831,16 +883,14 @@ else if(i+8 < len &&
_buf.getInt(); // 0x3f6ff9be
_buf.getInt();
byte[]_type=_buf.getString();
- //System.err.println("type: "+Util.byte2str(_type));
String _cipher=Util.byte2str(_buf.getString());
- //System.err.println("cipher: "+_cipher);
if(_cipher.equals("3des-cbc")){
_buf.getInt();
byte[] foo=new byte[data.length-_buf.getOffSet()];
_buf.getByte(foo);
data=foo;
encrypted=true;
- throw new JSchException("unknown privatekey format");
+ throw new JSchException("cipher " + _cipher + " is not supported for this privatekey format");
}
else if(_cipher.equals("none")){
_buf.getInt();
@@ -852,42 +902,10 @@ else if(_cipher.equals("none")){
_buf.getByte(foo);
data=foo;
}
- }
- // OPENSSH V1 PRIVATE KEY
- else if (data != null &&
- Util.array_equals(AUTH_MAGIC, Arrays.copyOfRange(data, 0, AUTH_MAGIC.length))) {
-
- vendor = VENDOR_OPENSSH_V1;
- Buffer buffer = new Buffer(data);
- byte[] magic = new byte[AUTH_MAGIC.length];
- buffer.getByte(magic);
-
- String cipherName = Util.byte2str(buffer.getString());
- kdfName = Util.byte2str(buffer.getString()); // string kdfname
- kdfOptions = buffer.getString(); // string kdfoptions
-
- int nrKeys = buffer.getInt(); // int number of keys N; Should be 1
- if (nrKeys != 1) {
- throw new IOException("We don't support having more than 1 key in the file (yet).");
- }
-
- pubkey = buffer.getString();
-
- if ("none".equals(cipherName)) {
- encrypted = false;
- data = buffer.getString();
- type = readOpenSSHKeyv1(data);
- } else if (Session.checkCipher(JSch.getConfig(cipherName))) {
- encrypted = true;
- Class extends Cipher> c = Class.forName(JSch.getConfig(cipherName)).asSubclass(Cipher.class);
- cipher = c.getDeclaredConstructor().newInstance();
- data = buffer.getString();
- // the type can only be determined after encryption, so we take this intermediate here:
- type = DEFERRED;
- } else {
- throw new JSchException("cipher " + cipherName + " is not available");
+ else{
+ throw new JSchException("cipher " + _cipher + " is not supported for this privatekey format");
+ }
}
- }
if(pubkey!=null){
try{
@@ -898,14 +916,14 @@ else if (data != null &&
boolean valid=true;
i=0;
- do{i++;}while(buf.length>i && buf[i]!=0x0a);
+ do{i++;}while(buf.length>i && buf[i]!='\n');
if(buf.length<=i) {valid=false;}
while(valid){
- if(buf[i]==0x0a){
+ if(buf[i]=='\n'){
boolean inheader=false;
for(int j=i+1; j0 && buf[i-1]==0x0d) i--;
+ if(i>0 && buf[i-1]=='\r') i--;
if(start0 && buf[i-1]==0x0d) i--;
+ if(i>0 && buf[i-1]=='\r') i--;
if(start c = Class.forName(JSch.getConfig(cipherName)).asSubclass(Cipher.class);
+ kpair.cipher = c.getDeclaredConstructor().newInstance();
+ kpair.iv = new byte[kpair.cipher.getIVSize()];
+ } catch (Exception e) {
+ throw new JSchException("cipher " + cipherName + " is not available", e);
+ }
+ } else {
+ throw new JSchException("cipher " + cipherName + " is not available");
+ }
+ try {
+ Buffer kdfOpts = new Buffer(kdfOptions);
+ byte[] salt = kdfOpts.getString();
+ int rounds = kdfOpts.getInt();
+ Class extends BCrypt> c = Class.forName(JSch.getConfig(kdfName)).asSubclass(BCrypt.class);
+ BCrypt bcrypt = c.getDeclaredConstructor().newInstance();
+ bcrypt.init(salt, rounds);
+ kpair.kdf = bcrypt;
+ } catch (Exception e) {
+ throw new JSchException("kdf " + kdfName + " is not available", e);
+ }
+ }
+
+ return kpair;
+ } catch (Exception e) {
+ Util.bzero(kpair.data);
+ throw e;
+ }
}
private static boolean isOpenSSHPrivateKey(byte[] buf, int i, int len) {
@@ -1094,81 +1139,174 @@ public void finalize(){
dispose();
}
- private static final String[] header1 = {
- "PuTTY-User-Key-File-2: ",
- "Encryption: ",
- "Comment: ",
- "Public-Lines: "
- };
-
- private static final String[] header2 = {
- "Private-Lines: "
- };
-
- private static final String[] header3 = {
- "Private-MAC: "
- };
-
static KeyPair loadPPK(JSch jsch, byte[] buf) throws JSchException {
byte[] pubkey = null;
byte[] prvkey = null;
+ byte[] _prvkey = null;
int lines = 0;
Buffer buffer = new Buffer(buf);
- Hashtable v = new Hashtable<>();
+ Map v = new HashMap<>();
while(true){
if(!parseHeader(buffer, v))
break;
}
+ int ppkVersion;
String typ = v.get("PuTTY-User-Key-File-2");
if(typ == null){
- return null;
+ typ = v.get("PuTTY-User-Key-File-3");
+ if(typ == null){
+ return null;
+ }
+ else{
+ ppkVersion = VENDOR_PUTTY_V3;
+ }
+ }
+ else{
+ ppkVersion = VENDOR_PUTTY;
}
- lines = Integer.parseInt(v.get("Public-Lines"));
- pubkey = parseLines(buffer, lines);
+ try{
+ lines = Integer.parseInt(v.get("Public-Lines"));
+ pubkey = parseLines(buffer, lines);
- while(true){
- if(!parseHeader(buffer, v))
- break;
- }
-
- lines = Integer.parseInt(v.get("Private-Lines"));
- prvkey = parseLines(buffer, lines);
+ while(true){
+ if(!parseHeader(buffer, v))
+ break;
+ }
- while(true){
- if(!parseHeader(buffer, v))
- break;
- }
+ lines = Integer.parseInt(v.get("Private-Lines"));
+ _prvkey = parseLines(buffer, lines);
+
+ while(true){
+ if(!parseHeader(buffer, v))
+ break;
+ }
+
+ prvkey = Util.fromBase64(_prvkey, 0, _prvkey.length);
+ pubkey = Util.fromBase64(pubkey, 0, pubkey.length);
+
+ KeyPair kpair = parsePubkeyBlob(jsch, pubkey, typ);
+ kpair.encrypted = !v.get("Encryption").equals("none");
+ kpair.publickeyblob = pubkey;
+ kpair.vendor = ppkVersion;
+ kpair.publicKeyComment = v.get("Comment");
+ if(kpair.encrypted){
+ if(Session.checkCipher(JSch.getConfig("aes256-cbc"))){
+ try {
+ Class extends Cipher> c=Class.forName(JSch.getConfig("aes256-cbc")).asSubclass(Cipher.class);
+ kpair.cipher=c.getDeclaredConstructor().newInstance();
+ kpair.iv=new byte[kpair.cipher.getIVSize()];
+ }
+ catch(Exception e){
+ throw new JSchException("The cipher 'aes256-cbc' is required, but it is not available.", e);
+ }
+ }
+ else {
+ throw new JSchException("The cipher 'aes256-cbc' is required, but it is not available.");
+ }
- prvkey = Util.fromBase64(prvkey, 0, prvkey.length);
- pubkey = Util.fromBase64(pubkey, 0, pubkey.length);
+ if(ppkVersion==VENDOR_PUTTY){
+ try {
+ Class extends HASH> c=Class.forName(JSch.getConfig("sha-1")).asSubclass(HASH.class);
+ HASH sha1=c.getDeclaredConstructor().newInstance();
+ sha1.init();
+ kpair.sha1=sha1;
+ }
+ catch(Exception e){
+ throw new JSchException("'sha-1' is required, but it is not available.", e);
+ }
+ }
+ else{
+ String argonTypeStr=v.get("Key-Derivation");
+ String saltStr=v.get("Argon2-Salt");
+ if(argonTypeStr == null || saltStr == null || (saltStr != null && saltStr.length() % 2 != 0)){
+ throw new JSchException("Invalid argon2 params.");
+ }
- KeyPair kpair = null;
+ int argonType;
+ switch(argonTypeStr){
+ case "Argon2d":
+ argonType=Argon2.ARGON2D;
+ break;
+ case "Argon2i":
+ argonType=Argon2.ARGON2I;
+ break;
+ case "Argon2id":
+ argonType=Argon2.ARGON2ID;
+ break;
+ default:
+ throw new JSchException("Invalid argon2 params.");
+ }
- if(typ.equals("ssh-rsa")) {
+ try{
+ int memory=Integer.parseInt(v.get("Argon2-Memory"));
+ int passes=Integer.parseInt(v.get("Argon2-Passes"));
+ int parallelism=Integer.parseInt(v.get("Argon2-Parallelism"));
- Buffer _buf = new Buffer(pubkey);
- _buf.skip(pubkey.length);
+ byte[] salt=new byte[saltStr.length()/2];
+ for(int i=0; i c=Class.forName(JSch.getConfig("argon2")).asSubclass(Argon2.class);
+ Argon2 argon2=c.getDeclaredConstructor().newInstance();
+ argon2.init(salt, passes, argonType, new byte[0], new byte[0], memory, parallelism, Argon2.V13);
+ kpair.kdf=argon2;
+ }
+ catch(NumberFormatException e){
+ throw new JSchException("Invalid argon2 params.", e);
+ }
+ catch(Exception e){
+ throw new JSchException("'argon2' is required, but it is not available.", e);
+ }
+ }
+
+ kpair.data = prvkey;
+ }
+ else {
+ kpair.data = prvkey;
+ if(!kpair.parse(prvkey)){
+ throw new JSchException("invalid privatekey");
+ }
+ else{
+ Util.bzero(prvkey);
+ }
+ }
+ return kpair;
+ }
+ catch (Exception e){
+ Util.bzero(prvkey);
+ throw e;
+ }
+ finally{
+ Util.bzero(_prvkey);
+ }
+ }
+
+ private static KeyPair parsePubkeyBlob(JSch jsch, byte[] pubkeyblob, String typ) throws JSchException{
+ Buffer _buf = new Buffer(pubkeyblob);
+ _buf.skip(pubkeyblob.length);
+
+ String pubkeyType = Util.byte2str(_buf.getString());
+ if(typ == null || typ.equals("")){
+ typ = pubkeyType;
+ } else if (!typ.equals(pubkeyType)){
+ throw new JSchException("pubkeyblob type ["+pubkeyType+"] does not match expected type ["+typ+"]");
+ }
+
+ if(typ.equals("ssh-rsa")) {
byte[] pub_array = new byte[_buf.getInt()];
_buf.getByte(pub_array);
byte[] n_array = new byte[_buf.getInt()];
_buf.getByte(n_array);
- kpair = new KeyPairRSA(jsch, n_array, pub_array, null);
+ return new KeyPairRSA(jsch, n_array, pub_array, null);
}
else if(typ.equals("ssh-dss")){
- Buffer _buf = new Buffer(pubkey);
- _buf.skip(pubkey.length);
-
- int len = _buf.getInt();
- _buf.getByte(new byte[len]); // ssh-dss
-
byte[] p_array = new byte[_buf.getInt()];
_buf.getByte(p_array);
byte[] q_array = new byte[_buf.getInt()];
@@ -1178,39 +1316,34 @@ else if(typ.equals("ssh-dss")){
byte[] y_array = new byte[_buf.getInt()];
_buf.getByte(y_array);
- kpair = new KeyPairDSA(jsch, p_array, q_array, g_array, y_array, null);
- }
- else {
- return null;
+ return new KeyPairDSA(jsch, p_array, q_array, g_array, y_array, null);
}
+ else if(typ.equals("ecdsa-sha2-nistp256") || typ.equals("ecdsa-sha2-nistp384") || typ.equals("ecdsa-sha2-nistp521")){
+ byte[] name = _buf.getString(); // nistpXXX
- if(kpair == null)
- return null;
+ int len = _buf.getInt();
+ int x04 = _buf.getByte(); // in case of x04 it is uncompressed https://tools.ietf.org/html/rfc5480#page-7
+ byte[] r_array = new byte[(len - 1) / 2];
+ byte[] s_array = new byte[(len - 1) / 2];
+ _buf.getByte(r_array);
+ _buf.getByte(s_array);
- kpair.encrypted = !v.get("Encryption").equals("none");
- kpair.vendor = VENDOR_PUTTY;
- kpair.publicKeyComment = v.get("Comment");
- if(kpair.encrypted){
- if(Session.checkCipher(JSch.getConfig("aes256-cbc"))){
- try {
- Class extends Cipher> c=Class.forName(JSch.getConfig("aes256-cbc")).asSubclass(Cipher.class);
- kpair.cipher=c.getDeclaredConstructor().newInstance();
- kpair.iv=new byte[kpair.cipher.getIVSize()];
- }
- catch(Exception e){
- throw new JSchException("The cipher 'aes256-cbc' is required, but it is not available.");
- }
+ return new KeyPairECDSA(jsch, name, r_array, s_array, null);
+ }
+ else if(typ.equals("ssh-ed25519") || typ.equals("ssh-ed448")){
+ byte[] pub_array = new byte[_buf.getInt()];
+ _buf.getByte(pub_array);
+
+ if(typ.equals("ssh-ed25519")){
+ return new KeyPairEd25519(jsch, pub_array, null);
}
- else {
- throw new JSchException("The cipher 'aes256-cbc' is required, but it is not available.");
+ else{
+ return new KeyPairEd448(jsch, pub_array, null);
}
- kpair.data = prvkey;
}
- else {
- kpair.data = prvkey;
- kpair.parse(prvkey);
+ else{
+ throw new JSchException("key type " + typ + " is not supported");
}
- return kpair;
}
private static byte[] parseLines(Buffer buffer, int lines){
@@ -1221,22 +1354,24 @@ private static byte[] parseLines(Buffer buffer, int lines){
int i = index;
while(lines-->0){
while(buf.length > i){
- if(buf[i++] == 0x0d){
+ byte c = buf[i++];
+ if(c == '\r' || c == '\n'){
+ int len = i - index - 1;
if(data == null){
- data = new byte[i - index - 1];
- System.arraycopy(buf, index, data, 0, i - index - 1);
+ data = new byte[len];
+ System.arraycopy(buf, index, data, 0, len);
}
- else {
- byte[] tmp = new byte[data.length + i - index - 1];
+ else if(len > 0){
+ byte[] tmp = new byte[data.length + len];
System.arraycopy(data, 0, tmp, 0, data.length);
- System.arraycopy(buf, index, tmp, data.length, i - index -1);
- for(int j = 0; j < data.length; j++) data[j] = 0; // clear
+ System.arraycopy(buf, index, tmp, data.length, len);
+ Util.bzero(data); // clear
data = tmp;
}
break;
}
}
- if(buf[i]==0x0a)
+ if(i < buf.length && buf[i]=='\n')
i++;
index=i;
}
@@ -1247,13 +1382,16 @@ private static byte[] parseLines(Buffer buffer, int lines){
return data;
}
- private static boolean parseHeader(Buffer buffer, Hashtable v){
+ private static boolean parseHeader(Buffer buffer, Map v){
byte[] buf = buffer.buffer;
int index = buffer.index;
String key = null;
String value = null;
for(int i = index; i < buf.length; i++){
- if(buf[i] == 0x0d){
+ if(buf[i] == '\r' || buf[i] == '\n'){
+ if(i+1 < buf.length && buf[i+1] == '\n'){
+ i++;
+ }
break;
}
if(buf[i] == ':'){
@@ -1271,10 +1409,10 @@ private static boolean parseHeader(Buffer buffer, Hashtable v){
return false;
for(int i = index; i < buf.length; i++){
- if(buf[i] == 0x0d){
+ if(buf[i] == '\r' || buf[i] == '\n'){
value = Util.byte2str(buf, index, i - index);
i++;
- if(i < buf.length && buf[i] == 0x0a){
+ if(i < buf.length && buf[i] == '\n'){
i++;
}
index = i;
@@ -1301,7 +1439,7 @@ static class ASN1Exception extends Exception {
private static final long serialVersionUID=-1L;
}
- class ASN1 {
+ static class ASN1 {
byte[] buf;
int start;
int length;
@@ -1330,6 +1468,24 @@ boolean isOBJECT() {
boolean isOCTETSTRING() {
return getType()==(0x04&0xff);
}
+ boolean isNULL() {
+ return getType()==(0x05&0xff);
+ }
+ boolean isBITSTRING() {
+ return getType()==(0x03&0xff);
+ }
+ boolean isCONTEXTPRIMITIVE(int tag) {
+ if((tag&~0xff)!=0 || (tag&0x60)!=0){
+ throw new IllegalArgumentException();
+ }
+ return getType()==((tag|0x80)&0xff);
+ }
+ boolean isCONTEXTCONSTRUCTED(int tag) {
+ if((tag&~0xff)!=0 || (tag&0x40)!=0){
+ throw new IllegalArgumentException();
+ }
+ return getType()==((tag|0xa0)&0xff);
+ }
private int getLength(int[] indexp) {
int index=indexp[0];
int length=buf[index++]&0xff;
@@ -1358,7 +1514,7 @@ ASN1[] getContents() throws ASN1Exception {
return new ASN1[0];
}
int index=indexp[0];
- Vector values = new Vector<>();
+ List values = new ArrayList<>();
while(length>0) {
index++; length--;
int tmp=index;
@@ -1366,14 +1522,12 @@ ASN1[] getContents() throws ASN1Exception {
int l=getLength(indexp);
index=indexp[0];
length-=(index-tmp);
- values.addElement(new ASN1(buf, tmp-1, 1+(index-tmp)+l));
+ values.add(new ASN1(buf, tmp-1, 1+(index-tmp)+l));
index+=l;
length-=l;
}
ASN1[] result = new ASN1[values.size()];
- for(int i = 0; i T requireDecrypted(T obj) {
- if (obj == null)
- throw new IllegalStateException("encrypted key has not been decrypted yet.");
- return obj;
- }
-}
diff --git a/src/main/java/com/jcraft/jsch/KeyPairECDSA.java b/src/main/java/com/jcraft/jsch/KeyPairECDSA.java
index 283e520b..86a316e3 100644
--- a/src/main/java/com/jcraft/jsch/KeyPairECDSA.java
+++ b/src/main/java/com/jcraft/jsch/KeyPairECDSA.java
@@ -177,22 +177,24 @@ boolean parse(byte[] plain){
*/
return false;
}
- else if(vendor==VENDOR_PUTTY){
- /*
+ else if(vendor==VENDOR_PUTTY || vendor==VENDOR_PUTTY_V3){
Buffer buf=new Buffer(plain);
buf.skip(plain.length);
try {
byte[][] tmp = buf.getBytes(1, "");
prv_array = tmp[0];
+ key_size = prv_array.length>=64 ? 521 :
+ (prv_array.length>=48 ? 384 : 256);
}
catch(JSchException e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key", e);
+ }
return false;
}
return true;
- */
- return false;
}
// OPENSSH Key v1 Format
@@ -305,8 +307,9 @@ else if(vendor==VENDOR_PUTTY){
(prv_array.length>=48 ? 384 : 256);
}
catch(Exception e){
- //System.err.println(e);
- //e.printStackTrace();
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key", e);
+ }
return false;
}
return true;
@@ -361,7 +364,9 @@ public byte[] getSignature(byte[] data){
return Buffer.fromBytes(tmp).buffer;
}
catch(Exception e){
- //System.err.println("e "+e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to generate signature", e);
+ }
}
return null;
}
@@ -390,7 +395,9 @@ public Signature getVerifier(){
return ecdsa;
}
catch(Exception e){
- //System.err.println("e "+e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to create verifier", e);
+ }
}
return null;
}
diff --git a/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java b/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java
index 48246d40..0279ef3d 100644
--- a/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java
+++ b/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java
@@ -58,7 +58,6 @@ void generate(int key_size) throws JSchException{
keypairgen=null;
}
catch(Exception | NoClassDefFoundError e){
- //System.err.println("KeyPairEdDSA: "+e);
throw new JSchException(e.toString(), e);
}
}
@@ -74,27 +73,67 @@ void generate(int key_size) throws JSchException{
@Override
boolean parse(byte [] plain){
+ if(vendor==VENDOR_PUTTY || vendor==VENDOR_PUTTY_V3){
+ Buffer buf=new Buffer(plain);
+ buf.skip(plain.length);
- // Only OPENSSH Key v1 Format supported for EdDSA keys
- if(vendor != VENDOR_OPENSSH_V1) return false;
- try{
- // OPENSSH Key v1 Format
- final Buffer buf = new Buffer(plain);
- int checkInt1 = buf.getInt(); // uint32 checkint1
- int checkInt2 = buf.getInt(); // uint32 checkint2
- if (checkInt1 != checkInt2) {
- throw new JSchException("check failed");
+ try {
+ byte[][] tmp = buf.getBytes(1, "");
+ prv_array = tmp[0];
+ }
+ catch(JSchException e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key", e);
+ }
+ return false;
}
- String keyType = Util.byte2str(buf.getString()); // string keytype
- pub_array = buf.getString(); // public key
- // OpenSSH stores private key in first half of string and duplicate copy of public key in second half of string
- byte[] tmp = buf.getString(); // secret key (private key + public key)
- prv_array = Arrays.copyOf(tmp, getKeySize());
- publicKeyComment = Util.byte2str(buf.getString());
+
return true;
}
- catch(Exception e){
- //System.err.println(e);
+ else if(vendor==VENDOR_OPENSSH_V1){
+ try{
+ // OPENSSH Key v1 Format
+ final Buffer buf = new Buffer(plain);
+ int checkInt1 = buf.getInt(); // uint32 checkint1
+ int checkInt2 = buf.getInt(); // uint32 checkint2
+ if (checkInt1 != checkInt2) {
+ throw new JSchException("check failed");
+ }
+ String keyType = Util.byte2str(buf.getString()); // string keytype
+ pub_array = buf.getString(); // public key
+ // OpenSSH stores private key in first half of string and duplicate copy of public key in second half of string
+ byte[] tmp = buf.getString(); // secret key (private key + public key)
+ prv_array = Arrays.copyOf(tmp, getKeySize());
+ publicKeyComment = Util.byte2str(buf.getString());
+ return true;
+ }
+ catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key", e);
+ }
+ return false;
+ }
+ }
+ else if(vendor==VENDOR_PKCS8){
+ try{
+ Class extends KeyPairGenEdDSA> c=Class.forName(JSch.getConfig("keypairgen_fromprivate.eddsa")).asSubclass(KeyPairGenEdDSA.class);
+ KeyPairGenEdDSA keypairgen=c.getDeclaredConstructor().newInstance();
+ keypairgen.init(getJceName(), plain);
+ pub_array=keypairgen.getPub();
+ prv_array=keypairgen.getPrv();
+ return true;
+ }
+ catch(Exception | NoClassDefFoundError e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key", e);
+ }
+ return false;
+ }
+ }
+ else {
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key");
+ }
return false;
}
}
@@ -135,6 +174,9 @@ public byte[] getSignature(byte[] data, String alg){
return Buffer.fromBytes(tmp).buffer;
}
catch(Exception | NoClassDefFoundError e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to generate signature", e);
+ }
}
return null;
}
@@ -161,6 +203,9 @@ public Signature getVerifier(String alg){
return eddsa;
}
catch(Exception | NoClassDefFoundError e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to create verifier", e);
+ }
}
return null;
}
diff --git a/src/main/java/com/jcraft/jsch/KeyPairGenEdDSA.java b/src/main/java/com/jcraft/jsch/KeyPairGenEdDSA.java
index e6983524..839d3df6 100644
--- a/src/main/java/com/jcraft/jsch/KeyPairGenEdDSA.java
+++ b/src/main/java/com/jcraft/jsch/KeyPairGenEdDSA.java
@@ -30,7 +30,10 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
package com.jcraft.jsch;
public interface KeyPairGenEdDSA{
- void init(String Name, int keylen) throws Exception;
+ void init(String name, int keylen) throws Exception;
byte[] getPub();
byte[] getPrv();
+ default void init(String name, byte[] prv) throws Exception{
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/src/main/java/com/jcraft/jsch/KeyPairPKCS8.java b/src/main/java/com/jcraft/jsch/KeyPairPKCS8.java
index fa1f3f06..8ab277e7 100644
--- a/src/main/java/com/jcraft/jsch/KeyPairPKCS8.java
+++ b/src/main/java/com/jcraft/jsch/KeyPairPKCS8.java
@@ -29,8 +29,9 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
package com.jcraft.jsch;
-import java.util.Vector;
import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
class KeyPairPKCS8 extends KeyPair {
private static final byte[] rsaEncryption = {
@@ -40,7 +41,35 @@ class KeyPairPKCS8 extends KeyPair {
private static final byte[] dsaEncryption = {
(byte)0x2a, (byte)0x86, (byte)0x48, (byte)0xce,
- (byte)0x38, (byte)0x04, (byte)0x1
+ (byte)0x38, (byte)0x04, (byte)0x01
+ };
+
+ private static final byte[] ecPublicKey = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0xce,
+ (byte)0x3d, (byte)0x02, (byte)0x01
+ };
+
+ private static final byte[] ed25519 = {
+ (byte)0x2b, (byte)0x65, (byte)0x70
+ };
+
+ private static final byte[] ed448 = {
+ (byte)0x2b, (byte)0x65, (byte)0x71
+ };
+
+ private static final byte[] secp256r1 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0xce,
+ (byte)0x3d, (byte)0x03, (byte)0x01, (byte)0x07
+ };
+
+ private static final byte[] secp384r1 = {
+ (byte)0x2b, (byte)0x81, (byte)0x04, (byte)0x00,
+ (byte)0x22
+ };
+
+ private static final byte[] secp521r1 = {
+ (byte)0x2b, (byte)0x81, (byte)0x04, (byte)0x00,
+ (byte)0x23
};
private static final byte[] pbes2 = {
@@ -53,6 +82,46 @@ class KeyPairPKCS8 extends KeyPair {
(byte)0x0d, (byte)0x01, (byte)0x05, (byte)0x0c
};
+ private static final byte[] scrypt = {
+ (byte)0x2b, (byte)0x06, (byte)0x01, (byte)0x04, (byte)0x01,
+ (byte)0xda, (byte)0x47, (byte)0x04, (byte)0x0b
+ };
+
+ private static final byte[] hmacWithSha1 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x02, (byte)0x07
+ };
+
+ private static final byte[] hmacWithSha224 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x02, (byte)0x08
+ };
+
+ private static final byte[] hmacWithSha256 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x02, (byte)0x09
+ };
+
+ private static final byte[] hmacWithSha384 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x02, (byte)0x0a
+ };
+
+ private static final byte[] hmacWithSha512 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x02, (byte)0x0b
+ };
+
+ private static final byte[] hmacWithSha512224 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x02, (byte)0x0c
+ };
+
+ private static final byte[] hmacWithSha512256 = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x02, (byte)0x0d
+ };
+
private static final byte[] aes128cbc = {
(byte)0x60, (byte)0x86, (byte)0x48, (byte)0x01, (byte)0x65,
(byte)0x03, (byte)0x04, (byte)0x01, (byte)0x02
@@ -68,11 +137,56 @@ class KeyPairPKCS8 extends KeyPair {
(byte)0x03, (byte)0x04, (byte)0x01, (byte)0x2a
};
+ private static final byte[] descbc = {
+ (byte)0x2b, (byte)0x0e, (byte)0x03, (byte)0x02, (byte)0x07
+ };
+
+ private static final byte[] des3cbc = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x03, (byte)0x07
+ };
+
+ private static final byte[] rc2cbc = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x03, (byte)0x02
+ };
+
+ private static final byte[] rc5cbc = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x03, (byte)0x09
+ };
+
+ private static final byte[] pbeWithMD2AndDESCBC = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x01, (byte)0x05, (byte)0x01
+ };
+
+ private static final byte[] pbeWithMD2AndRC2CBC = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x01, (byte)0x05, (byte)0x04
+ };
+
private static final byte[] pbeWithMD5AndDESCBC = {
(byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
(byte)0x0d, (byte)0x01, (byte)0x05, (byte)0x03
};
+ private static final byte[] pbeWithMD5AndRC2CBC = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x01, (byte)0x05, (byte)0x06
+ };
+
+ private static final byte[] pbeWithSHA1AndDESCBC = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x01, (byte)0x05, (byte)0x0a
+ };
+
+ private static final byte[] pbeWithSHA1AndRC2CBC = {
+ (byte)0x2a, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xf7,
+ (byte)0x0d, (byte)0x01, (byte)0x05, (byte)0x0b
+ };
+
+
private KeyPair kpair = null;
KeyPairPKCS8(JSch jsch){
@@ -113,36 +227,97 @@ boolean parse(byte[] plain){
}
*/
+ byte[] _data = null;
+ byte[] prv_array = null;
+ byte[] _plain = null;
+ KeyPair _key = null;
try{
- Vector values = new Vector<>();
+ ASN1[] contents;
- ASN1[] contents = null;
ASN1 asn1 = new ASN1(plain);
+ if(!asn1.isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+
contents = asn1.getContents();
+ if(contents.length<3 || contents.length>4){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+ if(!contents[2].isOCTETSTRING()){
+ throw new ASN1Exception();
+ }
+ // attributes [0] IMPLICIT Attributes OPTIONAL
+ if(contents.length>3 && !contents[3].isCONTEXTCONSTRUCTED(0)){
+ throw new ASN1Exception();
+ }
+
+ int version = parseASN1IntegerAsInt(contents[0].getContent());
+ if(version!=0){
+ throw new ASN1Exception();
+ }
ASN1 privateKeyAlgorithm = contents[1];
ASN1 privateKey = contents[2];
contents = privateKeyAlgorithm.getContents();
- byte[] privateKeyAlgorithmID = contents[0].getContent();
- contents = contents[1].getContents();
- if(contents.length>0){
- for(int i = 0; i < contents.length; i++){
- values.addElement(contents[i].getContent());
- }
+ if(contents.length==0){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isOBJECT()){
+ throw new ASN1Exception();
}
+ byte[] privateKeyAlgorithmID = contents[0].getContent();
- byte[] _data = privateKey.getContent();
+ _data = privateKey.getContent();
KeyPair _kpair = null;
if(Util.array_equals(privateKeyAlgorithmID, rsaEncryption)){
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isNULL()){
+ throw new ASN1Exception();
+ }
+
_kpair = new KeyPairRSA(jsch);
_kpair.copy(this);
if(_kpair.parse(_data)){
kpair = _kpair;
- }
+ return true;
+ }
+ else {
+ throw new JSchException("failed to parse RSA");
+ }
}
else if(Util.array_equals(privateKeyAlgorithmID, dsaEncryption)){
+ List values = new ArrayList<>(3);
+
+ if(contents.length>1 && contents[1].isSEQUENCE()){
+ contents = contents[1].getContents();
+ if(contents.length!=3){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[2].isINTEGER()){
+ throw new ASN1Exception();
+ }
+
+ values.add(contents[0].getContent());
+ values.add(contents[1].getContent());
+ values.add(contents[2].getContent());
+ }
+
asn1 = new ASN1(_data);
if(values.size() == 0) { // embedded DSA parameters format
/*
@@ -153,62 +328,264 @@ else if(Util.array_equals(privateKeyAlgorithmID, dsaEncryption)){
INTEGER // G_array
INTEGER // prv_array
*/
+ if(!asn1.isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+
contents = asn1.getContents();
- byte[] bar = contents[1].getContent();
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isINTEGER()){
+ throw new ASN1Exception();
+ }
+
+ prv_array = contents[1].getContent();
+
contents = contents[0].getContents();
- for(int i = 0; i < contents.length; i++){
- values.addElement(contents[i].getContent());
+ if(contents.length!=3){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isINTEGER()){
+ throw new ASN1Exception();
}
- values.addElement(bar);
+ if(!contents[2].isINTEGER()){
+ throw new ASN1Exception();
+ }
+
+ values.add(contents[0].getContent());
+ values.add(contents[1].getContent());
+ values.add(contents[2].getContent());
}
else {
/*
INTEGER // prv_array
*/
- values.addElement(asn1.getContent());
+ if(!asn1.isINTEGER()){
+ throw new ASN1Exception();
+ }
+ prv_array = asn1.getContent();
}
- byte[] P_array = values.elementAt(0);
- byte[] Q_array = values.elementAt(1);
- byte[] G_array = values.elementAt(2);
- byte[] prv_array = values.elementAt(3);
+ byte[] P_array = values.get(0);
+ byte[] Q_array = values.get(1);
+ byte[] G_array = values.get(2);
// Y = g^X mode p
byte[] pub_array =
(new BigInteger(G_array)).
modPow(new BigInteger(prv_array), new BigInteger(P_array)).
toByteArray();
- KeyPairDSA _key = new KeyPairDSA(jsch,
- P_array, Q_array, G_array,
- pub_array, prv_array);
- plain = _key.getPrivateKey();
+ _key = new KeyPairDSA(jsch,
+ P_array, Q_array, G_array,
+ pub_array, prv_array);
+ _plain = _key.getPrivateKey();
_kpair = new KeyPairDSA(jsch);
_kpair.copy(this);
- if(_kpair.parse(plain)){
+ if(_kpair.parse(_plain)){
+ kpair = _kpair;
+ return true;
+ }
+ else {
+ throw new JSchException("failed to parse DSA");
+ }
+ }
+ else if(Util.array_equals(privateKeyAlgorithmID, ecPublicKey)){
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isOBJECT()){
+ throw new ASN1Exception();
+ }
+
+ byte[] namedCurve = contents[1].getContent();
+ byte[] name;
+ if(!Util.array_equals(namedCurve, secp256r1)){
+ name = Util.str2byte("nistp256");
+ }
+ else if(!Util.array_equals(namedCurve, secp384r1)){
+ name = Util.str2byte("nistp384");
+ }
+ else if(!Util.array_equals(namedCurve, secp521r1)){
+ name = Util.str2byte("nistp521");
+ }
+ else {
+ throw new JSchException("unsupported named curve oid: "+Util.toHex(namedCurve));
+ }
+
+ ASN1 ecPrivateKey = new ASN1(_data);
+ if(!ecPrivateKey.isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+
+ // ECPrivateKey ::= SEQUENCE {
+ // version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+ // privateKey OCTET STRING,
+ // parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
+ // publicKey [1] BIT STRING OPTIONAL
+ // }
+ contents = ecPrivateKey.getContents();
+ if(contents.length<3 || contents.length>4){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isOCTETSTRING()){
+ throw new ASN1Exception();
+ }
+
+ version = parseASN1IntegerAsInt(contents[0].getContent());
+ if(version!=1){
+ throw new ASN1Exception();
+ }
+ prv_array = contents[1].getContent();
+
+ // publicKey is required here since there is no other way to derive it.
+ ASN1 publicKey;
+ if(contents.length==3){
+ publicKey = contents[2];
+ }
+ else {
+ publicKey = contents[3];
+
+ // parameters [0] ECParameters {{ NamedCurve }} OPTIONAL
+ if(!contents[2].isCONTEXTCONSTRUCTED(0)){
+ throw new ASN1Exception();
+ }
+
+ // NamedCurve isn't required here since it is already known.
+ // But if it is included, they should be the same...
+ ASN1[] goo = contents[2].getContents();
+ if(goo.length!=1){
+ throw new ASN1Exception();
+ }
+ if(!goo[0].isOBJECT()){
+ throw new ASN1Exception();
+ }
+ if(!Util.array_equals(goo[0].getContent(), namedCurve)){
+ throw new ASN1Exception();
+ }
+ }
+
+ // publicKey [1] BIT STRING OPTIONAL
+ if(!publicKey.isCONTEXTCONSTRUCTED(1)){
+ throw new ASN1Exception();
+ }
+ contents = publicKey.getContents();
+ if(contents.length!=1){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isBITSTRING()){
+ throw new ASN1Exception();
+ }
+
+ byte[] Q_array = contents[0].getContent();
+ byte[][] tmp = KeyPairECDSA.fromPoint(Q_array);
+ byte[] r_array = tmp[0];
+ byte[] s_array = tmp[1];
+
+ _key = new KeyPairECDSA(jsch, name, r_array, s_array, prv_array);
+ _plain = _key.getPrivateKey();
+
+ _kpair = new KeyPairECDSA(jsch);
+ _kpair.copy(this);
+ if(_kpair.parse(_plain)){
+ kpair = _kpair;
+ return true;
+ }
+ else {
+ throw new JSchException("failed to parse ECDSA");
+ }
+ }
+ else if(Util.array_equals(privateKeyAlgorithmID, ed25519) ||
+ Util.array_equals(privateKeyAlgorithmID, ed448)){
+ if(contents.length!=1){
+ throw new ASN1Exception();
+ }
+ ASN1 curvePrivateKey = new ASN1(_data);
+ if(!curvePrivateKey.isOCTETSTRING()){
+ throw new ASN1Exception();
+ }
+
+ prv_array=curvePrivateKey.getContent();
+ if(Util.array_equals(privateKeyAlgorithmID, ed25519)){
+ _kpair = new KeyPairEd25519(jsch);
+ }
+ else {
+ _kpair = new KeyPairEd448(jsch);
+ }
+ _kpair.copy(this);
+ if(_kpair.parse(prv_array)){
kpair = _kpair;
+ return true;
+ }
+ else {
+ throw new JSchException("failed to parse EdDSA");
}
}
+ else {
+ throw new JSchException("unsupported privateKeyAlgorithm oid: "+Util.toHex(privateKeyAlgorithmID));
+ }
}
catch(ASN1Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "PKCS8: failed to parse key: ASN1 parsing error", e);
+ }
return false;
}
catch(Exception e){
- //System.err.println(e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "PKCS8: failed to parse key: "+e.getMessage(), e);
+ }
return false;
}
- return kpair != null;
+ finally{
+ Util.bzero(_data);
+ Util.bzero(prv_array);
+ Util.bzero(_plain);
+ if(_key!=null){
+ _key.dispose();
+ }
+ }
}
@Override
public byte[] getPublicKeyBlob(){
- return kpair.getPublicKeyBlob();
+ if(kpair!=null){
+ return kpair.getPublicKeyBlob();
+ }
+ else {
+ return super.getPublicKeyBlob();
+ }
}
@Override
- byte[] getKeyTypeName(){ return kpair.getKeyTypeName();}
+ byte[] getKeyTypeName(){
+ if(kpair!=null){
+ return kpair.getKeyTypeName();
+ }
+ else {
+ return new byte[0];
+ }
+ }
+
@Override
- public int getKeyType(){return kpair.getKeyType();}
+ public int getKeyType(){
+ if(kpair!=null){
+ return kpair.getKeyType();
+ }
+ else {
+ return UNKNOWN;
+ }
+ }
@Override
public int getKeySize(){
@@ -264,6 +641,25 @@ public boolean decrypt(byte[] _passphrase){
OCTET STRING [HEX DUMP]:5B66E6B3BF03944C92317BC370CC3AD0
OCTET STRING [HEX DUMP]:
+or
+
+ SEQUENCE
+ SEQUENCE
+ OBJECT :PBES2
+ SEQUENCE
+ SEQUENCE
+ OBJECT :PBKDF2
+ SEQUENCE
+ OCTET STRING [HEX DUMP]:E4E24ADC9C00BD4D
+ INTEGER :0800
+ SEQUENCE
+ OBJECT :hmacWithSHA256
+ NULL
+ SEQUENCE
+ OBJECT :aes-128-cbc
+ OCTET STRING [HEX DUMP]:5B66E6B3BF03944C92317BC370CC3AD0
+ OCTET STRING [HEX DUMP]:
+
or
SEQUENCE
@@ -275,112 +671,344 @@ public boolean decrypt(byte[] _passphrase){
OCTET STRING [HEX DUMP]
*/
+ byte[] _data = null;
+ byte[] key = null;
+ byte[] plain = null;
try{
+ ASN1[] contents;
- ASN1[] contents = null;
ASN1 asn1 = new ASN1(data);
+ if(!asn1.isSEQUENCE()){
+ throw new ASN1Exception();
+ }
- contents = asn1.getContents();
-
- byte[] _data = contents[1].getContent();
+ contents = asn1.getContents();
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isOCTETSTRING()){
+ throw new ASN1Exception();
+ }
+ _data = contents[1].getContent();
ASN1 pbes = contents[0];
+
contents = pbes.getContents();
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isOBJECT()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+
byte[] pbesid = contents[0].getContent();
ASN1 pbesparam = contents[1];
- byte[] salt = null;
- int iterations = 0;
- byte[] iv = null;
- byte[] encryptfuncid = null;
+ String kdfname;
+ KDF kdfinst;
+ byte[] encryptfuncid;
+ ASN1 encryptparams;
if(Util.array_equals(pbesid, pbes2)){
contents = pbesparam.getContents();
- ASN1 pbkdf = contents[0];
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+
+ ASN1 kdf = contents[0];
ASN1 encryptfunc = contents[1];
- contents = pbkdf.getContents();
- byte[] pbkdfid = contents[0].getContent();
- ASN1 pbkdffunc = contents[1];
- contents = pbkdffunc.getContents();
- salt = contents[0].getContent();
- iterations =
- Integer.parseInt((new BigInteger(contents[1].getContent())).toString());
+
+ if(!kdf.isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+ if(!encryptfunc.isSEQUENCE()){
+ throw new ASN1Exception();
+ }
contents = encryptfunc.getContents();
+
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isOBJECT()){
+ throw new ASN1Exception();
+ }
+
encryptfuncid = contents[0].getContent();
- iv = contents[1].getContent();
- }
- else if(Util.array_equals(pbesid, pbeWithMD5AndDESCBC)){
- // not supported
- return false;
- }
- else {
- return false;
- }
+ encryptparams = contents[1];
+
+ contents = kdf.getContents();
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isOBJECT()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isSEQUENCE()){
+ throw new ASN1Exception();
+ }
- Cipher cipher=getCipher(encryptfuncid);
- if(cipher==null) return false;
+ byte[] kdfid = contents[0].getContent();
- byte[] key=null;
- try{
- Class extends PBKDF> c=Class.forName(JSch.getConfig("pbkdf")).asSubclass(PBKDF.class);
- PBKDF tmp=c.getDeclaredConstructor().newInstance();
- key = tmp.getKey(_passphrase, salt, iterations, cipher.getBlockSize());
+ if(Util.array_equals(kdfid, pbkdf2)){
+ ASN1 pbkdf2func = contents[1];
+ if(!pbkdf2func.isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+
+ ASN1 prf = null;
+ contents = pbkdf2func.getContents();
+ if(contents.length<2 || contents.length>4){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isOCTETSTRING()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isINTEGER()){
+ throw new ASN1Exception();
+ }
+
+ if(contents.length==4){
+ if(!contents[2].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[3].isSEQUENCE()){
+ throw new ASN1Exception();
+ }
+ prf = contents[3];
+ }
+ else if(contents.length==3){
+ if(contents[2].isSEQUENCE()){
+ prf = contents[2];
+ }
+ else if(!contents[2].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ }
+
+ byte[] prfid = null;
+ byte[] salt = contents[0].getContent();
+ int iterations = parseASN1IntegerAsInt(contents[1].getContent());
+
+ if(prf!=null){
+ contents = prf.getContents();
+ if(contents.length!=2){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isOBJECT()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isNULL()){
+ throw new ASN1Exception();
+ }
+
+ prfid = contents[0].getContent();
+ }
+
+ kdfname = getPBKDF2Name(prfid);
+ PBKDF2 pbkdf2kdf = getPBKDF2(kdfname);
+ pbkdf2kdf.init(salt, iterations);
+ kdfinst = pbkdf2kdf;
+ }
+ else if(Util.array_equals(kdfid, scrypt)){
+ contents = contents[1].getContents();
+ if(contents.length<4 || contents.length>5){
+ throw new ASN1Exception();
+ }
+ if(!contents[0].isOCTETSTRING()){
+ throw new ASN1Exception();
+ }
+ if(!contents[1].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[2].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(!contents[3].isINTEGER()){
+ throw new ASN1Exception();
+ }
+ if(contents.length>4 && !contents[4].isINTEGER()){
+ throw new ASN1Exception();
+ }
+
+ byte[] salt = contents[0].getContent();
+ int cost = parseASN1IntegerAsInt(contents[1].getContent());
+ int blocksize = parseASN1IntegerAsInt(contents[2].getContent());
+ int parallel = parseASN1IntegerAsInt(contents[3].getContent());
+
+ kdfname = "scrypt";
+ SCrypt scryptkdf = getSCrypt();
+ scryptkdf.init(salt, cost, blocksize, parallel);
+ kdfinst = scryptkdf;
+ }
+ else {
+ throw new JSchException("unsupported kdf oid: "+Util.toHex(kdfid));
+ }
}
- catch(Exception ee){
+ else {
+ String message;
+ if(Util.array_equals(pbesid, pbeWithMD2AndDESCBC)){
+ message="pbeWithMD2AndDES-CBC unsupported";
+ }
+ else if(Util.array_equals(pbesid, pbeWithMD2AndRC2CBC)){
+ message="pbeWithMD2AndRC2-CBC unsupported";
+ }
+ else if(Util.array_equals(pbesid, pbeWithMD5AndDESCBC)){
+ message="pbeWithMD5AndDES-CBC unsupported";
+ }
+ else if(Util.array_equals(pbesid, pbeWithMD5AndRC2CBC)){
+ message="pbeWithMD5AndRC2-CBC unsupported";
+ }
+ else if(Util.array_equals(pbesid, pbeWithSHA1AndDESCBC)){
+ message="pbeWithSHA1AndDES-CBC unsupported";
+ }
+ else if(Util.array_equals(pbesid, pbeWithSHA1AndRC2CBC)){
+ message="pbeWithSHA1AndRC2-CBC unsupported";
+ }
+ else {
+ message="unsupported encryption oid: "+Util.toHex(pbesid);
+ }
+ throw new JSchException(message);
}
+ byte[][] ivp = new byte[1][];
+ Cipher cipher=getCipher(encryptfuncid, encryptparams, ivp);
+ byte[] iv = ivp[0];
+
+ key = kdfinst.getKey(_passphrase, cipher.getBlockSize());
if(key==null){
- return false;
+ throw new JSchException("failed to generate key from KDF "+kdfname);
}
-
cipher.init(Cipher.DECRYPT_MODE, key, iv);
- Util.bzero(key);
- byte[] plain=new byte[_data.length];
+ plain=new byte[_data.length];
cipher.update(_data, 0, _data.length, plain, 0);
if(parse(plain)){
encrypted=false;
+ Util.bzero(data);
return true;
}
+ else {
+ throw new JSchException("failed to parse decrypted key");
+ }
}
catch(ASN1Exception e){
- // System.err.println(e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "PKCS8: failed to decrypt key: ASN1 parsing error", e);
+ }
+ return false;
}
catch(Exception e){
- // System.err.println(e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "PKCS8: failed to decrypt key: "+e.getMessage(), e);
+ }
+ return false;
}
+ finally{
+ Util.bzero(_data);
+ Util.bzero(key);
+ Util.bzero(plain);
+ }
+ }
- return false;
+ static String getPBKDF2Name(byte[] id) throws JSchException{
+ String name = null;
+ if(id==null || Util.array_equals(id, hmacWithSha1)){
+ name="pbkdf2-hmac-sha1";
+ }
+ else if(Util.array_equals(id, hmacWithSha224)){
+ name="pbkdf2-hmac-sha224";
+ }
+ else if(Util.array_equals(id, hmacWithSha256)){
+ name="pbkdf2-hmac-sha256";
+ }
+ else if(Util.array_equals(id, hmacWithSha384)){
+ name="pbkdf2-hmac-sha384";
+ }
+ else if(Util.array_equals(id, hmacWithSha512)){
+ name="pbkdf2-hmac-sha512";
+ }
+ else if(Util.array_equals(id, hmacWithSha512224)){
+ throw new JSchException("unsupported pbkdf2 function: pbkdf2-hmac-sha512-224");
+ }
+ else if(Util.array_equals(id, hmacWithSha512256)){
+ throw new JSchException("unsupported pbkdf2 function: pbkdf2-hmac-sha512-256");
+ }
+
+ if(name==null){
+ throw new JSchException("unsupported pbkdf2 function oid: "+Util.toHex(id));
+ }
+ return name;
+ }
+
+ static PBKDF2 getPBKDF2(String name) throws JSchException{
+ try{
+ Class extends PBKDF2> c=Class.forName(JSch.getConfig(name)).asSubclass(PBKDF2.class);
+ return c.getDeclaredConstructor().newInstance();
+ }
+ catch(Exception e){
+ throw new JSchException(name+" is not supported", e);
+ }
}
- Cipher getCipher(byte[] id){
- Cipher cipher=null;
+ static SCrypt getSCrypt() throws JSchException{
+ try{
+ Class extends SCrypt> c=Class.forName(JSch.getConfig("scrypt")).asSubclass(SCrypt.class);
+ return c.getDeclaredConstructor().newInstance();
+ }
+ catch(Exception e){
+ throw new JSchException("scrypt is not supported", e);
+ }
+ }
+
+ static Cipher getCipher(byte[] id, ASN1 encryptparams, byte[][] ivp) throws Exception{
String name = null;
+ if(Util.array_equals(id, aes128cbc)){
+ name="aes128-cbc";
+ }
+ else if(Util.array_equals(id, aes192cbc)){
+ name="aes192-cbc";
+ }
+ else if(Util.array_equals(id, aes256cbc)){
+ name="aes256-cbc";
+ }
+ else if(Util.array_equals(id, descbc)){
+ throw new JSchException("unsupported cipher function: des-cbc");
+ }
+ else if(Util.array_equals(id, des3cbc)){
+ throw new JSchException("unsupported cipher function: 3des-cbc");
+ }
+ else if(Util.array_equals(id, rc2cbc)){
+ throw new JSchException("unsupported cipher function: rc2-cbc");
+ }
+ else if(Util.array_equals(id, rc5cbc)){
+ throw new JSchException("unsupported cipher function: rc5-cbc");
+ }
+
+ if(name==null){
+ throw new JSchException("unsupported cipher function oid: "+Util.toHex(id));
+ }
+
+ if(!encryptparams.isOCTETSTRING()){
+ throw new ASN1Exception();
+ }
+ ivp[0] = encryptparams.getContent();
+
try{
- if(Util.array_equals(id, aes128cbc)){
- name="aes128-cbc";
- }
- else if(Util.array_equals(id, aes192cbc)){
- name="aes192-cbc";
- }
- else if(Util.array_equals(id, aes256cbc)){
- name="aes256-cbc";
- }
Class extends Cipher> c=Class.forName(JSch.getConfig(name)).asSubclass(Cipher.class);
- cipher=c.getDeclaredConstructor().newInstance();
+ return c.getDeclaredConstructor().newInstance();
}
catch(Exception e){
- if(jsch.getInstanceLogger().isEnabled(Logger.FATAL)){
- String message="";
- if(name==null){
- message="unknown oid: "+Util.toHex(id);
- }
- else {
- message="function "+name+" is not supported";
- }
- jsch.getInstanceLogger().log(Logger.FATAL, "PKCS8: "+message);
- }
+ throw new JSchException(name+" is not supported", e);
}
- return cipher;
+ }
+
+ static int parseASN1IntegerAsInt(byte[] content){
+ return new BigInteger(content).intValueExact();
}
}
diff --git a/src/main/java/com/jcraft/jsch/KeyPairRSA.java b/src/main/java/com/jcraft/jsch/KeyPairRSA.java
index 24f17dca..dc597597 100644
--- a/src/main/java/com/jcraft/jsch/KeyPairRSA.java
+++ b/src/main/java/com/jcraft/jsch/KeyPairRSA.java
@@ -81,7 +81,6 @@ void generate(int key_size) throws JSchException{
keypairgen=null;
}
catch(Exception e){
- //System.err.println("KeyPairRSA: "+e);
throw new JSchException(e.toString(), e);
}
}
@@ -132,7 +131,7 @@ boolean parse(byte [] plain){
int index=0;
int length=0;
- if(vendor==VENDOR_PUTTY){
+ if(vendor==VENDOR_PUTTY || vendor==VENDOR_PUTTY_V3){
Buffer buf = new Buffer(plain);
buf.skip(plain.length);
@@ -144,6 +143,9 @@ boolean parse(byte [] plain){
c_array = tmp[3];
}
catch(JSchException e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key", e);
+ }
return false;
}
@@ -172,6 +174,9 @@ boolean parse(byte [] plain){
return true;
}
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key");
+ }
return false;
}
@@ -193,6 +198,7 @@ boolean parse(byte [] plain){
if(n_array!=null){
key_size = (new BigInteger(n_array)).bitLength();
}
+ publicKeyComment=Util.byte2str(prvKEyBuffer.getString());
getEPArray();
getEQArray();
@@ -318,7 +324,9 @@ boolean parse(byte [] plain){
}
catch(Exception e){
- //System.err.println(e);
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to parse key", e);
+ }
return false;
}
return true;
@@ -369,6 +377,9 @@ public byte[] getSignature(byte[] data, String alg){
return Buffer.fromBytes(tmp).buffer;
}
catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to generate signature", e);
+ }
}
return null;
}
@@ -396,6 +407,9 @@ public Signature getVerifier(String alg){
return rsa;
}
catch(Exception e){
+ if(jsch.getInstanceLogger().isEnabled(Logger.ERROR)){
+ jsch.getInstanceLogger().log(Logger.ERROR, "failed to create verifier", e);
+ }
}
return null;
}
diff --git a/src/main/java/com/jcraft/jsch/Log4j2Logger.java b/src/main/java/com/jcraft/jsch/Log4j2Logger.java
index 27ee8ca5..291e5fa5 100644
--- a/src/main/java/com/jcraft/jsch/Log4j2Logger.java
+++ b/src/main/java/com/jcraft/jsch/Log4j2Logger.java
@@ -22,14 +22,14 @@ public void log(int level, String message) {
@Override
public void log(int level, String message, Throwable cause) {
- if (cause != null) {
+ if (cause == null) {
logger.log(getLevel(level), message);
return;
}
logger.log(getLevel(level), message, cause);
}
- private static Level getLevel(int level) {
+ static Level getLevel(int level) {
switch (level) {
case com.jcraft.jsch.Logger.DEBUG:
return Level.DEBUG;
diff --git a/src/main/java/com/jcraft/jsch/PBKDF.java b/src/main/java/com/jcraft/jsch/PBKDF.java
index 44619a37..4151303e 100644
--- a/src/main/java/com/jcraft/jsch/PBKDF.java
+++ b/src/main/java/com/jcraft/jsch/PBKDF.java
@@ -29,6 +29,10 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
package com.jcraft.jsch;
+/**
+ * Use PBKDF2 instead.
+ */
+@Deprecated
public interface PBKDF {
byte[] getKey(byte[] pass, byte[] salt, int iteration, int size);
}
diff --git a/src/main/java/com/jcraft/jsch/PBKDF2.java b/src/main/java/com/jcraft/jsch/PBKDF2.java
new file mode 100644
index 00000000..b578c76c
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/PBKDF2.java
@@ -0,0 +1,34 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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;
+
+public interface PBKDF2 extends KDF{
+ void init(byte[] salt, int iteration) throws Exception;
+}
diff --git a/src/main/java/com/jcraft/jsch/SCrypt.java b/src/main/java/com/jcraft/jsch/SCrypt.java
new file mode 100644
index 00000000..66b52ebd
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/SCrypt.java
@@ -0,0 +1,34 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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;
+
+public interface SCrypt extends KDF{
+ void init(byte[] salt, int cost, int blocksize, int parallel) throws Exception;
+}
diff --git a/src/main/java/com/jcraft/jsch/Session.java b/src/main/java/com/jcraft/jsch/Session.java
index a88b6c73..7c4393df 100644
--- a/src/main/java/com/jcraft/jsch/Session.java
+++ b/src/main/java/com/jcraft/jsch/Session.java
@@ -135,10 +135,6 @@ public class Session{
SocketFactory socket_factory=null;
- static final int buffer_margin = 32 + // maximum padding length
- 64 + // maximum mac length
- 32; // margin for deflater; deflater may inflate data
-
private Hashtable config=null;
private Proxy proxy=null;
@@ -562,7 +558,7 @@ public void connect(int connectTimeout) throws JSchException{
if(isConnected){
String message = e.toString();
packet.reset();
- buf.checkFreeSize(1+4*3+message.length()+2+buffer_margin);
+ buf.checkFreeSize(1+4*3+message.length()+2+getBufferMargin());
buf.putByte((byte)SSH_MSG_DISCONNECT);
buf.putInt(3);
buf.putString(Util.str2byte(message));
@@ -3117,6 +3113,9 @@ private void applyConfig() throws JSchException {
checkConfig(config, "kex");
checkConfig(config, "server_host_key");
checkConfig(config, "prefer_known_host_key_types");
+ checkConfig(config, "enable_pubkey_auth_query");
+ checkConfig(config, "try_additional_pubkey_algorithms");
+ checkConfig(config, "enable_auth_none");
checkConfig(config, "cipher.c2s");
checkConfig(config, "cipher.s2c");
@@ -3287,4 +3286,27 @@ public Logger getLogger() {
public void setLogger(Logger logger) {
this.logger = logger;
}
+
+ int getBufferMargin() {
+ int buffer_margin = 32 + // maximum padding length
+ 32; // margin for deflater; deflater may inflate data
+
+ Cipher _c2scipher = c2scipher;
+ MAC _c2smac = c2smac;
+
+ // maximum mac length
+ int mac_length = 20;
+ if (_c2scipher != null && (_c2scipher.isChaCha20() || _c2scipher.isAEAD())) {
+ if (_c2scipher.getTagSize() > mac_length) {
+ mac_length = _c2scipher.getTagSize();
+ }
+ } else if (_c2smac != null) {
+ if (_c2smac.getBlockSize() > mac_length) {
+ mac_length = _c2smac.getBlockSize();
+ }
+ }
+ buffer_margin += mac_length;
+
+ return buffer_margin;
+ }
}
diff --git a/src/main/java/com/jcraft/jsch/Slf4jLogger.java b/src/main/java/com/jcraft/jsch/Slf4jLogger.java
index ab05714a..c24c1563 100644
--- a/src/main/java/com/jcraft/jsch/Slf4jLogger.java
+++ b/src/main/java/com/jcraft/jsch/Slf4jLogger.java
@@ -8,19 +8,9 @@
*/
public class Slf4jLogger implements com.jcraft.jsch.Logger {
- private static final Logger stlogger = LoggerFactory.getLogger(JSch.class);
- private final Logger logger;
+ private static final Logger logger = LoggerFactory.getLogger(JSch.class);
- /**
- * Creates a new instance of Slf4jLogger
- */
- public Slf4jLogger() {
- this(stlogger);
- }
-
- Slf4jLogger(Logger logger) {
- this.logger = logger;
- }
+ public Slf4jLogger() {}
@Override
public boolean isEnabled(int level) {
diff --git a/src/main/java/com/jcraft/jsch/UserAuthNone.java b/src/main/java/com/jcraft/jsch/UserAuthNone.java
index 8eef355b..6d9f648c 100644
--- a/src/main/java/com/jcraft/jsch/UserAuthNone.java
+++ b/src/main/java/com/jcraft/jsch/UserAuthNone.java
@@ -66,6 +66,9 @@ public boolean start(Session session) throws Exception{
if(!result)
return false;
+ if(!session.getConfig("enable_auth_none").equals("yes"))
+ return false;
+
byte[] _username=null;
_username=Util.str2byte(username);
diff --git a/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java b/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java
index d18cf731..2884ae80 100644
--- a/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java
+++ b/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java
@@ -125,6 +125,9 @@ private boolean _start(Session session, List identities, List
return false;
}
+ boolean use_pk_auth_query=session.getConfig("enable_pubkey_auth_query").equals("yes");
+ boolean try_other_pkmethods=session.getConfig("try_additional_pubkey_algorithms").equals("yes");
+
List rsamethods=new ArrayList<>();
List nonrsamethods=new ArrayList<>();
for(String pkmethod : pkmethods){
@@ -156,32 +159,126 @@ private boolean _start(Session session, List identities, List
String _ipkmethod=identity.getAlgName();
List ipkmethods=null;
if(_ipkmethod.equals("ssh-rsa")){
- ipkmethods=rsamethods;
+ ipkmethods=new ArrayList<>(rsamethods);
}
else if(nonrsamethods.contains(_ipkmethod)){
- ipkmethods=Collections.singletonList(_ipkmethod);
+ ipkmethods=new ArrayList<>(1);
+ ipkmethods.add(_ipkmethod);
}
- if(ipkmethods==null) {
+ if(ipkmethods==null || ipkmethods.isEmpty()) {
if(session.getLogger().isEnabled(Logger.DEBUG)){
session.getLogger().log(Logger.DEBUG,
_ipkmethod+" cannot be used as public key type for identity "+identity.getName());
}
- continue;
+ continue iloop;
}
- byte[] pubkeyblob=identity.getPublicKeyBlob();
- List pkmethodsuccesses=null;
+ ipkmethodloop:
+ while(!ipkmethods.isEmpty() && session.auth_failures < session.max_auth_tries){
+ byte[] pubkeyblob=identity.getPublicKeyBlob();
+ List pkmethodsuccesses=null;
+
+ if(pubkeyblob!=null && use_pk_auth_query){
+ command=SSH_MSG_USERAUTH_FAILURE;
+ Iterator it=ipkmethods.iterator();
+ loop3:
+ while(it.hasNext()){
+ String ipkmethod=it.next();
+ it.remove();
+ if(not_available_pks.contains(ipkmethod) && !(identity instanceof AgentIdentity)){
+ if(session.getLogger().isEnabled(Logger.DEBUG)){
+ session.getLogger().log(Logger.DEBUG,
+ ipkmethod+" not available for identity "+identity.getName());
+ }
+ continue loop3;
+ }
+
+ // send
+ // byte SSH_MSG_USERAUTH_REQUEST(50)
+ // string user name
+ // string service name ("ssh-connection")
+ // string "publickey"
+ // boolen FALSE
+ // string public key algorithm name
+ // string public key blob
+ packet.reset();
+ buf.putByte((byte)SSH_MSG_USERAUTH_REQUEST);
+ buf.putString(_username);
+ buf.putString(Util.str2byte("ssh-connection"));
+ buf.putString(Util.str2byte("publickey"));
+ buf.putByte((byte)0);
+ buf.putString(Util.str2byte(ipkmethod));
+ buf.putString(pubkeyblob);
+ session.write(packet);
+
+ loop1:
+ while(true){
+ buf=session.read(buf);
+ command=buf.getCommand()&0xff;
+
+ if(command==SSH_MSG_USERAUTH_PK_OK){
+ if(session.getLogger().isEnabled(Logger.DEBUG)){
+ session.getLogger().log(Logger.DEBUG,
+ ipkmethod + " preauth success");
+ }
+ pkmethodsuccesses=new ArrayList<>(1);
+ pkmethodsuccesses.add(ipkmethod);
+ break loop3;
+ }
+ else if(command==SSH_MSG_USERAUTH_FAILURE){
+ if(session.getLogger().isEnabled(Logger.DEBUG)){
+ session.getLogger().log(Logger.DEBUG,
+ ipkmethod + " preauth failure");
+ }
+ continue loop3;
+ }
+ else if(command==SSH_MSG_USERAUTH_BANNER){
+ buf.getInt(); buf.getByte(); buf.getByte();
+ byte[] _message=buf.getString();
+ byte[] lang=buf.getString();
+ String message=Util.byte2str(_message);
+ if(userinfo!=null){
+ userinfo.showMessage(message);
+ }
+ continue loop1;
+ }
+ else{
+ //System.err.println("USERAUTH fail ("+command+")");
+ //throw new JSchException("USERAUTH fail ("+command+")");
+ if(session.getLogger().isEnabled(Logger.DEBUG)){
+ session.getLogger().log(Logger.DEBUG,
+ ipkmethod + " preauth failure command (" + command + ")");
+ }
+ continue loop3;
+ }
+ }
+ }
+
+ if(command!=SSH_MSG_USERAUTH_PK_OK){
+ continue iloop;
+ }
+ }
+
+ if(identity.isEncrypted()) continue iloop;
+ if(pubkeyblob==null) pubkeyblob=identity.getPublicKeyBlob();
+
+ //System.err.println("UserAuthPublicKey: pubkeyblob="+pubkeyblob);
- if(pubkeyblob!=null){
- command=SSH_MSG_USERAUTH_FAILURE;
- loop3:
- for(String ipkmethod : ipkmethods){
- if(not_available_pks.contains(ipkmethod) && !(identity instanceof AgentIdentity)){
+ if(pubkeyblob==null) continue iloop;
+ if(pkmethodsuccesses==null) pkmethodsuccesses=ipkmethods;
+ if(pkmethodsuccesses.isEmpty()) continue iloop;
+
+ Iterator it=pkmethodsuccesses.iterator();
+ loop4:
+ while(it.hasNext() && session.auth_failures < session.max_auth_tries){
+ String pkmethodsuccess=it.next();
+ it.remove();
+ if(not_available_pks.contains(pkmethodsuccess) && !(identity instanceof AgentIdentity)){
if(session.getLogger().isEnabled(Logger.DEBUG)){
session.getLogger().log(Logger.DEBUG,
- ipkmethod+" not available for identity "+identity.getName());
+ pkmethodsuccess+" not available for identity "+identity.getName());
}
- continue loop3;
+ continue loop4;
}
// send
@@ -189,38 +286,54 @@ else if(nonrsamethods.contains(_ipkmethod)){
// string user name
// string service name ("ssh-connection")
// string "publickey"
- // boolen FALSE
+ // boolen TRUE
// string public key algorithm name
// string public key blob
+ // string signature
packet.reset();
buf.putByte((byte)SSH_MSG_USERAUTH_REQUEST);
buf.putString(_username);
buf.putString(Util.str2byte("ssh-connection"));
buf.putString(Util.str2byte("publickey"));
- buf.putByte((byte)0);
- buf.putString(Util.str2byte(ipkmethod));
+ buf.putByte((byte)1);
+ buf.putString(Util.str2byte(pkmethodsuccess));
buf.putString(pubkeyblob);
+
+ //byte[] tmp=new byte[buf.index-5];
+ //System.arraycopy(buf.buffer, 5, tmp, 0, tmp.length);
+ //buf.putString(signature);
+
+ byte[] sid=session.getSessionId();
+ int sidlen=sid.length;
+ byte[] tmp=new byte[4+sidlen+buf.index-5];
+ tmp[0]=(byte)(sidlen>>>24);
+ tmp[1]=(byte)(sidlen>>>16);
+ tmp[2]=(byte)(sidlen>>>8);
+ tmp[3]=(byte)(sidlen);
+ System.arraycopy(sid, 0, tmp, 4, sidlen);
+ System.arraycopy(buf.buffer, 5, tmp, 4+sidlen, buf.index-5);
+ byte[] signature=identity.getSignature(tmp, pkmethodsuccess);
+ if(signature==null){ // for example, too long key length.
+ if(session.getLogger().isEnabled(Logger.DEBUG)){
+ session.getLogger().log(Logger.DEBUG,
+ pkmethodsuccess + " signature failure");
+ }
+ continue loop4;
+ }
+ buf.putString(signature);
session.write(packet);
- loop1:
+ loop2:
while(true){
buf=session.read(buf);
command=buf.getCommand()&0xff;
- if(command==SSH_MSG_USERAUTH_PK_OK){
+ if(command==SSH_MSG_USERAUTH_SUCCESS){
if(session.getLogger().isEnabled(Logger.DEBUG)){
session.getLogger().log(Logger.DEBUG,
- ipkmethod + " preauth success");
+ pkmethodsuccess + " auth success");
}
- pkmethodsuccesses=Collections.singletonList(ipkmethod);
- break loop3;
- }
- else if(command==SSH_MSG_USERAUTH_FAILURE){
- if(session.getLogger().isEnabled(Logger.DEBUG)){
- session.getLogger().log(Logger.DEBUG,
- ipkmethod + " preauth failure");
- }
- continue loop3;
+ return true;
}
else if(command==SSH_MSG_USERAUTH_BANNER){
buf.getInt(); buf.getByte(); buf.getByte();
@@ -230,132 +343,41 @@ else if(command==SSH_MSG_USERAUTH_BANNER){
if(userinfo!=null){
userinfo.showMessage(message);
}
- continue loop1;
+ continue loop2;
}
- else{
- //System.err.println("USERAUTH fail ("+command+")");
- //throw new JSchException("USERAUTH fail ("+command+")");
+ else if(command==SSH_MSG_USERAUTH_FAILURE){
+ buf.getInt(); buf.getByte(); buf.getByte();
+ byte[] foo=buf.getString();
+ int partial_success=buf.getByte();
+ //System.err.println(new String(foo)+
+ // " partial_success:"+(partial_success!=0));
+ if(partial_success!=0){
+ throw new JSchPartialAuthException(Util.byte2str(foo));
+ }
+ session.auth_failures++;
if(session.getLogger().isEnabled(Logger.DEBUG)){
session.getLogger().log(Logger.DEBUG,
- ipkmethod + " preauth failure command (" + command + ")");
+ pkmethodsuccess + " auth failure");
}
- continue loop3;
- }
- }
- }
-
- if(command!=SSH_MSG_USERAUTH_PK_OK){
- continue iloop;
- }
- }
-
-
- if(identity.isEncrypted()) continue;
- if(pubkeyblob==null) pubkeyblob=identity.getPublicKeyBlob();
-
-//System.err.println("UserAuthPublicKey: pubkeyblob="+pubkeyblob);
-
- if(pubkeyblob==null) continue;
- if(pkmethodsuccesses==null) pkmethodsuccesses=ipkmethods;
-
- loop4:
- for(String pkmethodsuccess : pkmethodsuccesses){
- if(not_available_pks.contains(pkmethodsuccess) && !(identity instanceof AgentIdentity)){
- if(session.getLogger().isEnabled(Logger.DEBUG)){
- session.getLogger().log(Logger.DEBUG,
- pkmethodsuccess+" not available for identity "+identity.getName());
- }
- continue loop4;
- }
-
- // send
- // byte SSH_MSG_USERAUTH_REQUEST(50)
- // string user name
- // string service name ("ssh-connection")
- // string "publickey"
- // boolen TRUE
- // string public key algorithm name
- // string public key blob
- // string signature
- packet.reset();
- buf.putByte((byte)SSH_MSG_USERAUTH_REQUEST);
- buf.putString(_username);
- buf.putString(Util.str2byte("ssh-connection"));
- buf.putString(Util.str2byte("publickey"));
- buf.putByte((byte)1);
- buf.putString(Util.str2byte(pkmethodsuccess));
- buf.putString(pubkeyblob);
-
-// byte[] tmp=new byte[buf.index-5];
-// System.arraycopy(buf.buffer, 5, tmp, 0, tmp.length);
-// buf.putString(signature);
-
- byte[] sid=session.getSessionId();
- int sidlen=sid.length;
- byte[] tmp=new byte[4+sidlen+buf.index-5];
- tmp[0]=(byte)(sidlen>>>24);
- tmp[1]=(byte)(sidlen>>>16);
- tmp[2]=(byte)(sidlen>>>8);
- tmp[3]=(byte)(sidlen);
- System.arraycopy(sid, 0, tmp, 4, sidlen);
- System.arraycopy(buf.buffer, 5, tmp, 4+sidlen, buf.index-5);
- byte[] signature=identity.getSignature(tmp, pkmethodsuccess);
- if(signature==null){ // for example, too long key length.
- if(session.getLogger().isEnabled(Logger.DEBUG)){
- session.getLogger().log(Logger.DEBUG,
- pkmethodsuccess + " signature failure");
- }
- continue loop4;
- }
- buf.putString(signature);
- session.write(packet);
-
- loop2:
- while(true){
- buf=session.read(buf);
- command=buf.getCommand()&0xff;
-
- if(command==SSH_MSG_USERAUTH_SUCCESS){
- if(session.getLogger().isEnabled(Logger.DEBUG)){
- session.getLogger().log(Logger.DEBUG,
- pkmethodsuccess + " auth success");
- }
- return true;
- }
- else if(command==SSH_MSG_USERAUTH_BANNER){
- buf.getInt(); buf.getByte(); buf.getByte();
- byte[] _message=buf.getString();
- byte[] lang=buf.getString();
- String message=Util.byte2str(_message);
- if(userinfo!=null){
- userinfo.showMessage(message);
- }
- continue loop2;
- }
- else if(command==SSH_MSG_USERAUTH_FAILURE){
- buf.getInt(); buf.getByte(); buf.getByte();
- byte[] foo=buf.getString();
- int partial_success=buf.getByte();
- //System.err.println(new String(foo)+
- // " partial_success:"+(partial_success!=0));
- if(partial_success!=0){
- throw new JSchPartialAuthException(Util.byte2str(foo));
+ if(session.auth_failures >= session.max_auth_tries){
+ return false;
+ }
+ else if(try_other_pkmethods){
+ break loop2;
+ }
+ else{
+ continue iloop;
+ }
}
- session.auth_failures++;
+ //System.err.println("USERAUTH fail ("+command+")");
+ //throw new JSchException("USERAUTH fail ("+command+")");
if(session.getLogger().isEnabled(Logger.DEBUG)){
session.getLogger().log(Logger.DEBUG,
- pkmethodsuccess + " auth failure");
+ pkmethodsuccess + " auth failure command (" + command +")");
}
break loop2;
}
- //System.err.println("USERAUTH fail ("+command+")");
- //throw new JSchException("USERAUTH fail ("+command+")");
- if(session.getLogger().isEnabled(Logger.DEBUG)){
- session.getLogger().log(Logger.DEBUG,
- pkmethodsuccess + " auth failure command (" + command +")");
- }
- break loop2;
}
}
}
diff --git a/src/main/java/com/jcraft/jsch/bc/Argon2.java b/src/main/java/com/jcraft/jsch/bc/Argon2.java
new file mode 100644
index 00000000..20372762
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/bc/Argon2.java
@@ -0,0 +1,90 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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.JSchException;
+import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
+import org.bouncycastle.crypto.params.Argon2Parameters;
+
+public class Argon2 implements com.jcraft.jsch.Argon2{
+ private Argon2BytesGenerator generator;
+
+ @Override
+ public void init(byte[] salt, int iteration, int type, byte[] additional, byte[] secret, int memory, int parallelism, int version) throws Exception{
+ switch(type){
+ case com.jcraft.jsch.Argon2.ARGON2D:
+ type=Argon2Parameters.ARGON2_d;
+ break;
+ case com.jcraft.jsch.Argon2.ARGON2I:
+ type=Argon2Parameters.ARGON2_i;
+ break;
+ case com.jcraft.jsch.Argon2.ARGON2ID:
+ type=Argon2Parameters.ARGON2_id;
+ break;
+ default:
+ throw new JSchException("Invalid argon2 type.");
+ }
+
+ switch(version){
+ case com.jcraft.jsch.Argon2.V10:
+ version=Argon2Parameters.ARGON2_VERSION_10;
+ break;
+ case com.jcraft.jsch.Argon2.V13:
+ version=Argon2Parameters.ARGON2_VERSION_13;
+ break;
+ default:
+ throw new JSchException("Invalid argon2 version.");
+ }
+
+ try{
+ Argon2Parameters params=new Argon2Parameters.Builder(type)
+ .withSalt(salt)
+ .withAdditional(additional)
+ .withSecret(secret)
+ .withIterations(iteration)
+ .withMemoryAsKB(memory)
+ .withParallelism(parallelism)
+ .withVersion(version)
+ .build();
+ generator=new Argon2BytesGenerator();
+ generator.init(params);
+ }
+ catch(NoClassDefFoundError e){
+ throw new JSchException("argon2 unavailable", e);
+ }
+ }
+
+ @Override
+ public byte[] getKey(byte[] pass, int size){
+ byte[] key=new byte[size];
+ generator.generateBytes(pass, key);
+ return key;
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/bc/KeyPairGenEdDSA.java b/src/main/java/com/jcraft/jsch/bc/KeyPairGenEdDSA.java
index ec8eecba..d6396850 100644
--- a/src/main/java/com/jcraft/jsch/bc/KeyPairGenEdDSA.java
+++ b/src/main/java/com/jcraft/jsch/bc/KeyPairGenEdDSA.java
@@ -58,7 +58,26 @@ public void init(String name, int keylen) throws Exception{
}
}
@Override
- public byte[] getPrv(){return pub;}
+ public byte[] getPrv(){return prv;}
@Override
- public byte[] getPub(){return prv;}
+ public byte[] getPub(){return pub;}
+ @Override
+ public void init(String name, byte[] prv) throws Exception{
+ if(!name.equals("Ed25519") && !name.equals("Ed448")){
+ throw new NoSuchAlgorithmException("invalid curve " + name);
+ }
+ this.name = name;
+
+ if(name.equals("Ed25519")){
+ Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(prv);
+ pub = privateKey.generatePublicKey().getEncoded();
+ this.prv = privateKey.getEncoded();
+ }
+ else{
+ Ed448PrivateKeyParameters privateKey = new Ed448PrivateKeyParameters(prv);
+ pub = privateKey.generatePublicKey().getEncoded();
+ this.prv = privateKey.getEncoded();
+ }
+ this.keylen = this.prv.length;
+ }
}
diff --git a/src/main/java/com/jcraft/jsch/bc/SCrypt.java b/src/main/java/com/jcraft/jsch/bc/SCrypt.java
new file mode 100644
index 00000000..298ab9ba
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/bc/SCrypt.java
@@ -0,0 +1,59 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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.JSchException;
+
+public class SCrypt implements com.jcraft.jsch.SCrypt{
+ private Class> ignore;
+ private byte[] salt;
+ private int cost;
+ private int blocksize;
+ private int parallel;
+
+ @Override
+ public void init(byte[] salt, int cost, int blocksize, int parallel) throws Exception{
+ try{
+ ignore=org.bouncycastle.crypto.generators.SCrypt.class;
+ this.salt=salt;
+ this.cost=cost;
+ this.blocksize=blocksize;
+ this.parallel=parallel;
+ }
+ catch(NoClassDefFoundError e){
+ throw new JSchException("scrypt unavailable", e);
+ }
+ }
+
+ @Override
+ public byte[] getKey(byte[] pass, int size){
+ return org.bouncycastle.crypto.generators.SCrypt.generate(pass, salt, cost, blocksize, parallel, size);
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/jbcrypt/JBCrypt.java b/src/main/java/com/jcraft/jsch/jbcrypt/JBCrypt.java
new file mode 100644
index 00000000..d443edae
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/jbcrypt/JBCrypt.java
@@ -0,0 +1,50 @@
+/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */
+/*
+Copyright (c) 2013-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.jbcrypt;
+
+public class JBCrypt implements com.jcraft.jsch.BCrypt{
+ private BCrypt bcrypt;
+ private byte[] salt;
+ private int iteration;
+
+ @Override
+ public void init(byte[] salt, int iteration) throws Exception{
+ bcrypt=new BCrypt();
+ this.salt=salt;
+ this.iteration=iteration;
+ }
+
+ @Override
+ public byte[] getKey(byte[] pass, int size){
+ byte[] key=new byte[size];
+ bcrypt.pbkdf(pass, salt, iteration, key);
+ return key;
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/jce/ECDHN.java b/src/main/java/com/jcraft/jsch/jce/ECDHN.java
index 37a6d277..5682a63f 100644
--- a/src/main/java/com/jcraft/jsch/jce/ECDHN.java
+++ b/src/main/java/com/jcraft/jsch/jce/ECDHN.java
@@ -134,17 +134,14 @@ private byte[] insert0(byte[] buf){
if ((buf[0] & 0x80) == 0) return buf;
byte[] tmp = new byte[buf.length+1];
System.arraycopy(buf, 0, tmp, 1, buf.length);
- bzero(buf);
+ Util.bzero(buf);
return tmp;
}
private byte[] chop0(byte[] buf){
if(buf[0]!=0) return buf;
byte[] tmp = new byte[buf.length-1];
System.arraycopy(buf, 1, tmp, 0, tmp.length);
- bzero(buf);
+ Util.bzero(buf);
return tmp;
}
- private void bzero(byte[] buf){
- for(int i = 0; i sshd =
+ new GenericContainer<>(
+ new ImageFromDockerfile()
+ .withFileFromClasspath("asyncsshd.py", "docker/asyncsshd.py")
+ .withFileFromClasspath("ssh_host_ed448_key", "docker/ssh_host_ed448_key")
+ .withFileFromClasspath("ssh_host_ed448_key.pub", "docker/ssh_host_ed448_key.pub")
+ .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("authorized_keys", "docker/authorized_keys")
+ .withFileFromClasspath("Dockerfile", "docker/Dockerfile.asyncssh")
+ .withBuildArg("MAX_PKTSIZE", Integer.toString(maxPktSize())))
+ .withExposedPorts(22);
+
+ @BeforeAll
+ public static void beforeAll() {
+ JSch.setLogger(new Slf4jLogger());
+ }
+
+ @BeforeEach
+ public void beforeEach() throws IOException {
+ if (sshdLogConsumer == null) {
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
+ sshd.followOutput(sshdLogConsumer);
+ }
+
+ in = tmpDir.resolve("in");
+ out = tmpDir.resolve("out");
+ Files.createFile(in);
+ try (OutputStream os = Files.newOutputStream(in)) {
+ byte[] data = new byte[1024];
+ for (int i = 0; i < 1024 * 100; i += 1024) {
+ new Random().nextBytes(data);
+ os.write(data);
+ }
+ }
+ hash = sha256sum.digestAsHex(in);
+
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
+ }
+
+ protected abstract int maxPktSize();
+
+ protected void doTestSftp(String cipher, String mac, String compression) throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("cipher.s2c", cipher);
+ session.setConfig("cipher.c2s", cipher);
+ session.setConfig("mac.s2c", mac);
+ session.setConfig("mac.c2s", mac);
+ session.setConfig("compression.s2c", compression);
+ session.setConfig("compression.c2s", compression);
+ doSftp(session, true);
+ }
+
+ protected void doTestScp(String cipher, String mac, String compression) throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("cipher.s2c", cipher);
+ session.setConfig("cipher.c2s", cipher);
+ session.setConfig("mac.s2c", mac);
+ session.setConfig("mac.c2s", mac);
+ session.setConfig("compression.s2c", compression);
+ session.setConfig("compression.c2s", compression);
+ doScp(session, true);
+ }
+
+ 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, Base64.getDecoder().decode(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 void doSftp(Session session, boolean debugException) throws Exception {
+ try {
+ session.setTimeout(timeout);
+ session.connect();
+ ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
+ sftp.connect(timeout);
+ sftp.put(in.toString(), "/root/test");
+ sftp.get("/root/test", out.toString());
+ sftp.disconnect();
+ session.disconnect();
+ } catch (Exception e) {
+ if (debugException) {
+ printInfo();
+ }
+ throw e;
+ }
+
+ assertEquals(1024L * 100L, Files.size(out));
+ assertEquals(hash, sha256sum.digestAsHex(out));
+ }
+
+ private void doScp(Session session, boolean debugException) throws Exception {
+ try {
+ session.setTimeout(timeout);
+ session.connect();
+ ChannelExec scp;
+
+ scp = (ChannelExec) session.openChannel("exec");
+ try (InputStream is = scp.getInputStream()) {
+ try (OutputStream os = scp.getOutputStream()) {
+ scp.setCommand("scp -t /root/test");
+ scp.connect(timeout);
+ checkAck(is);
+ String cmd = "C0644 102400 test\n";
+ os.write(cmd.getBytes(UTF_8));
+ os.flush();
+ checkAck(is);
+ Files.copy(in, os);
+ os.flush();
+ sendAck(os);
+ checkAck(is);
+ }
+ }
+ while (scp.isConnected()) {
+ Thread.sleep(100L);
+ }
+
+ scp = (ChannelExec) session.openChannel("exec");
+ try (OutputStream os = scp.getOutputStream()) {
+ try (InputStream is = scp.getInputStream()) {
+ scp.setCommand("scp -f /root/test");
+ scp.connect(timeout);
+ sendAck(os);
+ int c = checkAck(is);
+ if (c == 'C') {
+ byte[] buf = new byte[17];
+ is.read(buf, 0, 17);
+ sendAck(os);
+ Files.copy(ByteStreams.limit(is, 100L * 1024L), out);
+ checkAck(is);
+ sendAck(os);
+ }
+ }
+ }
+ while (scp.isConnected()) {
+ Thread.sleep(100L);
+ }
+
+ session.disconnect();
+ } catch (Exception e) {
+ if (debugException) {
+ printInfo();
+ }
+ throw e;
+ }
+
+ assertEquals(1024L * 100L, Files.size(out));
+ assertEquals(hash, sha256sum.digestAsHex(out));
+ }
+
+ private static int checkAck(InputStream is) throws IOException {
+ int b = is.read();
+ if (b == 0) {
+ return b;
+ } else if (b == -1) {
+ throw new IOException("no response");
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (b == 1 || b == 2) {
+ int c = is.read();
+ while (c > 0 && c != '\n') {
+ sb.append((char) c);
+ c = is.read();
+ }
+ }
+
+ switch (b) {
+ case 1:
+ throw new IOException("error: " + sb);
+ case 2:
+ throw new IOException("fatal error: " + sb);
+ default:
+ return b;
+ }
+ }
+
+ private static void sendAck(OutputStream os) throws IOException {
+ byte[] ack = new byte[1];
+ ack[0] = 0;
+ os.write(ack);
+ os.flush();
+ }
+
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ System.out.println("");
+ System.out.println("");
+ System.out.println("");
+ }
+
+ private String getResourceFile(String fileName) {
+ return ResourceUtil.getResourceFile(getClass(), fileName);
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/Algorithms2IT.java b/src/test/java/com/jcraft/jsch/Algorithms2IT.java
index b04b85b0..47559265 100644
--- a/src/test/java/com/jcraft/jsch/Algorithms2IT.java
+++ b/src/test/java/com/jcraft/jsch/Algorithms2IT.java
@@ -1,25 +1,25 @@
package com.jcraft.jsch;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.JRE.JAVA_11;
import static org.junit.jupiter.api.condition.JRE.JAVA_15;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
-import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -28,7 +28,6 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
-import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -41,9 +40,8 @@ public class Algorithms2IT {
// Python can be slow for DH group 18
private static final int timeout = 10000;
private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest());
- private static final ListAppender jschAppender = getListAppender(JSch.class);
- private static final ListAppender sshdAppender =
- getListAppender(AlgorithmsIT.class);
+ private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class);
+ private static final TestLogger sshdLogger = TestLoggerFactory.getTestLogger(Algorithms2IT.class);
@TempDir public Path tmpDir;
private Path in;
@@ -86,7 +84,7 @@ public static void beforeAll() {
@BeforeEach
public void beforeEach() throws IOException {
if (sshdLogConsumer == null) {
- sshdLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(Algorithms2IT.class));
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
sshd.followOutput(sshdLogConsumer);
}
@@ -102,18 +100,15 @@ public void beforeEach() throws IOException {
}
hash = sha256sum.digestAsHex(in);
- jschAppender.list.clear();
- sshdAppender.list.clear();
- jschAppender.start();
- sshdAppender.start();
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
- @AfterEach
- public void afterEach() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.clear();
- sshdAppender.list.clear();
+ @AfterAll
+ public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
@Test
@@ -199,7 +194,8 @@ public void testDHGEXSizes(String kex, String size) throws Exception {
doSftp(session, true);
String expectedKex = String.format("kex: algorithm: %s.*", kex);
- String expectedSizes = String.format("SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
+ String expectedSizes =
+ String.format("SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
checkLogs(expectedKex);
checkLogs(expectedSizes);
}
@@ -265,11 +261,7 @@ public void testRSA(String keyType) throws Exception {
}
@ParameterizedTest
- @CsvSource(
- value = {
- "seed-cbc@ssh.com,none",
- "seed-cbc@ssh.com,zlib@openssh.com"
- })
+ @CsvSource(value = {"seed-cbc@ssh.com,none", "seed-cbc@ssh.com,zlib@openssh.com"})
public void testCiphers(String cipher, String compression) throws Exception {
JSch ssh = createRSAIdentity();
Session session = createSession(ssh);
@@ -370,7 +362,7 @@ 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]));
+ return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
private Session createSession(JSch ssh) throws Exception {
@@ -390,8 +382,6 @@ private void doSftp(Session session, boolean debugException) throws Exception {
sftp.get("/root/test", out.toString());
sftp.disconnect();
session.disconnect();
- jschAppender.stop();
- sshdAppender.stop();
} catch (Exception e) {
if (debugException) {
printInfo();
@@ -403,20 +393,22 @@ private void doSftp(Session session, boolean debugException) throws Exception {
assertEquals(hash, sha256sum.digestAsHex(out));
}
- private static void printInfo() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
- sshdAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
System.out.println("");
System.out.println("");
System.out.println("");
}
- private static void checkLogs(String expected) {
+ private void checkLogs(String expected) {
Optional actualJsch =
- jschAppender.list.stream()
- .map(ILoggingEvent::getFormattedMessage)
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
.filter(msg -> msg.matches(expected))
.findFirst();
try {
@@ -430,11 +422,4 @@ private static void checkLogs(String expected) {
private String getResourceFile(String fileName) {
return ResourceUtil.getResourceFile(getClass(), fileName);
}
-
- private static ListAppender getListAppender(Class> clazz) {
- Logger logger = (Logger) LoggerFactory.getLogger(clazz);
- ListAppender listAppender = new ListAppender2<>();
- logger.addAppender(listAppender);
- return listAppender;
- }
}
diff --git a/src/test/java/com/jcraft/jsch/Algorithms3IT.java b/src/test/java/com/jcraft/jsch/Algorithms3IT.java
index a2560e88..314c4d1d 100644
--- a/src/test/java/com/jcraft/jsch/Algorithms3IT.java
+++ b/src/test/java/com/jcraft/jsch/Algorithms3IT.java
@@ -1,29 +1,28 @@
package com.jcraft.jsch;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
-import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
-import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -35,9 +34,8 @@ public class Algorithms3IT {
private static final int timeout = 2000;
private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest());
- private static final ListAppender jschAppender = getListAppender(JSch.class);
- private static final ListAppender sshdAppender =
- getListAppender(AlgorithmsIT.class);
+ private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class);
+ private static final TestLogger sshdLogger = TestLoggerFactory.getTestLogger(Algorithms3IT.class);
@TempDir public Path tmpDir;
private Path in;
@@ -62,7 +60,7 @@ public static void beforeAll() {
@BeforeEach
public void beforeEach() throws IOException {
if (sshdLogConsumer == null) {
- sshdLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(Algorithms3IT.class));
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
sshd.followOutput(sshdLogConsumer);
}
@@ -78,18 +76,15 @@ public void beforeEach() throws IOException {
}
hash = sha256sum.digestAsHex(in);
- jschAppender.list.clear();
- sshdAppender.list.clear();
- jschAppender.start();
- sshdAppender.start();
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
- @AfterEach
- public void afterEach() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.clear();
- sshdAppender.list.clear();
+ @AfterAll
+ public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
@ParameterizedTest
@@ -121,7 +116,7 @@ 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]));
+ return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
private Session createSession(JSch ssh) throws Exception {
@@ -146,8 +141,6 @@ private void doSftp(Session session, boolean debugException) throws Exception {
sftp.get("/root/test", out.toString());
sftp.disconnect();
session.disconnect();
- jschAppender.stop();
- sshdAppender.stop();
} catch (Exception e) {
if (debugException) {
printInfo();
@@ -159,20 +152,22 @@ private void doSftp(Session session, boolean debugException) throws Exception {
assertEquals(hash, sha256sum.digestAsHex(out));
}
- private static void printInfo() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
- sshdAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
System.out.println("");
System.out.println("");
System.out.println("");
}
- private static void checkLogs(String expected) {
+ private void checkLogs(String expected) {
Optional actualJsch =
- jschAppender.list.stream()
- .map(ILoggingEvent::getFormattedMessage)
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
.filter(msg -> msg.matches(expected))
.findFirst();
try {
@@ -186,11 +181,4 @@ private static void checkLogs(String expected) {
private String getResourceFile(String fileName) {
return ResourceUtil.getResourceFile(getClass(), fileName);
}
-
- private static ListAppender getListAppender(Class> clazz) {
- Logger logger = (Logger) LoggerFactory.getLogger(clazz);
- ListAppender listAppender = new ListAppender2<>();
- logger.addAppender(listAppender);
- return listAppender;
- }
}
diff --git a/src/test/java/com/jcraft/jsch/AlgorithmsIT.java b/src/test/java/com/jcraft/jsch/AlgorithmsIT.java
index 09292f0c..bcd9a350 100644
--- a/src/test/java/com/jcraft/jsch/AlgorithmsIT.java
+++ b/src/test/java/com/jcraft/jsch/AlgorithmsIT.java
@@ -2,26 +2,26 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.JRE.JAVA_11;
import static org.junit.jupiter.api.condition.JRE.JAVA_15;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
-import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -30,7 +30,6 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
-import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -42,9 +41,8 @@ public class AlgorithmsIT {
private static final int timeout = 2000;
private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest());
- private static final ListAppender jschAppender = getListAppender(JSch.class);
- private static final ListAppender sshdAppender =
- getListAppender(AlgorithmsIT.class);
+ private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class);
+ private static final TestLogger sshdLogger = TestLoggerFactory.getTestLogger(AlgorithmsIT.class);
@TempDir public Path tmpDir;
private Path in;
@@ -85,7 +83,7 @@ public static void beforeAll() {
@BeforeEach
public void beforeEach() throws IOException {
if (sshdLogConsumer == null) {
- sshdLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(AlgorithmsIT.class));
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
sshd.followOutput(sshdLogConsumer);
}
@@ -101,18 +99,15 @@ public void beforeEach() throws IOException {
}
hash = sha256sum.digestAsHex(in);
- jschAppender.list.clear();
- sshdAppender.list.clear();
- jschAppender.start();
- sshdAppender.start();
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
- @AfterEach
- public void afterEach() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.clear();
- sshdAppender.list.clear();
+ @AfterAll
+ public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
@ParameterizedTest
@@ -192,7 +187,8 @@ public void testDHGEXSizes(String kex, String size) throws Exception {
doSftp(session, true);
String expectedKex = String.format("kex: algorithm: %s.*", kex);
- String expectedSizes = String.format("SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
+ String expectedSizes =
+ String.format("SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
checkLogs(expectedKex);
checkLogs(expectedSizes);
}
@@ -534,7 +530,7 @@ 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]));
+ return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
private Session createSession(JSch ssh) throws Exception {
@@ -554,8 +550,6 @@ private void doSftp(Session session, boolean debugException) throws Exception {
sftp.get("/root/test", out.toString());
sftp.disconnect();
session.disconnect();
- jschAppender.stop();
- sshdAppender.stop();
} catch (Exception e) {
if (debugException) {
printInfo();
@@ -567,26 +561,29 @@ private void doSftp(Session session, boolean debugException) throws Exception {
assertEquals(hash, sha256sum.digestAsHex(out));
}
- private static void printInfo() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
- sshdAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
System.out.println("");
System.out.println("");
System.out.println("");
}
- private static void checkLogs(String expected) {
+ private void checkLogs(String expected) {
Optional actualJsch =
- jschAppender.list.stream()
- .map(ILoggingEvent::getFormattedMessage)
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
.filter(msg -> msg.matches(expected))
.findFirst();
- // Skip OpenSSH log checks, as log output from Docker falls behind and these assertions frequently run before they are output
+ // Skip OpenSSH log checks, as log output from Docker falls behind and these assertions
+ // frequently run before they are output
// Optional actualSshd =
- // sshdAppender.list.stream()
- // .map(ILoggingEvent::getFormattedMessage)
+ // sshdLogger.getAllLoggingEvents().stream()
+ // .map(LoggingEvent::getFormattedMessage)
// .filter(msg -> msg.matches("STDERR: debug1: " + expected))
// .findFirst();
try {
@@ -601,11 +598,4 @@ private static void checkLogs(String expected) {
private String getResourceFile(String fileName) {
return ResourceUtil.getResourceFile(getClass(), fileName);
}
-
- private static ListAppender getListAppender(Class> clazz) {
- Logger logger = (Logger) LoggerFactory.getLogger(clazz);
- ListAppender listAppender = new ListAppender2<>();
- logger.addAppender(listAppender);
- return listAppender;
- }
}
diff --git a/src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java b/src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java
index ceb238e1..c507b0b8 100644
--- a/src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java
+++ b/src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java
@@ -1,12 +1,12 @@
package com.jcraft.jsch;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.Base64;
import java.util.List;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@@ -96,7 +96,7 @@ 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]));
+ return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
private Session createSession(JSch ssh) throws Exception {
diff --git a/src/test/java/com/jcraft/jsch/JulLoggerTest.java b/src/test/java/com/jcraft/jsch/JulLoggerTest.java
index 88e21a70..735e2161 100644
--- a/src/test/java/com/jcraft/jsch/JulLoggerTest.java
+++ b/src/test/java/com/jcraft/jsch/JulLoggerTest.java
@@ -3,31 +3,54 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-class JulLoggerTest {
- private LinkedList messages;
- private Exception testException = new Exception("dummy exception");
+public class JulLoggerTest {
+
+ private static final Logger logger = Logger.getLogger(JSch.class.getName());
+ private static final ListHandler handler = new ListHandler();
+
+ private final Exception testException = new Exception("dummy exception");
+
+ @BeforeAll
+ public static void beforeAll() {
+ LogManager.getLogManager().reset();
+ Arrays.stream(logger.getHandlers()).forEach(logger::removeHandler);
+ logger.addHandler(handler);
+ logger.setLevel(Level.ALL);
+ logger.setUseParentHandlers(false);
+ Logger.getLogger("").setLevel(Level.OFF);
+ }
+
@BeforeEach
- void resetLogger() {
- messages = new LinkedList<>();
- Logger logger = Logger.getLogger(getClass().getName());
- Arrays.stream(logger.getHandlers())
- .forEach(logger::removeHandler);
-
+ public void beforeEach() {
+ handler.clear();
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ LogManager.getLogManager().reset();
}
+
@Test
- void testGetLevel() {
+ public void testGetLevel() {
assertEquals(Level.FINER, JulLogger.getLevel(-1));
-
+
assertEquals(Level.FINE, JulLogger.getLevel(com.jcraft.jsch.Logger.DEBUG));
assertEquals(Level.SEVERE, JulLogger.getLevel(com.jcraft.jsch.Logger.ERROR));
assertEquals(Level.SEVERE, JulLogger.getLevel(com.jcraft.jsch.Logger.FATAL));
@@ -36,21 +59,19 @@ void testGetLevel() {
assertEquals(Level.FINER, JulLogger.getLevel(Integer.MAX_VALUE));
}
-
+
@Test
- void testIsEnabled() {
- Logger logger = LogManager.getLogManager().getLogger(getClass().getName());
-
- JulLogger jl = new JulLogger(logger);
+ public void testIsEnabled() {
+ JulLogger jl = new JulLogger();
+
logger.setLevel(Level.FINEST);
-
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertTrue(jl.isEnabled(-1), "trace should be enabled");
-
+
logger.setLevel(Level.FINE);
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
@@ -58,7 +79,7 @@ void testIsEnabled() {
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertFalse(jl.isEnabled(-1), "trace should not be enabled");
-
+
logger.setLevel(Level.SEVERE);
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
@@ -66,7 +87,7 @@ void testIsEnabled() {
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should not be enabled");
assertFalse(jl.isEnabled(-1), "trace should not be enabled");
-
+
logger.setLevel(Level.INFO);
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
@@ -74,7 +95,7 @@ void testIsEnabled() {
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertFalse(jl.isEnabled(-1), "trace should not be enabled");
-
+
logger.setLevel(Level.OFF);
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should not be enabled");
@@ -82,7 +103,7 @@ void testIsEnabled() {
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should not be enabled");
assertFalse(jl.isEnabled(-1), "trace should not be enabled");
-
+
logger.setLevel(Level.FINER);
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
@@ -90,7 +111,7 @@ void testIsEnabled() {
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertTrue(jl.isEnabled(-1), "trace should be enabled");
-
+
logger.setLevel(Level.WARNING);
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
@@ -98,65 +119,85 @@ void testIsEnabled() {
assertFalse(jl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
assertTrue(jl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertFalse(jl.isEnabled(-1), "trace should not be enabled");
-
}
-
+
@Test
- void testLogging() {
- Logger logger = Logger.getLogger(getClass().getName());
- TestHandler handler = new TestHandler(messages);
-
- logger.addHandler(handler);
+ public void testLogging() {
+ JulLogger jl = new JulLogger();
+
+ List expectedMessages =
+ Arrays.asList("debug message", "debug message with null cause", "debug message with cause");
+ List> expectedExceptions =
+ Arrays.asList(Optional.empty(), Optional.ofNullable(null), Optional.of(testException));
+
logger.setLevel(Level.ALL);
- JulLogger jl = new JulLogger(logger);
-
jl.log(-1, "debug message");
jl.log(-1, "debug message with null cause", null);
jl.log(-1, "debug message with cause", testException);
- assertEquals("FINER: debug message (without cause)\r\n" +
- "FINER: debug message with null cause (without cause)\r\n" +
- "FINER: debug message with cause (with cause java.lang.Exception, dummy exception)", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
+ checkMessages(expectedMessages, expectedExceptions);
+
+ logger.setLevel(Level.ALL);
jl.log(com.jcraft.jsch.Logger.FATAL, "debug message");
jl.log(com.jcraft.jsch.Logger.FATAL, "debug message with null cause", null);
jl.log(com.jcraft.jsch.Logger.FATAL, "debug message with cause", testException);
- assertEquals("SEVERE: debug message (without cause)\r\n" +
- "SEVERE: debug message with null cause (without cause)\r\n" +
- "SEVERE: debug message with cause (with cause java.lang.Exception, dummy exception)", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
-
+ checkMessages(expectedMessages, expectedExceptions);
+
logger.setLevel(Level.SEVERE);
jl.log(-1, "debug message");
jl.log(-1, "debug message with null cause", null);
jl.log(-1, "debug message with cause", testException);
- assertEquals("", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
+ checkMessages(Collections.emptyList(), Collections.emptyList());
+
+ logger.setLevel(Level.SEVERE);
jl.log(com.jcraft.jsch.Logger.FATAL, "debug message");
jl.log(com.jcraft.jsch.Logger.FATAL, "debug message with null cause", null);
jl.log(com.jcraft.jsch.Logger.FATAL, "debug message with cause", testException);
- assertEquals("SEVERE: debug message (without cause)\r\n" +
- "SEVERE: debug message with null cause (without cause)\r\n" +
- "SEVERE: debug message with cause (with cause java.lang.Exception, dummy exception)", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
+ checkMessages(expectedMessages, expectedExceptions);
+ }
+
+ private void checkMessages(
+ List expectedMessages, List> expectedExceptions) {
+ List records = handler.getRecords();
+ handler.clear();
+ List actualMessages =
+ records.stream().map(LogRecord::getMessage).collect(Collectors.toList());
+ List> actualExceptions =
+ records.stream()
+ .map(LogRecord::getThrown)
+ .map(Optional::ofNullable)
+ .collect(Collectors.toList());
+ assertEquals(expectedMessages, actualMessages, "mismatch in logged messages");
+ assertEquals(expectedExceptions, actualExceptions, "mismatch in logged exceptions");
}
-
- private static class TestHandler extends Handler {
- private LinkedList messages;
- TestHandler(LinkedList messages) {
- this.messages = messages;
+ public static class ListHandler extends Handler {
+ private final List records;
+
+ public ListHandler() {
+ super();
+ records = Collections.synchronizedList(new ArrayList<>());
}
+
@Override
public void publish(LogRecord record) {
- Throwable cause = record.getThrown();
- messages.add(record.getLevel() + ": " + record.getMessage() + (cause == null ? " (without cause)" : " (with cause " + cause.getClass().getName() + ", " + cause.getMessage() + ")"));
+ records.add(record);
}
@Override
- public void flush() {
-
- }
+ public void flush() {}
@Override
- public void close() throws SecurityException {
-
+ public void close() throws SecurityException {}
+
+ public List getRecords() {
+ return Collections.unmodifiableList(new ArrayList<>(records));
+ }
+
+ public void clear() {
+ synchronized (records) {
+ records.clear();
+ setLevel(Level.ALL);
+ }
}
-
}
}
diff --git a/src/test/java/com/jcraft/jsch/KeyPair2IT.java b/src/test/java/com/jcraft/jsch/KeyPair2IT.java
new file mode 100644
index 00000000..f65f2ec7
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/KeyPair2IT.java
@@ -0,0 +1,131 @@
+package com.jcraft.jsch;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+public class KeyPair2IT {
+
+ // Python can be slow for DH group 18
+ private static final int timeout = 10000;
+
+ @Container
+ public GenericContainer> sshd = new GenericContainer<>(
+ new ImageFromDockerfile().withFileFromClasspath("asyncsshd.py", "docker/asyncsshd.py")
+ .withFileFromClasspath("ssh_host_ed448_key", "docker/ssh_host_ed448_key")
+ .withFileFromClasspath("ssh_host_ed448_key.pub", "docker/ssh_host_ed448_key.pub")
+ .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("authorized_keys", "docker/authorized_keys.KeyPairIT")
+ .withFileFromClasspath("Dockerfile", "docker/Dockerfile.asyncssh")).withExposedPorts(22);
+
+ @ParameterizedTest
+ @MethodSource("com.jcraft.jsch.KeyPair2Test#keyArgs")
+ void connectWithPublicKey(String path, String password, String keyType) throws Exception {
+
+ final JSch jSch = createIdentity(path, password);
+
+ Session session = createSession(jSch);
+
+ if (keyType != null) {
+ session.setConfig("PubkeyAcceptedAlgorithms", keyType);
+ }
+ try {
+ session.connect(timeout);
+ assertTrue(session.isConnected());
+ } finally {
+ session.disconnect();
+ }
+
+ }
+
+ @ParameterizedTest
+ @MethodSource("com.jcraft.jsch.KeyPair2Test#keyArgs")
+ void connectWithPublicKeyAndUserInfo(String path, String password, String keyType) throws Exception {
+
+ final JSch jSch = new JSch();
+
+ jSch.addIdentity(Paths.get(ClassLoader.getSystemResource(path).toURI()).toFile().getAbsolutePath());
+
+ Session session = createSession(jSch);
+ session.setUserInfo(new UserInfo() {
+ @Override
+ public String getPassphrase() {
+ return password;
+ }
+
+ @Override
+ public String getPassword() {
+ return null;
+ }
+
+ @Override
+ public boolean promptPassword(String message) {
+ return false;
+ }
+
+ @Override
+ public boolean promptPassphrase(String message) {
+ return true;
+ }
+
+ @Override
+ public boolean promptYesNo(String message) {
+ return false;
+ }
+
+ @Override
+ public void showMessage(String message) {
+
+ }
+ });
+
+ if (keyType != null) {
+ session.setConfig("PubkeyAcceptedAlgorithms", keyType);
+ }
+ try {
+ session.connect(timeout);
+ assertTrue(session.isConnected());
+ } finally {
+ session.disconnect();
+ }
+
+ }
+
+ private JSch createIdentity(String path, String password) throws JSchException, URISyntaxException {
+ JSch ssh = new JSch();
+ if (password != null) {
+ ssh.addIdentity(Paths.get(ClassLoader.getSystemResource(path).toURI()).toFile().getAbsolutePath(),
+ password);
+ } else {
+ ssh.addIdentity(Paths.get(ClassLoader.getSystemResource(path).toURI()).toFile().getAbsolutePath());
+ }
+ return ssh;
+ }
+
+ private Session createSession(JSch ssh) throws Exception {
+ Session session = ssh.getSession("root", sshd.getHost(), sshd.getFirstMappedPort());
+ session.setConfig("StrictHostKeyChecking", "no");
+ session.setConfig("PreferredAuthentications", "publickey");
+ return session;
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/KeyPair2Test.java b/src/test/java/com/jcraft/jsch/KeyPair2Test.java
new file mode 100644
index 00000000..24b38723
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/KeyPair2Test.java
@@ -0,0 +1,54 @@
+package com.jcraft.jsch;
+
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class KeyPair2Test {
+
+ @TempDir
+ public Path tmpDir;
+
+ static Stream keyArgs() {
+ return Stream.of(
+ // PuTTY v2 keys
+ Arguments.of("ppkv2_ed448_unix.ppk", null, "ssh-ed448"),
+ Arguments.of("ppkv2_ed448_unix_encrypted.ppk", "secret123", "ssh-ed448"),
+ Arguments.of("ppkv2_ed448_windows.ppk", null, "ssh-ed448"),
+ Arguments.of("ppkv2_ed448_windows_encrypted.ppk", "secret123", "ssh-ed448"),
+ // PuTTY v3 keys
+ Arguments.of("ppkv3_ed448_unix.ppk", null, "ssh-ed448"),
+ Arguments.of("ppkv3_ed448_unix_encrypted.ppk", "secret123", "ssh-ed448"),
+ Arguments.of("ppkv3_ed448_windows.ppk", null, "ssh-ed448"),
+ Arguments.of("ppkv3_ed448_windows_encrypted.ppk", "secret123", "ssh-ed448"),
+ // PKCS8 keys
+ Arguments.of("pkcs8_ed448", null, "ssh-ed448"),
+ Arguments.of("pkcs8_ed448_encrypted_scrypt", "secret123", "ssh-ed448")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("keyArgs")
+ void loadKey(String path, String password, String keyType) throws URISyntaxException, JSchException {
+ final JSch jSch = new JSch();
+ final String prvkey = Paths.get(ClassLoader.getSystemResource(path).toURI()).toFile().getAbsolutePath();
+ assertTrue(new File(prvkey).exists());
+ assertDoesNotThrow(() -> {
+ if (null != password) {
+ jSch.addIdentity(prvkey, password);
+ } else {
+ jSch.addIdentity(prvkey);
+ }
+ });
+ assertEquals(keyType, jSch.getIdentityRepository().getIdentities().get(0).getAlgName());
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/KeyPairIT.java b/src/test/java/com/jcraft/jsch/KeyPairIT.java
index f3ccfd68..46de7d6a 100644
--- a/src/test/java/com/jcraft/jsch/KeyPairIT.java
+++ b/src/test/java/com/jcraft/jsch/KeyPairIT.java
@@ -1,6 +1,5 @@
package com.jcraft.jsch;
-import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.containers.GenericContainer;
@@ -22,11 +21,6 @@ public class KeyPairIT {
.withFileFromClasspath("authorized_keys", "docker/authorized_keys.KeyPairIT")
.withFileFromClasspath("Dockerfile", "docker/Dockerfile.KeyPairIT")).withExposedPorts(22);
- @BeforeAll
- public static void beforeAll() {
- JSch.setLogger(new Slf4jLogger());
- }
-
@ParameterizedTest
@MethodSource("com.jcraft.jsch.KeyPairTest#keyArgs")
void connectWithPublicKey(String path, String password, String keyType) throws Exception {
diff --git a/src/test/java/com/jcraft/jsch/KeyPairTest.java b/src/test/java/com/jcraft/jsch/KeyPairTest.java
index 66262e44..8dafb13b 100644
--- a/src/test/java/com/jcraft/jsch/KeyPairTest.java
+++ b/src/test/java/com/jcraft/jsch/KeyPairTest.java
@@ -1,6 +1,5 @@
package com.jcraft.jsch;
-import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
@@ -22,11 +21,6 @@ class KeyPairTest {
@TempDir
public Path tmpDir;
- @BeforeAll
- static void init() {
- JSch.setLogger(new Slf4jLogger());
- }
-
static Stream keyArgs() {
return Stream.of(
// docker/id_rsa rsa
@@ -49,7 +43,72 @@ static Stream keyArgs() {
Arguments.of("docker/ssh_host_ecdsa384_key", null, "ecdsa-sha2-nistp384"),
Arguments.of("docker/ssh_host_ecdsa521_key", null, "ecdsa-sha2-nistp521"),
// encrypted ecdsa
- Arguments.of("encrypted_openssh_private_key_ecdsa", "secret123", "ecdsa-sha2-nistp256")
+ Arguments.of("encrypted_openssh_private_key_ecdsa", "secret123", "ecdsa-sha2-nistp256"),
+ // PuTTY v2 keys
+ Arguments.of("ppkv2_dsa_unix.ppk", null, "ssh-dss"),
+ Arguments.of("ppkv2_dsa_unix_encrypted.ppk", "secret123", "ssh-dss"),
+ Arguments.of("ppkv2_dsa_windows.ppk", null, "ssh-dss"),
+ Arguments.of("ppkv2_dsa_windows_encrypted.ppk", "secret123", "ssh-dss"),
+ Arguments.of("ppkv2_rsa_unix.ppk", null, "ssh-rsa"),
+ Arguments.of("ppkv2_rsa_unix_encrypted.ppk", "secret123", "ssh-rsa"),
+ Arguments.of("ppkv2_rsa_windows.ppk", null, "ssh-rsa"),
+ Arguments.of("ppkv2_rsa_windows_encrypted.ppk", "secret123", "ssh-rsa"),
+ Arguments.of("ppkv2_ecdsa256_unix.ppk", null, "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv2_ecdsa256_unix_encrypted.ppk", "secret123", "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv2_ecdsa384_unix.ppk", null, "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv2_ecdsa384_unix_encrypted.ppk", "secret123", "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv2_ecdsa521_unix.ppk", null, "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv2_ecdsa521_unix_encrypted.ppk", "secret123", "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv2_ecdsa256_windows.ppk", null, "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv2_ecdsa256_windows_encrypted.ppk", "secret123", "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv2_ecdsa384_windows.ppk", null, "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv2_ecdsa384_windows_encrypted.ppk", "secret123", "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv2_ecdsa521_windows.ppk", null, "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv2_ecdsa521_windows_encrypted.ppk", "secret123", "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv2_ed25519_unix.ppk", null, "ssh-ed25519"),
+ Arguments.of("ppkv2_ed25519_unix_encrypted.ppk", "secret123", "ssh-ed25519"),
+ Arguments.of("ppkv2_ed25519_windows.ppk", null, "ssh-ed25519"),
+ Arguments.of("ppkv2_ed25519_windows_encrypted.ppk", "secret123", "ssh-ed25519"),
+ // PuTTY v3 keys
+ Arguments.of("ppkv3_dsa_unix.ppk", null, "ssh-dss"),
+ Arguments.of("ppkv3_dsa_unix_encrypted.ppk", "secret123", "ssh-dss"),
+ Arguments.of("ppkv3_dsa_windows.ppk", null, "ssh-dss"),
+ Arguments.of("ppkv3_dsa_windows_encrypted.ppk", "secret123", "ssh-dss"),
+ Arguments.of("ppkv3_rsa_unix.ppk", null, "ssh-rsa"),
+ Arguments.of("ppkv3_rsa_unix_encrypted.ppk", "secret123", "ssh-rsa"),
+ Arguments.of("ppkv3_rsa_windows.ppk", null, "ssh-rsa"),
+ Arguments.of("ppkv3_rsa_windows_encrypted.ppk", "secret123", "ssh-rsa"),
+ Arguments.of("ppkv3_ecdsa256_unix.ppk", null, "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv3_ecdsa256_unix_encrypted.ppk", "secret123", "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv3_ecdsa384_unix.ppk", null, "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv3_ecdsa384_unix_encrypted.ppk", "secret123", "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv3_ecdsa521_unix.ppk", null, "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv3_ecdsa521_unix_encrypted.ppk", "secret123", "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv3_ecdsa256_windows.ppk", null, "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv3_ecdsa256_windows_encrypted.ppk", "secret123", "ecdsa-sha2-nistp256"),
+ Arguments.of("ppkv3_ecdsa384_windows.ppk", null, "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv3_ecdsa384_windows_encrypted.ppk", "secret123", "ecdsa-sha2-nistp384"),
+ Arguments.of("ppkv3_ecdsa521_windows.ppk", null, "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv3_ecdsa521_windows_encrypted.ppk", "secret123", "ecdsa-sha2-nistp521"),
+ Arguments.of("ppkv3_ed25519_unix.ppk", null, "ssh-ed25519"),
+ Arguments.of("ppkv3_ed25519_unix_encrypted.ppk", "secret123", "ssh-ed25519"),
+ Arguments.of("ppkv3_ed25519_windows.ppk", null, "ssh-ed25519"),
+ Arguments.of("ppkv3_ed25519_windows_encrypted.ppk", "secret123", "ssh-ed25519"),
+ // PKCS8 keys
+ Arguments.of("pkcs8_dsa", null, "ssh-dss"),
+ Arguments.of("pkcs8_dsa_encrypted_hmacsha1", "secret123", "ssh-dss"),
+ Arguments.of("pkcs8_dsa_encrypted_hmacsha256", "secret123", "ssh-dss"),
+ Arguments.of("pkcs8_rsa", null, "ssh-rsa"),
+ Arguments.of("pkcs8_rsa_encrypted_hmacsha1", "secret123", "ssh-rsa"),
+ Arguments.of("pkcs8_rsa_encrypted_hmacsha256", "secret123", "ssh-rsa"),
+ Arguments.of("pkcs8_ecdsa256", null, "ecdsa-sha2-nistp256"),
+ Arguments.of("pkcs8_ecdsa256_encrypted_scrypt", "secret123", "ecdsa-sha2-nistp256"),
+ Arguments.of("pkcs8_ecdsa384", null, "ecdsa-sha2-nistp384"),
+ Arguments.of("pkcs8_ecdsa384_encrypted_scrypt", "secret123", "ecdsa-sha2-nistp384"),
+ Arguments.of("pkcs8_ecdsa521", null, "ecdsa-sha2-nistp521"),
+ Arguments.of("pkcs8_ecdsa521_encrypted_scrypt", "secret123", "ecdsa-sha2-nistp521"),
+ Arguments.of("pkcs8_ed25519", null, "ssh-ed25519"),
+ Arguments.of("pkcs8_ed25519_encrypted_scrypt", "secret123", "ssh-ed25519")
);
}
@@ -87,18 +146,6 @@ void genKeypairEncrypted() {
});
}
- @Test
- void loadAndAcessEncryptedKeyWithoutPassword() throws URISyntaxException, JSchException {
- final JSch jSch = new JSch();
- final String prvkey = Paths.get(ClassLoader.getSystemResource("encrypted_openssh_private_key_dsa").toURI()).toFile().getAbsolutePath();
- assertTrue(new File(prvkey).exists());
- jSch.addIdentity(prvkey);
-
- assertThrows(IllegalStateException.class, () -> {
- jSch.getIdentityRepository().getIdentities().get(0).getAlgName();
- });
- }
-
@ParameterizedTest
@ValueSource(strings = {"encrypted_openssh_private_key_rsa", "encrypted_openssh_private_key_dsa", "encrypted_openssh_private_key_ecdsa"})
void decryptEncryptedOpensshKey(String keyFile) throws URISyntaxException, JSchException {
diff --git a/src/test/java/com/jcraft/jsch/ListAppender2.java b/src/test/java/com/jcraft/jsch/ListAppender2.java
deleted file mode 100644
index 2d72b8a2..00000000
--- a/src/test/java/com/jcraft/jsch/ListAppender2.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.jcraft.jsch;
-
-import ch.qos.logback.core.read.ListAppender;
-
-public class ListAppender2 extends ListAppender {
-
- @Override
- protected void append(E e) {
- // Avoid append messages after appender is stopped to avoid ConcurrentModificationException's
- // when examining the List of events.
- if (super.started) {
- super.append(e);
- }
- }
-}
diff --git a/src/test/java/com/jcraft/jsch/Log4j2LoggerTest.java b/src/test/java/com/jcraft/jsch/Log4j2LoggerTest.java
new file mode 100644
index 00000000..98e8b69a
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/Log4j2LoggerTest.java
@@ -0,0 +1,174 @@
+package com.jcraft.jsch;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.junit.LoggerContextSource;
+import org.apache.logging.log4j.junit.Named;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@LoggerContextSource("Log4j2LoggerTest.xml")
+public class Log4j2LoggerTest {
+
+ private final Exception testException = new Exception("dummy exception");
+ private final ListAppender appender;
+
+ public Log4j2LoggerTest(@Named("List") ListAppender appender) {
+ this.appender = appender;
+ }
+
+ @BeforeEach
+ public void beforeEach() {
+ appender.clear();
+ }
+
+ @Test
+ public void testGetLevel() {
+ assertEquals(Level.TRACE, Log4j2Logger.getLevel(-1));
+
+ assertEquals(Level.DEBUG, Log4j2Logger.getLevel(com.jcraft.jsch.Logger.DEBUG));
+ assertEquals(Level.ERROR, Log4j2Logger.getLevel(com.jcraft.jsch.Logger.ERROR));
+ assertEquals(Level.FATAL, Log4j2Logger.getLevel(com.jcraft.jsch.Logger.FATAL));
+ assertEquals(Level.INFO, Log4j2Logger.getLevel(com.jcraft.jsch.Logger.INFO));
+ assertEquals(Level.WARN, Log4j2Logger.getLevel(com.jcraft.jsch.Logger.WARN));
+
+ assertEquals(Level.TRACE, Log4j2Logger.getLevel(Integer.MAX_VALUE));
+ }
+
+ @Test
+ public void testIsEnabled() {
+ Log4j2Logger ll = new Log4j2Logger();
+
+ setLevel(Level.ALL);
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
+ assertTrue(ll.isEnabled(-1), "trace should be enabled");
+
+ setLevel(Level.DEBUG);
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
+ assertFalse(ll.isEnabled(-1), "trace should not be enabled");
+
+ setLevel(Level.ERROR);
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should not be enabled");
+ assertFalse(ll.isEnabled(-1), "trace should not be enabled");
+
+ setLevel(Level.FATAL);
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should not be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should not be enabled");
+ assertFalse(ll.isEnabled(-1), "trace should not be enabled");
+
+ setLevel(Level.INFO);
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
+ assertFalse(ll.isEnabled(-1), "trace should not be enabled");
+
+ setLevel(Level.OFF);
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should not be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should not be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should not be enabled");
+ assertFalse(ll.isEnabled(-1), "trace should not be enabled");
+
+ setLevel(Level.TRACE);
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
+ assertTrue(ll.isEnabled(-1), "trace should be enabled");
+
+ setLevel(Level.WARN);
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
+ assertFalse(ll.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
+ assertTrue(ll.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
+ assertFalse(ll.isEnabled(-1), "trace should not be enabled");
+ }
+
+ @Test
+ public void testLogging() {
+ Log4j2Logger ll = new Log4j2Logger();
+
+ List expectedMessages =
+ Arrays.asList("debug message", "debug message with null cause", "debug message with cause");
+ List> expectedExceptions =
+ Arrays.asList(Optional.empty(), Optional.ofNullable(null), Optional.of(testException));
+
+ setLevel(Level.TRACE);
+ ll.log(-1, "debug message");
+ ll.log(-1, "debug message with null cause", null);
+ ll.log(-1, "debug message with cause", testException);
+ checkMessages(expectedMessages, expectedExceptions);
+
+ setLevel(Level.TRACE);
+ ll.log(com.jcraft.jsch.Logger.FATAL, "debug message");
+ ll.log(com.jcraft.jsch.Logger.FATAL, "debug message with null cause", null);
+ ll.log(com.jcraft.jsch.Logger.FATAL, "debug message with cause", testException);
+ checkMessages(expectedMessages, expectedExceptions);
+
+ setLevel(Level.ERROR);
+ ll.log(-1, "debug message");
+ ll.log(-1, "debug message with null cause", null);
+ ll.log(-1, "debug message with cause", testException);
+ checkMessages(Collections.emptyList(), Collections.emptyList());
+
+ setLevel(Level.ERROR);
+ ll.log(com.jcraft.jsch.Logger.FATAL, "debug message");
+ ll.log(com.jcraft.jsch.Logger.FATAL, "debug message with null cause", null);
+ ll.log(com.jcraft.jsch.Logger.FATAL, "debug message with cause", testException);
+ checkMessages(expectedMessages, expectedExceptions);
+ }
+
+ private void checkMessages(
+ List expectedMessages, List> expectedExceptions) {
+ List events = appender.getEvents();
+ appender.clear();
+ List actualMessages =
+ events.stream()
+ .map(LogEvent::getMessage)
+ .map(Message::getFormattedMessage)
+ .collect(Collectors.toList());
+ List> actualExceptions =
+ events.stream()
+ .map(LogEvent::getThrown)
+ .map(Optional::ofNullable)
+ .collect(Collectors.toList());
+ assertEquals(expectedMessages, actualMessages, "mismatch in logged messages");
+ assertEquals(expectedExceptions, actualExceptions, "mismatch in logged exceptions");
+ }
+
+ private static void setLevel(Level level) {
+ Configurator.setLevel(JSch.class, level);
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/LoggerTest.java b/src/test/java/com/jcraft/jsch/LoggerTest.java
index c90166c3..fa73ed97 100644
--- a/src/test/java/com/jcraft/jsch/LoggerTest.java
+++ b/src/test/java/com/jcraft/jsch/LoggerTest.java
@@ -1,56 +1,60 @@
package com.jcraft.jsch;
import static org.junit.jupiter.api.Assertions.assertEquals;
+
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.util.LinkedList;
-import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import org.junit.jupiter.api.Test;
-class LoggerTest {
-
+public class LoggerTest {
+
+ private final Exception testException = new Exception("dummy exception");
+
@Test
- void testLogWithCause() {
- LinkedList messages = new LinkedList<>();
+ public void testLogging() {
+ List actualMessages = new ArrayList<>();
boolean[] enabledResult = new boolean[1];
- Logger logger = new Logger() {
- @Override
- public void log(int level, String message) {
- messages.add(level + ":" + message);
- }
- @Override
- public boolean isEnabled(int level) {
- return enabledResult[0];
- }
- };
-
- Exception ex = new Exception("dummy exception");
+ Logger logger =
+ new Logger() {
+ @Override
+ public void log(int level, String message) {
+ if (isEnabled(level)) {
+ actualMessages.add(level + ":" + message);
+ }
+ }
+
+ @Override
+ public boolean isEnabled(int level) {
+ return enabledResult[0];
+ }
+ };
+
+ actualMessages.clear();
+ enabledResult[0] = false;
+ logger.log(Logger.ERROR, "debug message");
+ logger.log(Logger.ERROR, "debug message with null cause", null);
+ logger.log(Logger.ERROR, "debug message with cause", testException);
+ assertEquals(Collections.emptyList(), actualMessages, "mismatch in logged messages");
+
StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- ex.printStackTrace(pw);
- String expectedTrace = sw.toString();
-
- logger.log(Logger.ERROR, "some message", null);
- logger.log(Logger.ERROR, "some message with trace", ex);
- assertEquals("", getMessageLines(messages), "message mismatch");
-
- enabledResult[0] = true;
- logger.log(Logger.ERROR, "some message", null);
- logger.log(Logger.ERROR, "some message with trace", ex);
- assertEquals(
- Logger.ERROR + ":some message\r\n" +
- Logger.ERROR + ":some message with trace" + System.lineSeparator() +
- expectedTrace +
- "", getMessageLines(messages), "message mismatch");
- }
-
- static String getMessageLines(LinkedList messages) {
- try {
- return messages.stream()
- .collect(Collectors.joining("\r\n"));
- }
- finally {
- messages.clear();
+ try (PrintWriter pw = new PrintWriter(sw, true)) {
+ testException.printStackTrace(pw);
}
+ List expectedMessages =
+ Arrays.asList(
+ Logger.ERROR + ":debug message",
+ Logger.ERROR + ":debug message with null cause",
+ Logger.ERROR + ":debug message with cause" + System.lineSeparator() + sw);
+
+ actualMessages.clear();
+ enabledResult[0] = true;
+ logger.log(Logger.ERROR, "debug message");
+ logger.log(Logger.ERROR, "debug message with null cause", null);
+ logger.log(Logger.ERROR, "debug message with cause", testException);
+ assertEquals(expectedMessages, actualMessages, "mismatch in logged messages");
}
}
diff --git a/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java b/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java
index f4b83046..65c33459 100644
--- a/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java
+++ b/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java
@@ -1,29 +1,28 @@
package com.jcraft.jsch;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
-import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.io.TempDir;
-import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -36,9 +35,9 @@ public class OpenSSH74ServerSigAlgsIT {
private static final int timeout = 2000;
private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest());
- private static final ListAppender jschAppender = getListAppender(JSch.class);
- private static final ListAppender sshdAppender =
- getListAppender(OpenSSH74ServerSigAlgsIT.class);
+ private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class);
+ private static final TestLogger sshdLogger =
+ TestLoggerFactory.getTestLogger(OpenSSH74ServerSigAlgsIT.class);
@TempDir public Path tmpDir;
private Path in;
@@ -79,7 +78,7 @@ public static void beforeAll() {
@BeforeEach
public void beforeEach() throws IOException {
if (sshdLogConsumer == null) {
- sshdLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(OpenSSH74ServerSigAlgsIT.class));
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
sshd.followOutput(sshdLogConsumer);
}
@@ -95,23 +94,21 @@ public void beforeEach() throws IOException {
}
hash = sha256sum.digestAsHex(in);
- jschAppender.list.clear();
- sshdAppender.list.clear();
- jschAppender.start();
- sshdAppender.start();
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
- @AfterEach
- public void afterEach() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.clear();
- sshdAppender.list.clear();
+ @AfterAll
+ public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
@Test
public void testServerSigAlgs() throws Exception {
- String algos = "ssh-rsa-sha512@ssh.com,ssh-rsa-sha384@ssh.com,ssh-rsa-sha256@ssh.com,ssh-rsa-sha224@ssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa";
+ String algos =
+ "ssh-rsa-sha512@ssh.com,ssh-rsa-sha384@ssh.com,ssh-rsa-sha256@ssh.com,ssh-rsa-sha224@ssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa";
JSch ssh = createRSAIdentity();
Session session = createSession(ssh);
session.setConfig("PubkeyAcceptedAlgorithms", algos);
@@ -120,10 +117,14 @@ public void testServerSigAlgs() throws Exception {
String expectedKex = "kex: host key algorithm: rsa-sha2-512";
String expectedExtInfo = "SSH_MSG_EXT_INFO received";
- String expectedServerSigAlgs = "server-sig-algs=";
- String expectedOpenSSH74Bug = "OpenSSH 7.4 detected: adding rsa-sha2-256 & rsa-sha2-512 to server-sig-algs";
- String expectedPubkeysKnown = "PubkeyAcceptedAlgorithms in server-sig-algs = \\[rsa-sha2-512, rsa-sha2-256, ssh-rsa\\]";
- String expectedPubkeysUnknown = "PubkeyAcceptedAlgorithms not in server-sig-algs = \\[ssh-rsa-sha512@ssh.com, ssh-rsa-sha384@ssh.com, ssh-rsa-sha256@ssh.com, ssh-rsa-sha224@ssh.com\\]";
+ String expectedServerSigAlgs =
+ "server-sig-algs=";
+ String expectedOpenSSH74Bug =
+ "OpenSSH 7.4 detected: adding rsa-sha2-256 & rsa-sha2-512 to server-sig-algs";
+ String expectedPubkeysKnown =
+ "PubkeyAcceptedAlgorithms in server-sig-algs = \\[rsa-sha2-512, rsa-sha2-256, ssh-rsa\\]";
+ String expectedPubkeysUnknown =
+ "PubkeyAcceptedAlgorithms not in server-sig-algs = \\[ssh-rsa-sha512@ssh.com, ssh-rsa-sha384@ssh.com, ssh-rsa-sha256@ssh.com, ssh-rsa-sha224@ssh.com\\]";
String expectedPreauth = "rsa-sha2-512 preauth success";
String expectedAuth = "rsa-sha2-512 auth success";
checkLogs(expectedKex);
@@ -148,7 +149,7 @@ 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]));
+ return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
private Session createSession(JSch ssh) throws Exception {
@@ -168,8 +169,6 @@ private void doSftp(Session session, boolean debugException) throws Exception {
sftp.get("/root/test", out.toString());
sftp.disconnect();
session.disconnect();
- jschAppender.stop();
- sshdAppender.stop();
} catch (Exception e) {
if (debugException) {
printInfo();
@@ -181,20 +180,22 @@ private void doSftp(Session session, boolean debugException) throws Exception {
assertEquals(hash, sha256sum.digestAsHex(out));
}
- private static void printInfo() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
- sshdAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
System.out.println("");
System.out.println("");
System.out.println("");
}
- private static void checkLogs(String expected) {
+ private void checkLogs(String expected) {
Optional actualJsch =
- jschAppender.list.stream()
- .map(ILoggingEvent::getFormattedMessage)
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
.filter(msg -> msg.matches(expected))
.findFirst();
try {
@@ -208,11 +209,4 @@ private static void checkLogs(String expected) {
private String getResourceFile(String fileName) {
return ResourceUtil.getResourceFile(getClass(), fileName);
}
-
- private static ListAppender getListAppender(Class> clazz) {
- Logger logger = (Logger) LoggerFactory.getLogger(clazz);
- ListAppender listAppender = new ListAppender2<>();
- logger.addAppender(listAppender);
- return listAppender;
- }
}
diff --git a/src/test/java/com/jcraft/jsch/PktSize128IT.java b/src/test/java/com/jcraft/jsch/PktSize128IT.java
new file mode 100644
index 00000000..29c05418
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/PktSize128IT.java
@@ -0,0 +1,55 @@
+package com.jcraft.jsch;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class PktSize128IT extends AbstractBufferMargin {
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ // 16 byte tag (MAC doesn't matter)
+ "aes128-gcm@openssh.com,hmac-sha1,none",
+ "aes128-gcm@openssh.com,hmac-sha1,zlib@openssh.com",
+ // 12 byte MAC
+ "aes128-ctr,hmac-md5-96,none",
+ "aes128-ctr,hmac-md5-96,zlib@openssh.com",
+ // 16 byte MAC
+ "aes128-ctr,hmac-md5,none",
+ "aes128-ctr,hmac-md5,zlib@openssh.com",
+ // 20 byte MAC
+ "aes128-ctr,hmac-sha1,none",
+ "aes128-ctr,hmac-sha1,zlib@openssh.com"
+ })
+ public void testSftp(String cipher, String mac, String compression) throws Exception {
+ doTestSftp(cipher, mac, compression);
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ // 16 byte tag (MAC doesn't matter)
+ "aes128-gcm@openssh.com,hmac-sha1,none",
+ "aes128-gcm@openssh.com,hmac-sha1,zlib@openssh.com",
+ // 12 byte MAC
+ "aes128-ctr,hmac-md5-96,none",
+ "aes128-ctr,hmac-md5-96,zlib@openssh.com",
+ // 16 byte MAC
+ "aes128-ctr,hmac-md5,none",
+ "aes128-ctr,hmac-md5,zlib@openssh.com",
+ // 20 byte MAC
+ "aes128-ctr,hmac-sha1,none",
+ "aes128-ctr,hmac-sha1,zlib@openssh.com",
+ // 32 byte MAC
+ "aes128-ctr,hmac-sha2-256,none",
+ "aes128-ctr,hmac-sha2-256,zlib@openssh.com"
+ })
+ public void testScp(String cipher, String mac, String compression) throws Exception {
+ doTestScp(cipher, mac, compression);
+ }
+
+ @Override
+ protected int maxPktSize() {
+ return 128;
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/PktSize160IT.java b/src/test/java/com/jcraft/jsch/PktSize160IT.java
new file mode 100644
index 00000000..60fca96b
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/PktSize160IT.java
@@ -0,0 +1,34 @@
+package com.jcraft.jsch;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class PktSize160IT extends AbstractBufferMargin {
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ // 32 byte MAC
+ "aes128-ctr,hmac-sha2-256,none",
+ "aes128-ctr,hmac-sha2-256,zlib@openssh.com"
+ })
+ public void testSftp(String cipher, String mac, String compression) throws Exception {
+ doTestSftp(cipher, mac, compression);
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ // 64 byte MAC
+ "aes128-ctr,hmac-sha2-512,none",
+ "aes128-ctr,hmac-sha2-512,zlib@openssh.com"
+ })
+ public void testScp(String cipher, String mac, String compression) throws Exception {
+ doTestScp(cipher, mac, compression);
+ }
+
+ @Override
+ protected int maxPktSize() {
+ return 160;
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/PktSize192IT.java b/src/test/java/com/jcraft/jsch/PktSize192IT.java
new file mode 100644
index 00000000..7ed19b6c
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/PktSize192IT.java
@@ -0,0 +1,23 @@
+package com.jcraft.jsch;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class PktSize192IT extends AbstractBufferMargin {
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ // 64 byte MAC
+ "aes128-ctr,hmac-sha2-512,none",
+ "aes128-ctr,hmac-sha2-512,zlib@openssh.com"
+ })
+ public void testSftp(String cipher, String mac, String compression) throws Exception {
+ doTestSftp(cipher, mac, compression);
+ }
+
+ @Override
+ protected int maxPktSize() {
+ return 192;
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/PktSize32768IT.java b/src/test/java/com/jcraft/jsch/PktSize32768IT.java
new file mode 100644
index 00000000..7ff73b1b
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/PktSize32768IT.java
@@ -0,0 +1,64 @@
+package com.jcraft.jsch;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class PktSize32768IT extends AbstractBufferMargin {
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ // 16 byte tag (MAC doesn't matter)
+ "aes128-gcm@openssh.com,hmac-sha1,none",
+ "aes128-gcm@openssh.com,hmac-sha1,zlib@openssh.com",
+ // 12 byte MAC
+ "aes128-ctr,hmac-md5-96,none",
+ "aes128-ctr,hmac-md5-96,zlib@openssh.com",
+ // 16 byte MAC
+ "aes128-ctr,hmac-md5,none",
+ "aes128-ctr,hmac-md5,zlib@openssh.com",
+ // 20 byte MAC
+ "aes128-ctr,hmac-sha1,none",
+ "aes128-ctr,hmac-sha1,zlib@openssh.com",
+ // 32 byte MAC
+ "aes128-ctr,hmac-sha2-256,none",
+ "aes128-ctr,hmac-sha2-256,zlib@openssh.com",
+ // 64 byte MAC
+ "aes128-ctr,hmac-sha2-512,none",
+ "aes128-ctr,hmac-sha2-512,zlib@openssh.com"
+ })
+ public void testSftp(String cipher, String mac, String compression) throws Exception {
+ doTestSftp(cipher, mac, compression);
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ value = {
+ // 16 byte tag (MAC doesn't matter)
+ "aes128-gcm@openssh.com,hmac-sha1,none",
+ "aes128-gcm@openssh.com,hmac-sha1,zlib@openssh.com",
+ // 12 byte MAC
+ "aes128-ctr,hmac-md5-96,none",
+ "aes128-ctr,hmac-md5-96,zlib@openssh.com",
+ // 16 byte MAC
+ "aes128-ctr,hmac-md5,none",
+ "aes128-ctr,hmac-md5,zlib@openssh.com",
+ // 20 byte MAC
+ "aes128-ctr,hmac-sha1,none",
+ "aes128-ctr,hmac-sha1,zlib@openssh.com",
+ // 32 byte MAC
+ "aes128-ctr,hmac-sha2-256,none",
+ "aes128-ctr,hmac-sha2-256,zlib@openssh.com",
+ // 64 byte MAC
+ "aes128-ctr,hmac-sha2-512,none",
+ "aes128-ctr,hmac-sha2-512,zlib@openssh.com"
+ })
+ public void testScp(String cipher, String mac, String compression) throws Exception {
+ doTestScp(cipher, mac, compression);
+ }
+
+ @Override
+ protected int maxPktSize() {
+ return 32768;
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/SSHAgentIT.java b/src/test/java/com/jcraft/jsch/SSHAgentIT.java
index 31daa774..ee15674f 100644
--- a/src/test/java/com/jcraft/jsch/SSHAgentIT.java
+++ b/src/test/java/com/jcraft/jsch/SSHAgentIT.java
@@ -1,22 +1,22 @@
package com.jcraft.jsch;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.JRE.JAVA_16;
import static org.junit.jupiter.api.condition.OS.LINUX;
import static org.testcontainers.containers.BindMode.READ_WRITE;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
import com.sun.jna.platform.unix.LibC;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Base64;
import java.util.List;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
@@ -30,7 +30,6 @@
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -43,9 +42,10 @@ public class SSHAgentIT {
private static final int timeout = 2000;
private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest());
- private static final ListAppender jschAppender = getListAppender(JSch.class);
- private static final ListAppender sshdAppender = getListAppender(SSHAgentIT.class);
- private static final ListAppender sshAgentAppender = getListAppender(AgentProxy.class);
+ private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class);
+ private static final TestLogger sshdLogger = TestLoggerFactory.getTestLogger(SSHAgentIT.class);
+ private static final TestLogger sshAgentLogger =
+ TestLoggerFactory.getTestLogger(AgentProxy.class);
@TempDir public static Path tmpDir;
private static String testuid;
private static String testgid;
@@ -104,12 +104,12 @@ public static void beforeAll() throws IOException {
@BeforeEach
public void beforeEach() throws IOException {
if (sshdLogConsumer == null) {
- sshdLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(SSHAgentIT.class));
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
sshd.followOutput(sshdLogConsumer);
}
if (sshAgentLogConsumer == null) {
- sshAgentLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(AgentProxy.class));
+ sshAgentLogConsumer = new Slf4jLogConsumer(sshAgentLogger);
sshAgent.followOutput(sshAgentLogConsumer);
}
@@ -126,23 +126,13 @@ public void beforeEach() throws IOException {
}
hash = sha256sum.digestAsHex(in);
- jschAppender.list.clear();
- sshdAppender.list.clear();
- sshAgentAppender.list.clear();
- jschAppender.start();
- sshdAppender.start();
- sshAgentAppender.start();
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
+ sshAgentLogger.clearAll();
}
@AfterEach
public void afterEach() {
- jschAppender.stop();
- sshdAppender.stop();
- sshAgentAppender.stop();
- jschAppender.list.clear();
- sshdAppender.list.clear();
- sshAgentAppender.list.clear();
-
try {
Files.deleteIfExists(sshAgentSock);
} catch (IOException ignore) {
@@ -166,6 +156,11 @@ public void afterEach() {
@AfterAll
public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
+ sshAgentLogger.clearAll();
+
try {
Files.deleteIfExists(sshAgentSock.getParent());
} catch (IOException ignore) {
@@ -277,7 +272,8 @@ public void testDSAUnixDomainSocketFactory() throws Exception {
}
private JSch createRSAIdentity(USocketFactory factory) throws Exception {
- IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
+ IdentityRepository ir =
+ new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty");
HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub"));
@@ -290,7 +286,8 @@ private JSch createRSAIdentity(USocketFactory factory) throws Exception {
}
private JSch createECDSA256Identity(USocketFactory factory) throws Exception {
- IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
+ IdentityRepository ir =
+ new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty");
HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub"));
@@ -303,7 +300,8 @@ private JSch createECDSA256Identity(USocketFactory factory) throws Exception {
}
private JSch createECDSA384Identity(USocketFactory factory) throws Exception {
- IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
+ IdentityRepository ir =
+ new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty");
HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub"));
@@ -317,7 +315,8 @@ private JSch createECDSA384Identity(USocketFactory factory) throws Exception {
}
private JSch createECDSA521Identity(USocketFactory factory) throws Exception {
- IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
+ IdentityRepository ir =
+ new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty");
HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub"));
@@ -331,7 +330,8 @@ private JSch createECDSA521Identity(USocketFactory factory) throws Exception {
}
private JSch createDSAIdentity(USocketFactory factory) throws Exception {
- IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
+ IdentityRepository ir =
+ new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty");
HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub"));
@@ -344,7 +344,8 @@ private JSch createDSAIdentity(USocketFactory factory) throws Exception {
}
private JSch createEd25519Identity(USocketFactory factory) throws Exception {
- IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
+ IdentityRepository ir =
+ new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock));
assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty");
HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub"));
@@ -361,7 +362,7 @@ 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]));
+ return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
private Session createSession(JSch ssh) throws Exception {
@@ -381,9 +382,6 @@ private void doSftp(Session session, boolean debugException) throws Exception {
sftp.get("/root/test", out.toString());
sftp.disconnect();
session.disconnect();
- jschAppender.stop();
- sshdAppender.stop();
- sshAgentAppender.stop();
} catch (Exception e) {
if (debugException) {
printInfo();
@@ -395,13 +393,16 @@ private void doSftp(Session session, boolean debugException) throws Exception {
assertEquals(hash, sha256sum.digestAsHex(out));
}
- private static void printInfo() {
- jschAppender.stop();
- sshdAppender.stop();
- sshAgentAppender.stop();
- jschAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
- sshdAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
- sshAgentAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshAgentLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
System.out.println("");
System.out.println("");
System.out.println("");
@@ -410,11 +411,4 @@ private static void printInfo() {
private String getResourceFile(String fileName) {
return ResourceUtil.getResourceFile(getClass(), fileName);
}
-
- private static ListAppender getListAppender(Class> clazz) {
- Logger logger = (Logger) LoggerFactory.getLogger(clazz);
- ListAppender listAppender = new ListAppender2<>();
- logger.addAppender(listAppender);
- return listAppender;
- }
}
diff --git a/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java b/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java
index 095d9789..0096c754 100644
--- a/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java
+++ b/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java
@@ -1,28 +1,27 @@
package com.jcraft.jsch;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
-import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
-import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -34,9 +33,9 @@ public class ServerSigAlgsIT {
private static final int timeout = 2000;
private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest());
- private static final ListAppender jschAppender = getListAppender(JSch.class);
- private static final ListAppender sshdAppender =
- getListAppender(ServerSigAlgsIT.class);
+ private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class);
+ private static final TestLogger sshdLogger =
+ TestLoggerFactory.getTestLogger(ServerSigAlgsIT.class);
@TempDir public Path tmpDir;
private Path in;
@@ -77,7 +76,7 @@ public static void beforeAll() {
@BeforeEach
public void beforeEach() throws IOException {
if (sshdLogConsumer == null) {
- sshdLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(ServerSigAlgsIT.class));
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
sshd.followOutput(sshdLogConsumer);
}
@@ -93,23 +92,21 @@ public void beforeEach() throws IOException {
}
hash = sha256sum.digestAsHex(in);
- jschAppender.list.clear();
- sshdAppender.list.clear();
- jschAppender.start();
- sshdAppender.start();
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
- @AfterEach
- public void afterEach() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.clear();
- sshdAppender.list.clear();
+ @AfterAll
+ public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
}
@Test
public void testServerSigAlgs() throws Exception {
- String algos = "ssh-rsa-sha512@ssh.com,ssh-rsa-sha384@ssh.com,ssh-rsa-sha256@ssh.com,ssh-rsa-sha224@ssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa";
+ String algos =
+ "ssh-rsa-sha512@ssh.com,ssh-rsa-sha384@ssh.com,ssh-rsa-sha256@ssh.com,ssh-rsa-sha224@ssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa";
JSch ssh = createRSAIdentity();
Session session = createSession(ssh);
session.setConfig("PubkeyAcceptedAlgorithms", algos);
@@ -118,9 +115,12 @@ public void testServerSigAlgs() throws Exception {
String expectedKex = "kex: host key algorithm: rsa-sha2-512";
String expectedExtInfo = "SSH_MSG_EXT_INFO received";
- String expectedServerSigAlgs = "server-sig-algs=";
- String expectedPubkeysKnown = "PubkeyAcceptedAlgorithms in server-sig-algs = \\[rsa-sha2-512, rsa-sha2-256, ssh-rsa\\]";
- String expectedPubkeysUnknown = "PubkeyAcceptedAlgorithms not in server-sig-algs = \\[ssh-rsa-sha512@ssh.com, ssh-rsa-sha384@ssh.com, ssh-rsa-sha256@ssh.com, ssh-rsa-sha224@ssh.com\\]";
+ String expectedServerSigAlgs =
+ "server-sig-algs=";
+ String expectedPubkeysKnown =
+ "PubkeyAcceptedAlgorithms in server-sig-algs = \\[rsa-sha2-512, rsa-sha2-256, ssh-rsa\\]";
+ String expectedPubkeysUnknown =
+ "PubkeyAcceptedAlgorithms not in server-sig-algs = \\[ssh-rsa-sha512@ssh.com, ssh-rsa-sha384@ssh.com, ssh-rsa-sha256@ssh.com, ssh-rsa-sha224@ssh.com\\]";
String expectedPreauth = "rsa-sha2-512 preauth success";
String expectedAuth = "rsa-sha2-512 auth success";
checkLogs(expectedKex);
@@ -134,7 +134,8 @@ public void testServerSigAlgs() throws Exception {
@Test
public void testNoServerSigAlgs() throws Exception {
- String algos = "ssh-rsa-sha512@ssh.com,ssh-rsa-sha384@ssh.com,ssh-rsa-sha256@ssh.com,ssh-rsa-sha224@ssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa";
+ String algos =
+ "ssh-rsa-sha512@ssh.com,ssh-rsa-sha384@ssh.com,ssh-rsa-sha256@ssh.com,ssh-rsa-sha224@ssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa";
JSch ssh = createRSAIdentity();
Session session = createSession(ssh);
session.setConfig("enable_server_sig_algs", "no");
@@ -143,7 +144,8 @@ public void testNoServerSigAlgs() throws Exception {
doSftp(session, true);
String expectedKex = "kex: host key algorithm: rsa-sha2-512";
- String expectedPubkeysNoServerSigs = String.format("No server-sig-algs found, using PubkeyAcceptedAlgorithms = %s", algos);
+ String expectedPubkeysNoServerSigs =
+ String.format("No server-sig-algs found, using PubkeyAcceptedAlgorithms = %s", algos);
String expectedPreauthFail1 = "ssh-rsa-sha512@ssh.com preauth failure";
String expectedPreauthFail2 = "ssh-rsa-sha384@ssh.com preauth failure";
String expectedPreauthFail3 = "ssh-rsa-sha256@ssh.com preauth failure";
@@ -171,7 +173,7 @@ 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]));
+ return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
private Session createSession(JSch ssh) throws Exception {
@@ -191,8 +193,6 @@ private void doSftp(Session session, boolean debugException) throws Exception {
sftp.get("/root/test", out.toString());
sftp.disconnect();
session.disconnect();
- jschAppender.stop();
- sshdAppender.stop();
} catch (Exception e) {
if (debugException) {
printInfo();
@@ -204,20 +204,22 @@ private void doSftp(Session session, boolean debugException) throws Exception {
assertEquals(hash, sha256sum.digestAsHex(out));
}
- private static void printInfo() {
- jschAppender.stop();
- sshdAppender.stop();
- jschAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
- sshdAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println);
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
System.out.println("");
System.out.println("");
System.out.println("");
}
- private static void checkLogs(String expected) {
+ private void checkLogs(String expected) {
Optional actualJsch =
- jschAppender.list.stream()
- .map(ILoggingEvent::getFormattedMessage)
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
.filter(msg -> msg.matches(expected))
.findFirst();
try {
@@ -231,11 +233,4 @@ private static void checkLogs(String expected) {
private String getResourceFile(String fileName) {
return ResourceUtil.getResourceFile(getClass(), fileName);
}
-
- private static ListAppender getListAppender(Class> clazz) {
- Logger logger = (Logger) LoggerFactory.getLogger(clazz);
- ListAppender listAppender = new ListAppender2<>();
- logger.addAppender(listAppender);
- return listAppender;
- }
}
diff --git a/src/test/java/com/jcraft/jsch/Slf4jLoggerTest.java b/src/test/java/com/jcraft/jsch/Slf4jLoggerTest.java
index 1e410b9a..056361e1 100644
--- a/src/test/java/com/jcraft/jsch/Slf4jLoggerTest.java
+++ b/src/test/java/com/jcraft/jsch/Slf4jLoggerTest.java
@@ -2,93 +2,82 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import java.util.LinkedList;
+
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import ch.qos.logback.classic.Level;
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.classic.spi.IThrowableProxy;
-import ch.qos.logback.core.AppenderBase;
-
-class Slf4jLoggerTest {
- private LinkedList messages;
- private Exception testException = new Exception("dummy exception");
- private ThrowableProxyConverter tpc = new ThrowableProxyConverter();
+import uk.org.lidalia.slf4jext.ConventionalLevelHierarchy;
+
+public class Slf4jLoggerTest {
+
+ private static final TestLogger logger = TestLoggerFactory.getTestLogger(JSch.class);
+
+ private final Exception testException = new Exception("dummy exception");
@BeforeEach
- void resetLogger() {
- Logger logger = LoggerFactory.getLogger(getClass());
- assertNotNull(logger, "logger should not be null");
- assertEquals("ch.qos.logback.classic.Logger", logger.getClass().getName(), "we need logback as backend for slf4j to test");
- ch.qos.logback.classic.Logger lbLogger = (ch.qos.logback.classic.Logger) logger;
- lbLogger.iteratorForAppenders().forEachRemaining(lbLogger::detachAppender);
- messages = new LinkedList<>();
-
- tpc.start();
+ public void beforeEach() {
+ logger.clearAll();
}
-
+
+ @AfterAll
+ public static void afterAll() {
+ logger.clearAll();
+ }
+
@Test
- void testIsEnabled() {
- LoggerContext ct = (LoggerContext) LoggerFactory.getILoggerFactory();
- ch.qos.logback.classic.Logger lbLogger = ct.getLogger(getClass());
- lbLogger.addAppender(new TestAppender(messages));
- lbLogger.setLevel(Level.ALL);
- Slf4jLogger sl = new Slf4jLogger(lbLogger);
-
- assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
- assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
- assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
- assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
- assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
- assertTrue(sl.isEnabled(-1), "trace should be enabled");
-
- lbLogger.setLevel(Level.DEBUG);
+ public void testIsEnabled() {
+ Slf4jLogger sl = new Slf4jLogger();
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.DEBUG_LEVELS);
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertFalse(sl.isEnabled(-1), "trace should not be enabled");
-
- lbLogger.setLevel(Level.ERROR);
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.ERROR_LEVELS);
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should not be enabled");
assertFalse(sl.isEnabled(-1), "trace should not be enabled");
-
- lbLogger.setLevel(Level.INFO);
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.INFO_LEVELS);
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertFalse(sl.isEnabled(-1), "trace should not be enabled");
-
- lbLogger.setLevel(Level.OFF);
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.OFF_LEVELS);
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should not be enabled");
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should not be enabled");
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should not be enabled");
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should not be enabled");
assertFalse(sl.isEnabled(-1), "trace should not be enabled");
-
- lbLogger.setLevel(Level.TRACE);
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.TRACE_LEVELS);
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.INFO), "info should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertTrue(sl.isEnabled(-1), "trace should be enabled");
-
- lbLogger.setLevel(Level.WARN);
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.WARN_LEVELS);
assertFalse(sl.isEnabled(com.jcraft.jsch.Logger.DEBUG), "debug should not be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.ERROR), "error should be enabled");
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.FATAL), "fatal should be enabled");
@@ -96,60 +85,50 @@ void testIsEnabled() {
assertTrue(sl.isEnabled(com.jcraft.jsch.Logger.WARN), "warn should be enabled");
assertFalse(sl.isEnabled(-1), "trace should not be enabled");
}
-
+
@Test
- void testLogging() {
- LoggerContext ct = (LoggerContext) LoggerFactory.getILoggerFactory();
- ch.qos.logback.classic.Logger lbLogger = ct.getLogger(getClass());
- TestAppender app = new TestAppender(messages);
- app.setContext(ct);
- app.start();
- lbLogger.addAppender(app);
- lbLogger.setAdditive(false);
- lbLogger.setLevel(Level.ALL);
- Slf4jLogger sl = new Slf4jLogger(lbLogger);
-
+ public void testLogging() {
+ Slf4jLogger sl = new Slf4jLogger();
+
+ List expectedMessages =
+ Arrays.asList("debug message", "debug message with null cause", "debug message with cause");
+ List> expectedExceptions =
+ Arrays.asList(Optional.empty(), Optional.ofNullable(null), Optional.of(testException));
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.TRACE_LEVELS);
sl.log(-1, "debug message");
sl.log(-1, "debug message with null cause", null);
sl.log(-1, "debug message with cause", testException);
- assertEquals("TRACE: debug message (without cause)\r\n" +
- "TRACE: debug message with null cause (without cause)\r\n" +
- "TRACE: debug message with cause (with cause java.lang.Exception, dummy exception)", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
+ checkMessages(expectedMessages, expectedExceptions);
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.TRACE_LEVELS);
sl.log(com.jcraft.jsch.Logger.FATAL, "debug message");
sl.log(com.jcraft.jsch.Logger.FATAL, "debug message with null cause", null);
sl.log(com.jcraft.jsch.Logger.FATAL, "debug message with cause", testException);
- assertEquals("ERROR: debug message (without cause)\r\n" +
- "ERROR: debug message with null cause (without cause)\r\n" +
- "ERROR: debug message with cause (with cause java.lang.Exception, dummy exception)", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
-
- lbLogger.setLevel(Level.ERROR);
+ checkMessages(expectedMessages, expectedExceptions);
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.ERROR_LEVELS);
sl.log(-1, "debug message");
sl.log(-1, "debug message with null cause", null);
sl.log(-1, "debug message with cause", testException);
- assertEquals("", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
+ checkMessages(Collections.emptyList(), Collections.emptyList());
+
+ logger.setEnabledLevelsForAllThreads(ConventionalLevelHierarchy.ERROR_LEVELS);
sl.log(com.jcraft.jsch.Logger.FATAL, "debug message");
sl.log(com.jcraft.jsch.Logger.FATAL, "debug message with null cause", null);
sl.log(com.jcraft.jsch.Logger.FATAL, "debug message with cause", testException);
- assertEquals("ERROR: debug message (without cause)\r\n" +
- "ERROR: debug message with null cause (without cause)\r\n" +
- "ERROR: debug message with cause (with cause java.lang.Exception, dummy exception)", LoggerTest.getMessageLines(messages), "mismatch in logged messages");
+ checkMessages(expectedMessages, expectedExceptions);
}
-
- static class TestAppender extends AppenderBase {
- private LinkedList messages;
- TestAppender(LinkedList messages) {
- this.messages = messages;
- }
- @Override
- protected void append(ILoggingEvent eventObject) {
- try {
- IThrowableProxy thp = eventObject.getThrowableProxy();
-
- messages.add(eventObject.getLevel()+ ": " + eventObject.getMessage() + (thp == null ? " (without cause)" : " (with cause " + thp.getClassName() + ", " + thp.getMessage() + ")"));
- }
- catch(Exception e) {
- e.printStackTrace();
- }
- }
+
+ private void checkMessages(
+ List expectedMessages, List> expectedExceptions) {
+ List events = logger.getAllLoggingEvents();
+ logger.clearAll();
+ List actualMessages =
+ events.stream().map(LoggingEvent::getFormattedMessage).collect(Collectors.toList());
+ List> actualExceptions =
+ events.stream().map(LoggingEvent::getThrowable).collect(Collectors.toList());
+ assertEquals(expectedMessages, actualMessages, "mismatch in logged messages");
+ assertEquals(expectedExceptions, actualExceptions, "mismatch in logged exceptions");
}
}
diff --git a/src/test/java/com/jcraft/jsch/UserAuthIT.java b/src/test/java/com/jcraft/jsch/UserAuthIT.java
new file mode 100644
index 00000000..996c1783
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/UserAuthIT.java
@@ -0,0 +1,202 @@
+package com.jcraft.jsch;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Base64;
+import java.util.List;
+import java.util.Random;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers
+public class UserAuthIT {
+
+ private static final int timeout = 2000;
+ private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest());
+ private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class);
+ private static final TestLogger sshdLogger =
+ TestLoggerFactory.getTestLogger(ServerSigAlgsIT.class);
+
+ @TempDir public Path tmpDir;
+ private Path in;
+ private Path out;
+ private String hash;
+ private Slf4jLogConsumer sshdLogConsumer;
+
+ @Container
+ public GenericContainer> 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);
+
+ @BeforeAll
+ public static void beforeAll() {
+ JSch.setLogger(new Slf4jLogger());
+ }
+
+ @BeforeEach
+ public void beforeEach() throws IOException {
+ if (sshdLogConsumer == null) {
+ sshdLogConsumer = new Slf4jLogConsumer(sshdLogger);
+ sshd.followOutput(sshdLogConsumer);
+ }
+
+ in = tmpDir.resolve("in");
+ out = tmpDir.resolve("out");
+ Files.createFile(in);
+ try (OutputStream os = Files.newOutputStream(in)) {
+ byte[] data = new byte[1024];
+ for (int i = 0; i < 1024 * 100; i += 1024) {
+ new Random().nextBytes(data);
+ os.write(data);
+ }
+ }
+ hash = sha256sum.digestAsHex(in);
+
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ JSch.setLogger(null);
+ jschLogger.clearAll();
+ sshdLogger.clearAll();
+ }
+
+ @Test
+ public void testAuthNoneEnabledPubkeyAuthQueryEnabled() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_auth_none", "yes");
+ session.setConfig("enable_pubkey_auth_query", "yes");
+ doSftp(session, true);
+ }
+
+ @Test
+ public void testAuthNoneEnabledPubkeyAuthQueryDisabled() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_auth_none", "yes");
+ session.setConfig("enable_pubkey_auth_query", "no");
+ doSftp(session, true);
+ }
+
+ @Test
+ public void testAuthNoneDisabledPubkeyAuthQueryEnabled() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_auth_none", "no");
+ session.setConfig("enable_pubkey_auth_query", "yes");
+ doSftp(session, true);
+ }
+
+ @Test
+ public void testAuthNoneDisabledPubkeyAuthQueryDisabled() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_auth_none", "no");
+ session.setConfig("enable_pubkey_auth_query", "no");
+ doSftp(session, true);
+ }
+
+ 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, Base64.getDecoder().decode(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 void doSftp(Session session, boolean debugException) throws Exception {
+ assertDoesNotThrow(
+ () -> {
+ try {
+ session.setTimeout(timeout);
+ session.connect();
+ ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
+ sftp.connect(timeout);
+ sftp.put(in.toString(), "/root/test");
+ sftp.get("/root/test", out.toString());
+ sftp.disconnect();
+ session.disconnect();
+ } catch (Exception e) {
+ if (debugException) {
+ printInfo();
+ }
+ throw e;
+ }
+ });
+
+ assertEquals(1024L * 100L, Files.size(out));
+ assertEquals(hash, sha256sum.digestAsHex(out));
+ }
+
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ System.out.println("");
+ System.out.println("");
+ System.out.println("");
+ }
+
+ private String getResourceFile(String fileName) {
+ return ResourceUtil.getResourceFile(getClass(), fileName);
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/jzlib/Adler32Test.java b/src/test/java/com/jcraft/jsch/jzlib/Adler32Test.java
new file mode 100644
index 00000000..b3e09d95
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/jzlib/Adler32Test.java
@@ -0,0 +1,74 @@
+package com.jcraft.jsch.jzlib;
+
+import static com.jcraft.jsch.jzlib.Package.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class Adler32Test {
+ private Adler32 adler;
+
+ @BeforeEach
+ public void before() {
+ adler = new Adler32();
+ }
+
+ @AfterEach
+ public void after() {}
+
+ @Test
+ public void testAdler32IsCompatibleWithJavaUtilZipAdler32() {
+ byte[] buf1 = randombuf(1024);
+ java.util.zip.Adler32 juza = new java.util.zip.Adler32();
+ juza.update(buf1, 0, buf1.length);
+ long expected = juza.getValue();
+ long actual = getValue(Arrays.asList(buf1));
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testAdler32CanCopyItself() {
+ byte[] buf1 = randombuf(1024);
+ byte[] buf2 = randombuf(1024);
+
+ Adler32 adler1 = new Adler32();
+
+ adler1.update(buf1, 0, buf1.length);
+
+ Adler32 adler2 = adler1.copy();
+
+ adler1.update(buf2, 0, buf1.length);
+ adler2.update(buf2, 0, buf1.length);
+
+ long expected = adler1.getValue();
+ long actual = adler2.getValue();
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testAdler32CanCombineValues() {
+
+ byte[] buf1 = randombuf(1024);
+ byte[] buf2 = randombuf(1024);
+
+ long adler1 = getValue(Arrays.asList(buf1));
+ long adler2 = getValue(Arrays.asList(buf2));
+ long expected = getValue(Arrays.asList(buf1, buf2));
+
+ long actual = Adler32.combine(adler1, adler2, buf2.length);
+
+ assertEquals(expected, actual);
+ }
+
+ private synchronized long getValue(List buf) {
+ adler.reset();
+ buf.forEach(b -> adler.update(b, 0, b.length));
+ return adler.getValue();
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/jzlib/CRC32Test.java b/src/test/java/com/jcraft/jsch/jzlib/CRC32Test.java
new file mode 100644
index 00000000..b30acd83
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/jzlib/CRC32Test.java
@@ -0,0 +1,74 @@
+package com.jcraft.jsch.jzlib;
+
+import static com.jcraft.jsch.jzlib.Package.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class CRC32Test {
+ private CRC32 crc;
+
+ @BeforeEach
+ public void before() {
+ crc = new CRC32();
+ }
+
+ @AfterEach
+ public void after() {}
+
+ @Test
+ public void testCRC32IsCompatibleWithJavaUtilZipCRC32() {
+ byte[] buf1 = randombuf(1024);
+ java.util.zip.CRC32 juza = new java.util.zip.CRC32();
+ juza.update(buf1, 0, buf1.length);
+ long expected = juza.getValue();
+ long actual = getValue(Arrays.asList(buf1));
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testCRC2CanCopyItself() {
+ byte[] buf1 = randombuf(1024);
+ byte[] buf2 = randombuf(1024);
+
+ CRC32 crc1 = new CRC32();
+
+ crc1.update(buf1, 0, buf1.length);
+
+ CRC32 crc2 = crc1.copy();
+
+ crc1.update(buf2, 0, buf1.length);
+ crc2.update(buf2, 0, buf1.length);
+
+ long expected = crc1.getValue();
+ long actual = crc2.getValue();
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testCRC32CanCombineValues() {
+
+ byte[] buf1 = randombuf(1024);
+ byte[] buf2 = randombuf(1024);
+
+ long crc1 = getValue(Arrays.asList(buf1));
+ long crc2 = getValue(Arrays.asList(buf2));
+ long expected = getValue(Arrays.asList(buf1, buf2));
+
+ long actual = CRC32.combine(crc1, crc2, buf2.length);
+
+ assertEquals(expected, actual);
+ }
+
+ private synchronized long getValue(List buf) {
+ crc.reset();
+ buf.forEach(b -> crc.update(b, 0, b.length));
+ return crc.getValue();
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/jzlib/DeflateInflateTest.java b/src/test/java/com/jcraft/jsch/jzlib/DeflateInflateTest.java
new file mode 100644
index 00000000..9136135c
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/jzlib/DeflateInflateTest.java
@@ -0,0 +1,353 @@
+package com.jcraft.jsch.jzlib;
+
+import static com.jcraft.jsch.jzlib.JZlib.*;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class DeflateInflateTest {
+ private final int comprLen = 40000;
+ private final int uncomprLen = comprLen;
+ private byte[] compr;
+ private byte[] uncompr;
+
+ private Deflater deflater;
+ private Inflater inflater;
+ private int err;
+
+ @BeforeEach
+ public void before() {
+ compr = new byte[comprLen];
+ uncompr = new byte[uncomprLen];
+
+ deflater = new Deflater();
+ inflater = new Inflater();
+
+ err = Z_OK;
+ }
+
+ @AfterEach
+ public void after() {}
+
+ @Test
+ public void testDeflaterAndInflaterCanDeflateAndInflateDataInLargeBuffer() {
+ err = deflater.init(Z_BEST_SPEED);
+ assertEquals(Z_OK, err);
+
+ deflater.setInput(uncompr);
+ deflater.setOutput(compr);
+
+ err = deflater.deflate(Z_NO_FLUSH);
+ assertEquals(Z_OK, err);
+
+ assertEquals(0, deflater.avail_in);
+
+ deflater.params(Z_NO_COMPRESSION, Z_DEFAULT_STRATEGY);
+ deflater.setInput(compr);
+ deflater.avail_in = comprLen / 2;
+
+ err = deflater.deflate(Z_NO_FLUSH);
+ assertEquals(Z_OK, err);
+
+ deflater.params(Z_BEST_COMPRESSION, Z_FILTERED);
+ deflater.setInput(uncompr);
+ deflater.avail_in = uncomprLen;
+
+ err = deflater.deflate(Z_NO_FLUSH);
+ assertEquals(Z_OK, err);
+
+ err = deflater.deflate(JZlib.Z_FINISH);
+ assertEquals(Z_STREAM_END, err);
+
+ err = deflater.end();
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+
+ err = inflater.init();
+ assertEquals(Z_OK, err);
+
+ boolean loop = true;
+ while (loop) {
+ inflater.setOutput(uncompr);
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else assertEquals(Z_OK, err);
+ }
+
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ int total_out = (int) inflater.total_out;
+
+ assertEquals(2 * uncomprLen + comprLen / 2, total_out);
+ }
+
+ @Test
+ public void testDeflaterAndInflaterCanDeflateAndInflateDataInSmallBuffer() {
+ byte[] data = "hello, hello!".getBytes();
+
+ err = deflater.init(Z_DEFAULT_COMPRESSION);
+ assertEquals(Z_OK, err);
+
+ deflater.setInput(data);
+ deflater.setOutput(compr);
+
+ while (deflater.total_in < data.length && deflater.total_out < comprLen) {
+ deflater.avail_in = 1;
+ deflater.avail_out = 1;
+ err = deflater.deflate(Z_NO_FLUSH);
+ assertEquals(Z_OK, err);
+ }
+
+ do {
+ deflater.avail_out = 1;
+ err = deflater.deflate(Z_FINISH);
+ } while (err != Z_STREAM_END);
+
+ err = deflater.end();
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+ inflater.setOutput(uncompr);
+
+ err = inflater.init();
+ assertEquals(Z_OK, err);
+
+ boolean loop = true;
+ while (inflater.total_out < uncomprLen && inflater.total_in < comprLen && loop) {
+ inflater.avail_in = 1; // force small buffers
+ inflater.avail_out = 1; // force small buffers
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else assertEquals(Z_OK, err);
+ }
+
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ int total_out = (int) inflater.total_out;
+ byte[] actual = new byte[total_out];
+ System.arraycopy(uncompr, 0, actual, 0, total_out);
+
+ assertArrayEquals(data, actual);
+ }
+
+ @Test
+ public void testDeflaterAndInflaterSupportDictionary() {
+ byte[] hello = "hello".getBytes();
+ byte[] dictionary = "hello, hello!".getBytes();
+
+ err = deflater.init(Z_DEFAULT_COMPRESSION);
+ assertEquals(Z_OK, err);
+
+ deflater.setDictionary(dictionary, dictionary.length);
+ assertEquals(Z_OK, err);
+
+ long dictID = deflater.getAdler();
+
+ deflater.setInput(hello);
+ deflater.setOutput(compr);
+
+ err = deflater.deflate(Z_FINISH);
+ assertEquals(Z_STREAM_END, err);
+
+ err = deflater.end();
+ assertEquals(Z_OK, err);
+
+ err = inflater.init();
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+ inflater.setOutput(uncompr);
+
+ boolean loop = true;
+ do {
+ err = inflater.inflate(JZlib.Z_NO_FLUSH);
+ switch (err) {
+ case Z_STREAM_END:
+ loop = false;
+ break;
+ case Z_NEED_DICT:
+ assertEquals(inflater.getAdler(), dictID);
+ err = inflater.setDictionary(dictionary, dictionary.length);
+ assertEquals(Z_OK, err);
+ break;
+ default:
+ assertEquals(Z_OK, err);
+ break;
+ }
+ } while (loop);
+
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ int total_out = (int) inflater.total_out;
+ byte[] actual = new byte[total_out];
+ System.arraycopy(uncompr, 0, actual, 0, total_out);
+
+ assertArrayEquals(hello, actual);
+ }
+
+ @Test
+ public void testDeflaterAndInflaterSupportSync() {
+ byte[] hello = "hello".getBytes();
+
+ err = deflater.init(Z_DEFAULT_COMPRESSION);
+ assertEquals(Z_OK, err);
+
+ deflater.setInput(hello);
+ deflater.avail_in = 3;
+ deflater.setOutput(compr);
+
+ err = deflater.deflate(Z_FULL_FLUSH);
+ assertEquals(Z_OK, err);
+
+ compr[3] = (byte) (compr[3] + 1);
+ deflater.avail_in = hello.length - 3;
+
+ err = deflater.deflate(Z_FINISH);
+ assertEquals(Z_STREAM_END, err);
+ int comprLen = (int) deflater.total_out;
+
+ err = deflater.end();
+ assertEquals(Z_OK, err);
+
+ err = inflater.init();
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+ inflater.avail_in = 2;
+
+ inflater.setOutput(uncompr);
+
+ err = inflater.inflate(JZlib.Z_NO_FLUSH);
+ assertEquals(Z_OK, err);
+
+ inflater.avail_in = comprLen - 2;
+ err = inflater.sync();
+
+ err = inflater.inflate(Z_FINISH);
+ assertEquals(Z_DATA_ERROR, err);
+
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ int total_out = (int) inflater.total_out;
+ byte[] actual = new byte[total_out];
+ System.arraycopy(uncompr, 0, actual, 0, total_out);
+
+ assertEquals(new String(hello), "hel" + new String(actual));
+ }
+
+ @Test
+ public void testInflaterCanInflateGzipData() {
+ byte[] hello = "foo".getBytes();
+ byte[] data = {
+ (byte) 0x1f,
+ (byte) 0x8b,
+ (byte) 0x08,
+ (byte) 0x18,
+ (byte) 0x08,
+ (byte) 0xeb,
+ (byte) 0x7a,
+ (byte) 0x0b,
+ (byte) 0x00,
+ (byte) 0x0b,
+ (byte) 0x58,
+ (byte) 0x00,
+ (byte) 0x59,
+ (byte) 0x00,
+ (byte) 0x4b,
+ (byte) 0xcb,
+ (byte) 0xcf,
+ (byte) 0x07,
+ (byte) 0x00,
+ (byte) 0x21,
+ (byte) 0x65,
+ (byte) 0x73,
+ (byte) 0x8c,
+ (byte) 0x03,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00
+ };
+
+ err = inflater.init(15 + 32);
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(data);
+ inflater.setOutput(uncompr);
+
+ int comprLen = data.length;
+
+ boolean loop = true;
+ while (inflater.total_out < uncomprLen && inflater.total_in < comprLen && loop) {
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else assertEquals(Z_OK, err);
+ }
+
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ int total_out = (int) inflater.total_out;
+ byte[] actual = new byte[total_out];
+ System.arraycopy(uncompr, 0, actual, 0, total_out);
+
+ assertArrayEquals(hello, actual);
+ }
+
+ @Test
+ public void testInflaterAndDeflaterCanSupportGzipData() {
+ byte[] data = "hello, hello!".getBytes();
+
+ err = deflater.init(Z_DEFAULT_COMPRESSION, 15 + 16);
+ assertEquals(Z_OK, err);
+
+ deflater.setInput(data);
+ deflater.setOutput(compr);
+
+ while (deflater.total_in < data.length && deflater.total_out < comprLen) {
+ deflater.avail_in = 1;
+ deflater.avail_out = 1;
+ err = deflater.deflate(Z_NO_FLUSH);
+ assertEquals(Z_OK, err);
+ }
+
+ do {
+ deflater.avail_out = 1;
+ err = deflater.deflate(Z_FINISH);
+ } while (err != Z_STREAM_END);
+
+ err = deflater.end();
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+ inflater.setOutput(uncompr);
+
+ err = inflater.init(15 + 32);
+ assertEquals(Z_OK, err);
+
+ boolean loop = true;
+ while (inflater.total_out < uncomprLen && inflater.total_in < comprLen && loop) {
+ inflater.avail_in = 1; // force small buffers
+ inflater.avail_out = 1; // force small buffers
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else assertEquals(Z_OK, err);
+ }
+
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ int total_out = (int) inflater.total_out;
+ byte[] actual = new byte[total_out];
+ System.arraycopy(uncompr, 0, actual, 0, total_out);
+
+ assertArrayEquals(data, actual);
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/jzlib/DeflaterInflaterStreamTest.java b/src/test/java/com/jcraft/jsch/jzlib/DeflaterInflaterStreamTest.java
new file mode 100644
index 00000000..703aaace
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/jzlib/DeflaterInflaterStreamTest.java
@@ -0,0 +1,126 @@
+package com.jcraft.jsch.jzlib;
+
+import static com.jcraft.jsch.jzlib.Package.*;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class DeflaterInflaterStreamTest {
+
+ @BeforeEach
+ public void before() {}
+
+ @AfterEach
+ public void after() {}
+
+ @Test
+ public void testDeflaterAndInflaterCanDeflateAndInflateDataOneByOne() throws IOException {
+ byte[] data1 = randombuf(1024);
+ byte[] buf = new byte[1];
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DeflaterOutputStream gos = new DeflaterOutputStream(baos);
+ readArray(data1, gos, buf);
+ gos.close();
+
+ ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
+ readIS(new InflaterInputStream(new ByteArrayInputStream(baos.toByteArray())), baos2, buf);
+ byte[] data2 = baos2.toByteArray();
+
+ assertEquals(data1.length, data2.length);
+ assertArrayEquals(data1, data2);
+ }
+
+ @Test
+ public void testDeflaterOutputStreamAndInflaterInputStreamCanDeflateAndInflate()
+ throws IOException {
+
+ for (int i = 1; i < 100; i += 3) {
+
+ byte[] buf = new byte[i];
+
+ byte[] data1 = randombuf(10240);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DeflaterOutputStream gos = new DeflaterOutputStream(baos);
+ readArray(data1, gos, buf);
+ gos.close();
+
+ ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
+ readIS(new InflaterInputStream(new ByteArrayInputStream(baos.toByteArray())), baos2, buf);
+ byte[] data2 = baos2.toByteArray();
+
+ assertEquals(data1.length, data2.length);
+ assertArrayEquals(data1, data2);
+ }
+ }
+
+ @Test
+ public void testDeflaterAndInflaterCanDeflateAndInflateNowrapData() throws IOException {
+
+ for (int i = 1; i < 100; i += 3) {
+
+ byte[] buf = new byte[i];
+
+ byte[] data1 = randombuf(10240);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Deflater deflater = new Deflater(JZlib.Z_DEFAULT_COMPRESSION, JZlib.DEF_WBITS, true);
+ DeflaterOutputStream gos = new DeflaterOutputStream(baos, deflater);
+ readArray(data1, gos, buf);
+ gos.close();
+
+ ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
+ Inflater inflater = new Inflater(JZlib.DEF_WBITS, true);
+ readIS(
+ new InflaterInputStream(new ByteArrayInputStream(baos.toByteArray()), inflater),
+ baos2,
+ buf);
+ byte[] data2 = baos2.toByteArray();
+
+ assertEquals(data1.length, data2.length);
+ assertArrayEquals(data1, data2);
+ }
+ }
+
+ @Test
+ public void testDeflaterAndInflaterCanDeflateAndInflateNowrapDataWithMaxWbits() {
+ byte[] buf = new byte[100];
+
+ Arrays.asList(
+ randombuf(10240),
+ "{\"color\":2,\"id\":\"EvLd4UG.CXjnk35o1e8LrYYQfHu0h.d*SqVJPoqmzXM::Ly::Snaps::Store::Commit\"}"
+ .getBytes())
+ .forEach(
+ uncheckedConsumer(
+ data1 -> {
+ Deflater deflater =
+ new Deflater(JZlib.Z_DEFAULT_COMPRESSION, JZlib.MAX_WBITS, true);
+
+ Inflater inflater = new Inflater(JZlib.MAX_WBITS, true);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DeflaterOutputStream gos = new DeflaterOutputStream(baos, deflater);
+ readArray(data1, gos, buf);
+ gos.close();
+
+ ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
+ readIS(
+ new InflaterInputStream(
+ new ByteArrayInputStream(baos.toByteArray()), inflater),
+ baos2,
+ buf);
+ byte[] data2 = baos2.toByteArray();
+
+ assertEquals(data1.length, data2.length);
+ assertArrayEquals(data1, data2);
+ }));
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/jzlib/Package.java b/src/test/java/com/jcraft/jsch/jzlib/Package.java
new file mode 100644
index 00000000..413a8e4a
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/jzlib/Package.java
@@ -0,0 +1,61 @@
+package com.jcraft.jsch.jzlib;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.util.Random;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class Package {
+ public static void readIS(InputStream is, OutputStream out, byte[] buf) throws IOException {
+ int i;
+ while ((i = is.read(buf)) != -1) {
+ out.write(buf, 0, i);
+ }
+ is.close();
+ }
+
+ public static void readArray(byte[] is, OutputStream out, byte[] buf) throws IOException {
+ readIS(new ByteArrayInputStream(is), out, buf);
+ }
+
+ public static byte[] randombuf(int n) {
+ Random random = new Random();
+ byte[] ret = new byte[n];
+ random.nextBytes(ret);
+ return ret;
+ }
+
+ public static Consumer uncheckedConsumer(IOConsumer consumer) {
+ return t -> {
+ try {
+ consumer.accept(t);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ };
+ }
+
+ public static Function uncheckedFunction(IOFunction function) {
+ return t -> {
+ try {
+ return function.apply(t);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ };
+ }
+
+ @FunctionalInterface
+ public interface IOConsumer {
+ void accept(T t) throws IOException;
+ }
+
+ @FunctionalInterface
+ public interface IOFunction {
+ R apply(T t) throws IOException;
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/jzlib/WrapperTypeTest.java b/src/test/java/com/jcraft/jsch/jzlib/WrapperTypeTest.java
new file mode 100644
index 00000000..f0693072
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/jzlib/WrapperTypeTest.java
@@ -0,0 +1,274 @@
+package com.jcraft.jsch.jzlib;
+
+import static com.jcraft.jsch.jzlib.JZlib.*;
+import static com.jcraft.jsch.jzlib.Package.*;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class WrapperTypeTest {
+ private final byte[] data = "hello, hello!".getBytes();
+
+ private final int comprLen = 40000;
+ private final int uncomprLen = comprLen;
+ private byte[] compr;
+ private byte[] uncompr;
+ private int err;
+
+ private final List cases =
+ Arrays.asList(
+ /* success fail */
+ new Case(W_ZLIB, Arrays.asList(W_ZLIB, W_ANY), Arrays.asList(W_GZIP, W_NONE)),
+ new Case(W_GZIP, Arrays.asList(W_GZIP, W_ANY), Arrays.asList(W_ZLIB, W_NONE)),
+ new Case(W_NONE, Arrays.asList(W_NONE, W_ANY), Arrays.asList(W_ZLIB, W_GZIP)));
+
+ @BeforeEach
+ public void before() {
+ compr = new byte[comprLen];
+ uncompr = new byte[uncomprLen];
+
+ err = Z_OK;
+ }
+
+ @AfterEach
+ public void after() {}
+
+ @Test
+ public void testDeflaterCanDetectDataTypeOfInput() {
+ byte[] buf = compr;
+
+ cases.forEach(
+ uncheckedConsumer(
+ c -> {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Deflater deflater = new Deflater(Z_DEFAULT_COMPRESSION, DEF_WBITS, 9, c.iflag);
+ DeflaterOutputStream gos = new DeflaterOutputStream(baos, deflater);
+ readArray(data, gos, buf);
+ gos.close();
+
+ byte[] deflated = baos.toByteArray();
+
+ c.good.stream()
+ .map(
+ uncheckedFunction(
+ w -> {
+ ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
+ Inflater inflater = new Inflater(w);
+ readIS(
+ new InflaterInputStream(
+ new ByteArrayInputStream(deflated), inflater),
+ baos2,
+ buf);
+ byte[] data1 = baos2.toByteArray();
+ assertEquals(data.length, data1.length);
+ assertArrayEquals(data, data1);
+ return new Tuple(
+ inflater.avail_in,
+ inflater.avail_out,
+ inflater.total_in,
+ inflater.total_out);
+ }))
+ .reduce(
+ (x, y) -> {
+ assertEquals(y, x);
+ return x;
+ });
+
+ c.bad.forEach(
+ uncheckedConsumer(
+ w -> {
+ ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
+ Inflater inflater = new Inflater(w);
+ assertThrows(
+ IOException.class,
+ () ->
+ readIS(
+ new InflaterInputStream(
+ new ByteArrayInputStream(deflated), inflater),
+ baos2,
+ buf));
+ }));
+ }));
+ }
+
+ @Test
+ public void testZStreamCanDetectDataTypeOfInput() {
+ cases.forEach(
+ c -> {
+ ZStream deflater = new ZStream();
+
+ err = deflater.deflateInit(Z_BEST_SPEED, DEF_WBITS, 9, c.iflag);
+ assertEquals(Z_OK, err);
+
+ deflate(deflater, data, compr);
+
+ c.good.forEach(
+ w -> {
+ ZStream inflater = inflate(compr, uncompr, w);
+ int total_out = (int) inflater.total_out;
+ assertEquals(new String(data), new String(uncompr, 0, total_out));
+ });
+
+ c.bad.forEach(
+ w -> {
+ inflate_fail(compr, uncompr, w);
+ });
+ });
+ }
+
+ @Test
+ public void testDeflaterCanSupportWbitsPlus32() {
+
+ Deflater deflater = new Deflater();
+ err = deflater.init(Z_BEST_SPEED, DEF_WBITS, 9);
+ assertEquals(Z_OK, err);
+
+ deflate(deflater, data, compr);
+
+ Inflater inflater = new Inflater();
+ err = inflater.init(DEF_WBITS + 32);
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+
+ boolean loop = true;
+ while (loop) {
+ inflater.setOutput(uncompr);
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else assertEquals(Z_OK, err);
+ }
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ int total_out = (int) inflater.total_out;
+ assertEquals(new String(data), new String(uncompr, 0, total_out));
+
+ deflater = new Deflater();
+ err = deflater.init(Z_BEST_SPEED, DEF_WBITS + 16, 9);
+ assertEquals(Z_OK, err);
+
+ deflate(deflater, data, compr);
+
+ inflater = new Inflater();
+ err = inflater.init(DEF_WBITS + 32);
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+
+ loop = true;
+ while (loop) {
+ inflater.setOutput(uncompr);
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else assertEquals(Z_OK, err);
+ }
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ total_out = (int) inflater.total_out;
+ assertEquals(new String(data), new String(uncompr, 0, total_out));
+ }
+
+ private void deflate(ZStream deflater, byte[] data, byte[] compr) {
+ deflater.setInput(data);
+ deflater.setOutput(compr);
+
+ err = deflater.deflate(JZlib.Z_FINISH);
+ assertEquals(Z_STREAM_END, err);
+
+ err = deflater.end();
+ assertEquals(Z_OK, err);
+ }
+
+ private ZStream inflate(byte[] compr, byte[] uncompr, WrapperType w) {
+ ZStream inflater = new ZStream();
+ err = inflater.inflateInit(w);
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+
+ boolean loop = true;
+ while (loop) {
+ inflater.setOutput(uncompr);
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else assertEquals(Z_OK, err);
+ }
+ err = inflater.end();
+ assertEquals(Z_OK, err);
+
+ return inflater;
+ }
+
+ private void inflate_fail(byte[] compr, byte[] uncompr, WrapperType w) {
+ ZStream inflater = new ZStream();
+
+ err = inflater.inflateInit(w);
+ assertEquals(Z_OK, err);
+
+ inflater.setInput(compr);
+
+ boolean loop = true;
+ while (loop) {
+ inflater.setOutput(uncompr);
+ err = inflater.inflate(Z_NO_FLUSH);
+ if (err == Z_STREAM_END) loop = false;
+ else {
+ assertEquals(Z_DATA_ERROR, err);
+ loop = false;
+ }
+ }
+ }
+
+ static class Case {
+ final WrapperType iflag;
+ final List good;
+ final List bad;
+
+ Case(WrapperType iflag, List good, List bad) {
+ this.iflag = iflag;
+ this.good = good;
+ this.bad = bad;
+ }
+ }
+
+ static class Tuple {
+ private final int a;
+ private final int b;
+ private final long c;
+ private final long d;
+
+ Tuple(int a, int b, long c, long d) {
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ this.d = d;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Tuple)) return false;
+ else if (a != ((Tuple) obj).a) return false;
+ else if (b != ((Tuple) obj).b) return false;
+ else if (c != ((Tuple) obj).c) return false;
+ else if (d != ((Tuple) obj).d) return false;
+ else return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(a, b, c, d);
+ }
+ }
+}
diff --git a/src/test/resources/Log4j2LoggerTest.xml b/src/test/resources/Log4j2LoggerTest.xml
new file mode 100644
index 00000000..31187168
--- /dev/null
+++ b/src/test/resources/Log4j2LoggerTest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/resources/docker/Dockerfile.asyncssh b/src/test/resources/docker/Dockerfile.asyncssh
index 0b34d900..734a0e6c 100644
--- a/src/test/resources/docker/Dockerfile.asyncssh
+++ b/src/test/resources/docker/Dockerfile.asyncssh
@@ -1,4 +1,5 @@
FROM python:3.9
+ARG MAX_PKTSIZE=32768
RUN pip install 'asyncssh[bcrypt]' && \
mkdir /root/.ssh && \
chmod 700 /root/.ssh
@@ -20,4 +21,5 @@ COPY ssh_host_dsa_key.pub /etc/ssh/
COPY authorized_keys /root/.ssh/
RUN chmod 600 /etc/ssh/ssh_*_key /root/.ssh/authorized_keys
RUN passwd -u root
+ENV MAX_PKTSIZE=${MAX_PKTSIZE}
ENTRYPOINT ["python", "/asyncsshd.py"]
diff --git a/src/test/resources/docker/asyncsshd.py b/src/test/resources/docker/asyncsshd.py
index d82f0fec..a76843b1 100755
--- a/src/test/resources/docker/asyncsshd.py
+++ b/src/test/resources/docker/asyncsshd.py
@@ -2,6 +2,7 @@
import asyncio
import asyncssh
+import os
import sys
@@ -11,8 +12,8 @@ def __init__(self, chan):
super().__init__(chan, chroot=root)
-async def start_server():
- await asyncssh.listen('', 22, sftp_factory=MySFTPServer, allow_scp=True,
+async def start_server(max_pktsize: int):
+ await asyncssh.listen('', 22, sftp_factory=MySFTPServer, allow_scp=True, max_pktsize=max_pktsize,
authorized_client_keys='/root/.ssh/authorized_keys',
server_host_keys=['/etc/ssh/ssh_host_ecdsa256_key', '/etc/ssh/ssh_host_ecdsa384_key',
'/etc/ssh/ssh_host_ecdsa521_key', '/etc/ssh/ssh_host_ed448_key',
@@ -51,8 +52,9 @@ async def start_server():
loop = asyncio.get_event_loop()
try:
- loop.run_until_complete(start_server())
-except (OSError, asyncssh.Error) as exc:
+ max_pktsize = int(os.getenv("MAX_PKTSIZE"))
+ loop.run_until_complete(start_server(max_pktsize))
+except (ValueError, OSError, asyncssh.Error) as exc:
sys.exit('Error starting server: ' + str(exc))
loop.run_forever()
diff --git a/src/test/resources/docker/authorized_keys.KeyPairIT b/src/test/resources/docker/authorized_keys.KeyPairIT
index afb8b812..fc007f26 100644
--- a/src/test/resources/docker/authorized_keys.KeyPairIT
+++ b/src/test/resources/docker/authorized_keys.KeyPairIT
@@ -13,3 +13,75 @@ ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBPKyvhX/
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAEhmKgKa4VuG9k/oiIGUZMLKwIGJ1rwKhHHZ/m4ZGhaWbvBApeDPkSj8WG0uz4o+LnQHD/i9rzWiRr5sd27quHErAHFzzLbUUoRvKpVjGEKWt+5yWoK9kkj/shi4Rv5UOiwZRr7dcHX+5L9ff1KpkxsGUD9mq6dVSVttU4Bpygw5PP5tw== test
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKHuAe5N1uLPUpY3t5kyYuISOxUobPZfK8H+CQaJTCALTMFrT63UDDYLyI2xroS67T2bWHkuhX1BHiTGP6JpwL8= test
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBPzeZvkHBTpjn1KGR0ZeW1mCEJhpL0TjvO3nei+16zUh6z3D6rs09rSbzwz9ovDzGd0SHN1vTeanm987Ebkpij4oIlypz3D6xSWXQZTQwKxB0JnOpccdeC7FxcLmHFFecg== test
+ssh-dss AAAAB3NzaC1kc3MAAAEBAO8/3Ymo6sXoRb7gfTBqwAYPO+ck8aKsoBdQIn2oD9cX2EIjXaoUXv/gK8tVN4EN0C8XADEFXpE3AQU5WhVZ11bv0AXOqGOreoP381pN72aphh9o989Ee2PET+wEXZnZ4PmftMcuhtHWC8Pq0coht7CO6Xt56ItQuvj3nxMwYc0hvEFDOpHaFCyqiann9q3W5bLB9+ek2tJg5s1AVQKnlGCwn1fj+FXY1cRFDsp7AxGJZPDSNjq4awEZwECG/QLQYiWT0FVILFv68SxQeGk6OlOGip4dKkdXPIhMqHy0GT0diBUm//UweV44S9IXPF9Xsiidq1Dn0vDe9e5OYNmV8kcAAAAVAKpXVF5GLMgMtlhSvjICPOy/A+T7AAABAQDEFLE91TMvZkCGeAHHXHkjF02m3CafJ7qLej8RoTEeZxV8Op/XlRluKNn97XF5G3DHp/DWGqBgawbBwqEq7l3V97zRHESMlivPr0UuCvjSmFR/on5nFCbl3RtZJOHOUkamtv51zBfk124uw7+bpnm9aiDNUs+AHNC3SLSyvdBbz6+sgt3kqD+FZyk+7TC/sOm+zuNmducFDDQIjldI7gx78b8D4BBx33+P5qW12PSlKGrQ7E1Rnk+o9WMSJosq/D7lV56waSV6/AJE9lruQr3LGEwN28DrvbVWen8KtvivGlJUwl1OK/hMBS5u+QZTIzN5M6iGB2hfJjBbF+LUONDNAAABAHBA/s/1ShLjRiIfenBfnInfjl3a3DSF9M3/o1fuY6EFkNs8MR4P2aeOfk0zE4gmO3vlT7TFs960tdUdIHm4TFOo+PFp4pjvVSO3K93cAjzOTwMI4sPhB3rRJxrDmx1CWVdnBrWQKO78kLpLf63/QB71qEe7MirH4FXZRLFP39gRcWknBtHmez92CFlzKzqLQXAMfaSbZ95lPh/bwgQ8OCuYExuB3aw3QmoQQdzSJRfWfuCEsUMmJ3Aub/yUMkpfe+hmQwkG2v730MXp/uDXfDXhZKkoh+v7Mhm04wzpxtB/C1dYpmfSP/i5ews4+9Lo9me6l54xy3+k05PPBBhnYzw= test
+ssh-dss AAAAB3NzaC1kc3MAAAEBAKU4cgWySBcfMzYy9aSOQwvH5JM1/M4UMNIB5BOZocC2FvJbEok0gTTbLCzGI9A0pe2bnr6EWx78v+aIH6p1GOmbhxZVY4+vkEUMH9Ee8v7XMYqz+CCefWWiAH2bXNa1v7LC9mImiuKmcDdHiVLJwrj/TH3c2EUYlcyQSUGRyOmiw5/auITPrAlmwhyB2ZXLC0X1qNKn+nJiwWxe+SsTXV6F38jbZo+fepbnVEqZ3wTUVnW2gpk0z+LRiLM+1mhASykpmRCyggxW7H2NyNGW+9jVTXD8WCqB/z0o+7PVCPS85qpyfNi6PhH+etXiYnyG8iGI19BWZxDmMCG8Sgr+AnEAAAAVALkJetYWk3MacexWEA0Xa78/o9RfAAABAQCeNE28P6R13v2OUqUtBe6oYQPHOHzMFdEk+qJoXyzKmz7QuQiCGuJ90YUST3fhvsBCOY6lPc6zGfeaVb/r2loa6zm3/0DoOXc/dnU4sbB0auohPlwmjFXDwTQF5BRyxuRWtvPuNgKlgwFd/GcnIwlgW/jRIF3j5wopM3m12+yhZnW+gh575hjvf9edpra7Y1u1VR/jYEQRg+GuXFDvcGE3OTWpphAe0zfjwqIOMYYvx/JQ4HMnB+FxJ4svs5aUOPwBXhMCT9YPWEWVr+1GBVf/VlKTrbapNODYEp0tuL2z8FvakgquJ2XGY74+6M1wbhrrDZ+LPd0Iuwcr7N0ghtVYAAABAEU47Jcr0BB3XEcmy/LsL+hSKudgmczV6QD+fxelS4mY3//8INn+wMRyQzgix4B5yGL6OzQ2RBGlqho2TdfLo5UOjIoQzOpZsjZu2TLZezS5Lpx85ZVuLWwqezx1NZqGDXhtDz2tdUV8eNbePWb5788WXYXOW43ya6Z5RMoywkO7sZRziHH4I+WwlorUpDMgzzfGs2j91bpejsBYcSnqcUDcHJedbMmcUQ53OWocK+/MwoYi8+F/UBuwAoZ8wYauasMZ/ph2k8ygzWHCv7NNteyVAY3o5OvrML4sfu9kmgBIZKKfGZHeFptjQdQcOj7fTI3tuqocwiWVohYBF7laKvI= test
+ssh-dss AAAAB3NzaC1kc3MAAAEBAIRPWjGVTl5F+y9uOPu8PPtFyjFy+Qclpqww/vdKq7GVAYPU5zdDHPvmEBZKfklfcROB5Ody3Bf7miR3RoLmmK9SZLDqEint+FdbvSWEooFYexZmYukhS9FeGUwttMOz4K4JIAPMVBxbGMPOL2AzPhE45BT2PPUpXMNZTLft2YHYhSIg30nsx+gc7tK7dkdY38pJ3Qtazc3jpyHnxT9o5qA1OdrA09nYHs9osneC4O29uru4Qz+foNP8XbGbkl1XxfooweQiElA9kvoOX+IgtJGgX2fuexAl0VcRmeb8lYxC0KOo9QL/HRW84nC8ZvwhiOXwbvN6X+tmKqTpcYQXgOcAAAAVAKearN3tBfL6HPzkCHzgGrUbSK1zAAABABW+7UNnhWAvSwiHShVSWm+eHjY56rss8OoYCvVRxDTLYhnZs8GvjlHJTTvuPbNMiWYe/vqwKhBDkNJe2cw2on58GsJwLLmdhsZzGRuCZ5twFLusZsjjRImjBTY5ZNYhdbhM4/527a7bSRLT+QPsedmaEvJHeSMHwDYwEb/WMsYnL7XnQTaPRjetz1a64zWIYQU6PTHW+HRrQDT2Dh/knymQvcw3QC5/ZwNf9k1HS8FdE5HC9G9eQfPCGjiFGLfVfAKbq6HslLZO4Ea8WnFLm5CHz3+mFF+1fHXkahGxlOkPGMr/kSeIvcneiMAqUDINC7VcXShVAvATGyaVh324uwoAAAEBAIM8p/hq3+3N5V885fGyBuO6H9RFmmFsuA5c70CXm3A7CHLNh1+ql72WzaRs3DyTICexOtEy/m+2jFd2suKaSa4F74OMQv4KaeeXrGuC8+EGdWFMN0Tf5UkvR/3igCFdxUbhfEa7qbMJBi5fOkkY1/OCldoHHWwWmp7uJA9oY7AVaFcwVyzOOQtUxBL6rtAE8Bcff88H2TFT2vf0WX/fKMKss69uzbphIIxnqnk152lA/gVWcT/mhCWa6NBIGJqKTNppKExTcLBcLlglqmLFi616A7gm8Y1+gi3dnDVu/hWiyz8uOSE31LB4oJGX4fkhOcKgwMJbB00F2ZGPT7JM4JU= test
+ssh-dss AAAAB3NzaC1kc3MAAAEBAOex/Q9Z8ZKXJbCIQnLHKuJdhGNpdIIOsmFYpNGCClPz4ZTZmk0Dbw7sn2KrshVAbi2RXAIz4gJn+MfHRF7okwgAgN94mMP6VG+D4B2ZpgyyqWRYSSdV5AmbKc2VDX+DVK9qKmHlEX3Wjm4Yfw3s1kGN1/eWwI5MrHB7w5tA8Fw8iUfsRLnwTyGPBXbR1ATqT/obLAic6vogcpGojg1s61k8Rlz2bVMaYRu/RQsIdxvUVi27jn00ClTaMGWKH1ZMhnmjedvN7qI7GcAUgNXFjOGa786aPXzJDC0JdNcmDoei9G/ynTJOpMMo8oy4bJLnVqL/kS8v5k+P4JeIhwvgp+MAAAAVALGpbWxdf1geBNR9INuHLoevUHjvAAABAQDijQTRDHymwb7TO5Lxy2P5MBvxlhTlRN4C+KIo5rnZMX+oFeEX5/hG18grUn7FYdx5/4AHP5EhK4GIcoKYJqnEjXvfSuvBvD2OxES8y9QI5Y0DZSGyAsV0p0/9N6a/khkdJ5Zs9S8iFeeJkevIVsCskLRkz6rrBbsuiXB/N3ojtetEQfFuznFVgEJyBNrk7QuVvu43nWnzYaF98ah2o4C4HdjAds57XzyhDks5kJsi1mryl5jvbCSxcf4iZW9OsOHYcjifwfuM+hz2oYYf87obEIIMdOtsvHjMEfTmBqechUqqsRP6wzv0u6g8rReMBldaoG2HFGpwjvMM+PGU/zdWAAABAAhLKqKGhmXzW3ZugZftycNwrxXFeVXFBywlC1qtDH8N/0aW4AvhWXBk2gw+rDXQudpNlVbtV9sT/+9JBM5iQ9txRfDjVeEzdOuXR2ch51p9Ep7KTYoSaBrBqL5KCxsz12G1+KJazMmlbpxQbdWYrAJrlRjTcHnYCDuhyUV/yCnrR+7VMAx5FyIduuRtYrCgo/+Z/Z9UklPU2596/pp7uozkCBR+RkOm7+9ldU+U9YiaUWymVmXhohFJvy9O6tqj3kZYQll2h7NnGpKVeIqp8wNW5BoCyVwV+fIuyCVsWg5vOKTBu5GR2Kc3AMc18Uq+WEeC/jsXZS53ItONrdNsQNk= test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsUXXjApfYYCtRK2LrymKvcZpS4cf6agyOj1/VW48g1GVtJM4g5kOjHWRqqsQZJCb8R9CvChCsJp4LPOS3tC36dSS52loeMoti7Z3GtKORjPGKDO/byt7V68l02KZdaO6U91D73a7y9v0g+JsLtR4xkZMd1zXLK3NnT43rtLg116YBsLxAxbAC8eeC1xBF7MNzdLbmg8Gp9dA6iNnDlUhDsf2/FJ8a43WbU8inaNJh6eGd6NtL1fb9iDWho+JEhw7WbyoNrBAf4YlqO+3Y0oO1A4ezOspv3Vx8CkoZTrtyCzVCPEIV/3wSK40VQaMxt4ptbPwQK9mSUI2dG3kuzkxV test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCXX5GPy2zMByhViM5aW+lVH3oPVBVvsfXjb7Afy2ldttHHkDMhoQKMNUScBTdyLwHrje6c0C99aYNkSl5TLXePWx4nIkNFrEyhOfooY8CHPTuhCjfy+4A/psxeE5aoI5YGfffSr0uOVOUdhV3X3p1SmQsybzSiFz0THULrtirxoCbf5a1UGNafYoXPvtSS3/qGJnsnbxE/gL2mTEvOyLI7o3ffFIgbfOranbuow/HSboX/Uc9i2FsbJR4i6K5A6KB3NIL3gFJHxzjumJB4QXt7mUXzo0FWnOXLORFrzYd1Rf26AVcUc24wwStVAOtXO3EIr/AylwgsqWqoStOzoRFB test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCdPnSRJDt/YuU5Oj1gq6JKYsJacWbgsdU+vpzAT7IiIVOoGB7oPi8EGcZSt1kIJBc9Lf70uRYCbCmq3v3EOLGpSdhV16oKQygSgSZawq2YwYHzHnonp6akLfQQXwE/nnubX3S6iYAaJSrJ50APhKfQXYsz4CH2alrzYb9Y2TDl4hKfxYABgMza3O02BFooK2tbxy2+0fbOKB/qjamGVE/CoMcv4ChB9FDGdUr0AVy7BYtfELAZ1dPrUh7lMCXa6JRRhFtdRWQGKmfM1nH/KpuzB+yZsl8kfHf47qiX/mwTQM87JMxm7HuV8pHHr7DNFs9g30CreF+hbY1Cx+IKh+eR test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCPJDCMNOM7Huv+NNeP626Slq54aPceMLEaoqlfvLJiQf11b8jXcKfFgLlp/f0Ltk10p+5hAGlzBw/nlYRU85i7Eq75kSb6LPcQ3SNY1/NkrNcZcFf4p/AKeC0s1qgbypvjWy7Zk8AUooPFrhIo/kKaPMouq4hcyENz2VKgpdJ21SzTxjDASj7zd9DZP8uc1Lsg3ftbr8Mi+MFFQRnFPLv/w2zIoLo6xwO0UplUIyAGAsRPDdRZQ50h9b5BFyFLnXUlwI/Jrpgs2J31Vg8J2j2Y3BFDgn/Mf/P9Rsvdo4fp/y+W195198VQ2gsH2+Az+3Id6ySsGhztBP5Pl2Z2K5Pt test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCo/5qxk4V6ZZGDFHDeVBr7ayBkc85+7SPuuY2OnBSrObqo06lw3nlH8dcloRzCt7qYvbmWTutlTujgSOJuvd3A= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAJ3Mot6VLEkkG5HscBLY3geI20FnwrgQi2wzAKiktPDhF39ZavVlOMzvkIGgQDxmo8WoQllY52ed4e8+7KQ76M= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCDseBAbWBVvO3A6gqqd2+Qm9uSTBPEFvvj/huI35w7iYhMmIjWAYwi9av867hF9GsRJaPGSkoWUIcA6QBXarAM= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKIrHi0RIW1gbd9+rJLTPJ5qAyXzNjEHidpxBzRnN22jDcnvBjqnAtfWPBjazs2KHSn/RP+KtyejY1Q+mMikDDk= test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBCTReLXxPo93rpl1xlrrx7yYZgD0DzRieTvFO9C3a8Z8blHuZHNy2iljsIYeCPOtK9lcivx+faOrp5FdE2VDroGPdDY2ZAuwF4/UxBiGNNEXWBhRFkupBcM6RbyjvCXaA== test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBRylqfIuL/Bzq92j3g4UHd3TOlU2aG1U/2W76hrf1svwNPNe5d81xXA6j7Uh+AxOYfYrJRQr9KotRvd8WcxbikMZs5O/dKG+xx8WmTcgvu+jVW4lCbZrZk4S7/hqIlVjw== test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBPATBzcVXx8g+unFg+8RAEvjiY/ZSb98vuIQ6MAtFCkNU2zgUHI9m/Jn9FitFd/F7KlIh4G5/wMB2dGhks/FYSk+OxgAmhjif6lKLfD5veRCLhRnVbhTQm1fOmU0Sy+FSA== test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBGxZq9s9JfIvKluYcWj/f+OuiV0bJ9RLE8rT+8qF0syMvzZZVRqLmF+a9VxJ4uxxRVvmMQ1CXb5TGtLVdBv1SrviKWMoaBw/sosSMr8v3eoQ04f4bs32fx8/dN4JQbCZEg== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACuthNYwDJn4I/b0fdhJdD3ykWe5gMBS91H9qWt0NHh4QTYxVyu3npbLveRVr9AWoF36UYIVF0Z3WjFV4u4fOPapQFNlGDf1DNn8aipy/5iUKY02z+AxnWIIjToN0IPPizMw3LApwmy0UaBSw7sZyfxNHD0Nu8wXsP8A6HG+Y01zSMAqg== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAEUP4llO/qJcKrD5gZ6L+8+JzEU5RLICud1jgVh+QgtuuAZQUztdee3GEPmR+C9Mv5cAHg138OCNcyRI1eukjFNDgAq3xeh0V6BwjZZur+v5zyTnXQASBwuI+iofZZqS0z8lsM48eIluTZuo3siTlv6uvFLs4ovN/cg67QyGIpsKusESQ== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGIVlZVYKq71fKe0Uxj73A5WPmvTuohTARpvnJ0VB5zIHFEtHRuI7Qkn6pRGv8rbKDRKygv1kZc6rFZ2fgfhLxXcQHG1LwWM8x+XXoWMJM0C0kpCEErKd/tCZstQqg4v2JO0iG5+wmoZ90Cru912t24fhFGfVeFIbLN/xqznIm0dZTN/Q== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACcIEokqqdXr1cB4vJksHNPuIb4HhwaRqaCi22yZa1slGr5+KQhWc6TaRP2yEsXaskuwThVy5hNjfONP/uB5IMj6QCxnR3bA/knuB2n7YRFlbIICLmCxr6QI5bz271MxCB+ccJO1L8sSfEsS6IcxH7O3rYv7hDadwcY9t5VmsJ3t7A67w== test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINknjInRhIeQB0yCD/N+ilxpTNwD2OGA776pYHI8bOdZ test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYLKsn61Pg/YYmumVe7oXik0o+Ca/P3vvN0Om3KCHqo test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHh90f1nUVIpoKQoYxtD66cTGgLZ33D3hCxVqS/N5R3u test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILhbcrIlE43dp+IlosYszkS0c4zpwC60ec1MRwZuxwvr test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnTDrr21+87QbcPF+2Ayh61ot/Jtf0c9yTy126ki8Um/UUEW6uy87s/9gpS0OHyoH3GO3WgsUoe/AA= test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnwdqOa0pIeeorEzsEY7kskGeyZfhpkYZtbA/iR5Nyp0cbPZnl9UFsfKQIYKJwPWwnim8BXTkEq6YA= test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADk5uIOJgx7OfFkbkRV43yMP37dcnIVemLPjlaH/WRKjLTwueFIgN5Mc4UYcIZtlbAaUhiiEvW7aYQA= test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADkVNtCwBIBWO41k+QYmuMromfii6QbjVKRxjxbTYGPDaw8u+gSCHAnqgaxi9qkc7HU0U01BJI5tJgA= test
+ssh-dss AAAAB3NzaC1kc3MAAAEBAM6Xy9zencF2KDhq1r0gTXBdA4AL5Rtg5GYd4tx6qlSxk3ZX634ude5bV8Io2UrsgvIDQHSfj+1J4sDpWnhMtT12KucddwAzLhb+/5gYEr1UCZtPn1WlsvOXrJ9pmAt/MB1KppyG38UX3QK6Z04oWspqHWJBfO0oquUUGx5Ua9JSneJCrzk++Sd1GkhjZElR51Hm7MKV9KZejn/tUZQK2FRjpfP50GsIlAs1v31OkY1glhgs90/6v7PyV1ljp27Nw1aNL+CxOjA/wYnFo+jE5+EsW81oT8C06n4yrcFySWFR96wOfb2fAuCl4K9Ma4jN+7J0RLMY1++KzCsAtppi4KMAAAAVAMbFE/Q0VL6xGj975l/7AqEVxWH7AAABAQCbU609B2d7d5EipWyjlETAs64vaKBXOAREcQr8HQfAkWmSUBr//brBdNWeA4TVla4hyKL1CBiP1Hi6pDQ12lCB+J30ikRqdi45y2MvOR8OfEDy3sn94sLwswARnMwdfHWrPepfyVdqOnIlhsYWzLpc5EhfOThvL8YZ78tPTCqyui8COi3TznOFfW/do0ydEEmfQwJTlHi6dpFiokh+mpQVOlstehmgKzrolLEjccS/vnxSqsdWDwKErbtg0bYhhV3rztiMcMozoBblgCSDiVmvGBAO8GnIoyX5/J+UfuwWYQKYrtuZRfB4hRyRU4zKopPfQrZkc1Jummk959KgfeTIAAABAQCayyiO1dYHPHF6g21swekxrukaXix6sab4m+OLm0bNRa+7nI3A09wG+otKajznna/mdjcIaWuhdG5S4+l+aPypIUWTdM0krUu0A+9Pu9YpIgVgEFKVQX9PBcP5+WHanMaeYmLOCEP1WqFMJ5HiRCJmssOKwZJygH/KEMIGaDGhYnSmN6R+41j8sDAoY9/00WlSTKqpmVGTe7bJGWomnhT08RsATIyvM2i/J13qrh5UfGprj6GsaE9q+Zjg47oMdZIRSgyp4jw9coQoR68TemOmkODmXaoixfrGT1FC9LbqOmN/iDwv0QvLUen3RTbbB2lE3LwObBXJRpd44RjeMo7c test
+ssh-dss AAAAB3NzaC1kc3MAAAEBALvd0uyCDZFuKcpphdcnMEYVXnoy16rW5XIFhlxmvVSUDDz3DAGyXISyhyQ43BhI8R0OeOr6VpivujFu0srXrcxcgsAy6yv/6uAvaeLfekc/JErS7OqVqACFmt5ISkEBcvWKnUseFRwoy2KCeiEo2GyKwyV00caLnYb1Th2CUNRFZcH5p5XLj3gwFbPf7EdkKV0PHWV4E5ptFVVT/aRijCJRQn//FlSNlG6iOfUF4IRUpz05qivVGaHLWNOw1fSXK1Pc53qQqCAaEict3arhQCwSEKl1xktSNBDVmeapLPj7LNcZIGvAA52Tpg3qR1rp/8a+bdtteNkbRg4Gfx2uBuMAAAAVAKFdLforDVztYQzh7pFjN8qAjogVAAABACTsvZOw0aPNwdQFiaQik1F08HQrjlp298vgbjIZFrgABE4xeuWMydEcxAKKEhy7Y9Go4l9shUddkHgk3Y+OLvAsifowqfc8Fpsqxh1WKLxnw9NO5rjenO+kNeiwhX/oWa1VBMOa/O+iGTvCy1XcW3u0E1LqGVAx3DdkP6EHV91ma3d1LWih/RR8ClERNaovnETash8NQCHAmKejIObneLtNRqyaiNohYK0t5EAtP4JutVwrsnj3ZoSKrCZct2wVrMUkkqCsKcKygCsr29uk1xpVYBkv9zUn6V199ZB/uXanRLcL/vq0UYWGV9nXU0shVanMwHNQaMk8oWnd6hTPB6EAAAEADwMN1uDvQqkyKel83xNG4Er3LsSCx6iARNWpaV1ZLNrtf2h6MnYCKdSdyVgRjIL5coBQB5yZb/+CXm9QgysH+K5FHXgnh4hjz8W8/Magj+qm3/SKDTmbLAsNvvvenQvayTWUGNQlfQET/tHvGRy7gz4r95phWGmxS8Zu1NkT0RHl6xxcSqWmvvjG/m2MuNo+5lfKFa2+j+Is59HwN3ZK5umpPlJl4uM5SkyFD+iUbptOVCXdVV4xRhIzYy4wllADQQHkbNQtNGJT7JEHuot92J3FWOCEQVw4F06j8NsdghKWIwGSlcHLHAbA8zZDq11R6vnhOFDwmfekxCwLXaBlGQ== test
+ssh-dss AAAAB3NzaC1kc3MAAAEBAOGfXe0SXEr7JIMEZQo6rkTc6CfPIWsgqDYem72p84pn6KykGD6RonyqDEfDoS0XpwIY2sCZLmwauEhTQtBZpMyix4sD49w+SjaHUP1aoAT7iPFvsXXGNw9/jgK5a67NzWxmcl5XMQG/VDjtVLWzNfl9OXHMSNmGUKQmlPZKXRrm06SYVZMDsRbgSJgi6PNOatuVvfR+b2hK+ppMpQNKnHXe/vpgcblZ1ViPUTH4vG+04PkidIbwHIIl5Yv69lUYOcycxR3yzd8ZC3a7qzVkxet+tGWCD4/PzfWEP+/G2TWXbRuxV6YEaq9POmI0JuZa7CAZ8iA/OFtYua3TB4cUsSsAAAAVAJzJoUVy/fnPi6UuPLR8cCyhqB3HAAABAQDH61jRVXoAqKJRHgIWhBsrQMmAvOqVHyrALSlAchNT5cso4e7Rd9ajJFg+hVZGKu5PoQdhp/8ERk8mnABKue6r9qO/6qT8NvCRuI0Wb8Sxgw5q/4hzo4BVdx4UhLqTRl9rF7B5wLSjV75HIsqW61ZcbT9+VdYpRaNg8/Te1jfqwgrTndaD7jhkhU2P0Z7vHSRlzaeSksF6b+SRFu+bnQ6neBxZZTuMrSfKOSpp2ltLUvWrQ48GJ0I/uE1HbQehz9Tn0Q3VA2BjpfKgj8bLE5YFokveuqjFULF7EiL4CmAt6qhiQOl/f6zKR4gaLUjkVuvU/uyTr/da7/FfOJouETpFAAABAERldAO0JWHLr1nClDX+ZPN8XbbpjSaWO+fFLQO0A7QE/2gKJ8lt5qi39GOOhmUO10johHaOupqRqe4iCRnBxMJeLxXmS2KbcwDke+IBRaid81ccocpcPOrpov582DjJuwZ0394Akyrc7uMonjBy+JJaPwpXHQS4SXRe7NybuEE4S+DsV6DNx9WG5n5PFh4YELbzdP2oJZ3hyZ2MiZgo02ShziEDPI1vQ4BBAsknp6Ubzt40Gt1yjyNMbGpv8IIRZzu+H8CIRnzbCYKT8mRKe0HvTG48c6la0/wvvG62zfmbFYgkw0tQxgekpf3ojNpRJZ1vcQPgtvQ5IjkCYSF/ss4= test
+ssh-dss AAAAB3NzaC1kc3MAAAEBAIYJqKE0jtG+HpbHurIu+kVIuQLrMrgXhL7woINHOHGfb/0O85u+3DsSy+s7f/M9bWUIqRtt2+JV2J/V28mDgF7IEeiP3OjSYspmoxnCy8d6g313NcYYoCqG9f/yWrzSXQtK3hD8+KNqm68GQx2L6lceyyHn3CgUFTwHw0MNntOfAk94PCoM5wOZCfqx9sZfTVk+uQb++aG8xcAqPoPQdTgaqrFa9lrp2Ul8RhM/eDpCmByg8I5tIsbk17IVzS0gLp9slfInhwr2UjX2wIOlVOnNmS8UHLAwhDtdtHcWYdFbEMpO9hFMJCz39JqnJ7g8qjct/bfMxAEqhwlGpgbYrz0AAAAVAL66GQDORVX2on1ic6drzCN/9f4ZAAABAA8eU3kzUacUj50ggFzpTLuBBztILvjIpNGnXy7SUG5EWCZXLvBPTjJf9idxL/0P42AlJqKpaZSsdDLeaPbyUsQ0B9R+eTVDKQd5po5kBmWcxqMJh0f1XU1CYFZgN2cvzmtyx9euekEka/j1mqGLuXHRpX0vak/wsy3T8mEPlqLr09c6hPUDG5U8NXEy3aWS5gRRj7wPDLAi1sFGo4ReUfjn6LJglbCi71tSirnV0dJJvVGpXA17RzetDgZmIjdDur6XdGz+6B07puj/OUTTCFSFzJC0wNKGS/OjY9+59ziFj+M7LCA7HUAlYeL4n83EsZp4fHI09JyuLJppPo/4xxAAAAEAU2ThrVHZ3fT8JuXT4dgJVtgoBTQVf1vbzKA/o5yrOCKj+pPLFv+mXSJxLWvdSUeHkaRZF76hCwSpy6K+Wt9sRSuL6qTPog8TT7Yhk0YgfMd/wXwYlhhoH6HqRIIQRHd1bIOn+n1DUWQHS2w6Ksd6jnVDd5cte+sBRNFeNjRJPUg2dzFqYQUgf5bfDu6/mhISduez7BctGb0cWtVPyjzQivJRUVnLiUJJE3V0saEC2FYOalknyW2x99tolUn4YwOLjUuQm1oKZ+BEbXlgGe4pKe/JxB3nYHT78wP7ArnhImPGK8b0+/JD4nAA15GEkFLRV/u9qyZJe/iwbpqKz2vB/Q== test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCclEvT+gGh6exRwmgPhsN+PLMpcNltztLQ1x8iKOsxCCbXWXRxfvvG/ye8lp4kEOWIZ1224UoG1401aNBVTGNUT7SVyAFSdnHG7AGgFnLwJcxrsEHLX/f+gj/m4syNJfAg1PrS4O8IeaEnTdql9x4cYkzd+Jpa/OHZLgltc8SHezhmxrc5Ylm1IMHu6gxJb0WwQ1mIZ44IAIaYvNflal7dP8v//bY+6kvS5iARk6hDfiR/wH3926r7U0pB3siNDdz5yq7rjvTg6igtOhQDSnd9Drp0yHjBCyQoiDqP1P2jhyfz1iDdf6u71CFhZK20rfbsUUHCjWilB00onvjjIDYJ test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCMTxeJiE+rerxDpf3UhJRVvaWulraY8VZryHhSPnHCODQw3MsQ0X6ECmTs8c3YeNRBRhiaB6zOQorUFqFbAwO2XIFQgDGk9qSJF1kwsVXKnReFqUQ4MQGjRt5odjHc4+00KnmfdLUubvLGJmzHYJ4Ia1+EEyzkUik4nVTxdreXrvFEIEej/QHJlhVSqsL2oKXKVMLAJ6OvGt9K1pFSJRCyedHVmR/z3tfsiFqpw0R2P2WhhkK9jXG47tJcG9v5HPrGJqd3D+W982/YUFNFvLMVkq5qU4wEDFVh1u7Z95dABtZm2TwzhrYfxdAIcYbZLpbFozWX+f2mEQRAbWws2YE7 test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCyu2eTD/3HRZnFhKMiy7YLGW7WUsT1uEgRCUufHFbjwZqSdUMXplB7wD5g/iombD36oZtFGpZWjiReT8hh2k2dR5pzF7Hg9yTZosuBu8YRSTjwicc/NHTcDfl5kQFSGz40azFXWn1xJqPRQesDGZ2rQVloCxW771AVS5hZvAybtM/qf6En7PxHJE4Eg7klvjH2bzhacDQch56u/ij+/ipwtqkWt40k6sMQ7xtRIQH4AUNlqQ+3qpZxKfAl+NCbJ/V8Mj9BM3WuyeD6DYPwBT1mHXug9KWi7aii2c/UvDv1xpF7u0mTRSW4Dkm7FTEbX8iPP1QF3ze87YEaF1qRkeMf test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCnTnSVyVw7nyqnxy/WWUUuFswXUsdwvNtojMVuqltV3CTDHfX6+uMu+XBC6Gqr/AzHvwzOPvzR+/k+m5fV5zLvjfe3zTU1gs5EmOtu3YbOnn6il/IvVvU9ILq/5Dg89aIbSYG9R28iu8KciZxHOOpjgFpmoCiMAg8x4OWRHc6irKj5VKTXMJC+5pmb4QZlSKal7KPqKyAsKII6IxMjpPNJejZm/Omw3QZ19YIN02ttbM8rwoLuPLi1DoZiAUvF7J3XTEUcDAn5y5upltk1Dx+bWhuth/sYZ26qksxPr+KmtHwJ5R8pWJ4CXtENlwxQiAWftVWW8MeQlyN17qPW+Z9x test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLoiF7se+l2KwZh9oxhfTASh5diPDNvYbVgS98Q4yPE9uExiDDMeX2Nu9XLtnevuTWrmcAF6u6/ozI3BIkeHiBQ= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGrSiZUMuXa3aTNDF/H+rTZb4wUBapMIf9QXzfoJI6hOYPcCtHGH8KChdeZ2U9W3ohzX/WZIX8IQ9fpxcGyvpbc= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDMTBUib+twute16i5Rs2zKlVDMam5/RMWuFcXs2dvIs0kLP5N06diHYHBqvMPT9N336KMChz0/O4S/dXR1pwHE= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMQNLejYwJxwVlAGIVi55Lb0zfkz18CEAwqDtrr6xs2sz+vqRU1xadOgEzHeWovBqCXdC9wVja202PyupH5QPsg= test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBnR7wyGL+IOT7A7vKr4Do1OGlsMhBFN28GuKUohpkd5rvvZfe/9UNbdobFa38KmLWpxzAlL9Go3AK6+7OpWz6HE8JHdrJGyRYoS87XVv4oIIzmp8B8GThUYiDt8Qh8FmA== test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBGdfKrIhLUFtiMfBOFdF1LulPAF3OKNqlahGbA0bf/LXPf7tmbJUs+GvozsqYAbsMhed1vmwfBwpaQdIG97JRSK4MK4yIrCjDjGwqmakX/hg/WUQZPi17463t9zSzFZ8xA== test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNShRribjCP+pkCkAMwdj2LjxNrZjIKHtylcTdohEgfHJTXzub4tbq6odNS3izPwgMEfX6M6Vl5NBIstk3c34+kDuJ/a+Cyn7lZ9HZPdDuaKE66PNp3zGK9linfzn3iFBA== test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKGDqZyyEHaKkg0ijDH9nRu86AiXmUNJcKWC05ksthEMheDRMyZtyqzIIwQI9Goum+Tktu3LeQS4fBh9aNHwMP2xN5uvj/uUYxN1dmQmLQ4NsQhpz+DY5TtxHJ/tUKvNpA== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADwW+/zw0OOBYVS2jEOym6OctYKqb1/uSTegImb7XOcklV6HZSnkGDrp89gzXL6AgoFWMQkySgR93l1j22kCwxGWQFQBLalq5ekPGpqBiE31QNot0QKPat2f4O8bw0DO5sUNPmH7ImKmyxAjHGiCPhi5egoIgrgzHoGnx1ynMr32Pntkg== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGCln1Y+fp/uQTIVhjoNHuuRGVSzHc8shdMTGUQz0q+TpDSEf+SViws/gS0+V73QggV5TcUqOIOra4uD8mGSieuUwHea4ieXw+cbcts9lpYdjWkFmgSGwYfIVyosFZ+aE07We7fNRqSUpH5gyBpdSpFKAiBTwtv73FOIwz88I82E4HVQw== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAA37JVxrRSMTbtG8SUUuPzzwjsrumeJD+100IbXoAcXK0mPbGei04TdyMTpfy1SDWL3YND5CSzcuHMOjApNKrxJ6wB8A5LE178vFKb7kvYo5SJq+YAbv7utQqlMH4sdKdywGwvdSTSZXCu+mzx9ebdoyRsmPS7vBrJYKcQPHiBDa9iVMA== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAAKniAtk8UOBIVa/348YbbZAyiLgJKpbH+2rEB0tHG7YYf+fAAGAZJsE7K7bl68XzunBT1HguytUpcjzNgOS2hx2AAVJT1ObdU0IK8+B0DTXrntG8BMKpDjpHZJr/RQLEDljHt6kc355TLoKZygF7vUujNjceWFjSmBlFeCbr5Fl4KoXw== test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFrJ3kAPWlhD8uR/Mo1dqhqtJIYACuH6vIh7/oP+r8UY test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINnWA1NU5+6rlguJ4vDwB7Ro3wTO+ntaSPfmWIXjp384 test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGGj5a3dN4+iyqsAXenA/CzejFmBNyZinnYHhXXcjbBV test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEy7J6rEkUafMJmtqE8SpWZg/uYx6EcME5FH3E3HB3rR test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADkhFs87N5abAoXAcsLiYTDOCujVJngP6C9j5dSttY/EBgCQBMK60a0vt1lF7b7Drby5kqg7+IvkGgA= test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADldNmjRx57BM40rrc4DabuF0L+RhEkOcDC8/jAhtbdTO0X+sqgITai5gSk1JGlIrCRIF83Kvs0TKIA= test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADkVoUhABT+RHfYEGAMKySSyUSoJFj6HHcz87U+io4Dpe4ye88DGUU2Lnxfux/WQq61DPHWxQHo4GoA= test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADkdO1HUptQ2ksEmblqqTM8loY2Q/Ep003qRxW753XVaz8eh9CejJvTO2FYtyHNib5syBD814By7C4A= test
+ssh-dss AAAAB3NzaC1kc3MAAACBAOGamTQ4i4AEWhMGssPosXwsaLBWIsldlycRQEfRCRlmi0hzdmYvuJJn+YQ2Jf1QJ49jTTxlCuFJLrDct+UxpittxRtDb+AdDpmhGp/zMeNdPBsfeLX9REfANyveQrfT9hqZ2gIbMOzJ0wRPgInmSRi22A0It7vxGaw47J5bAEf/AAAAFQD/Q9R8eCV6qL7aQr4E4aNOzZ5JdwAAAIAaUAApSPx+S6f+H49ml7sHdAQC4lVyKVQVh0RG55/rsIrd/dSYgo6LZ522vzNgxw2BmBeFJUpNZkQzIGmxN2FkTmlSgOxZkRH2oZA2j04vcskJgIiN8K1rhrpRUtJjcKXnF9YRn0eR4o1xph74ySPmIWX12GULB4R1j4nysqDmbwAAAIBL1zviIOohyjKKILovL9rPlTLHlcdXkyd5S3PZImq3CXDY5mQFlxH8r/hEnwMBBTAHmun7ptcdH/R5kZVB6v7xsAdJXDYiYOyoLI8yL4aWbjs3flriPXVmCb0hCh4n6pSKVR5IHLzsABABlJTULxSkvWcKwMms7arQm46WR6MPlQ== test
+ssh-dss AAAAB3NzaC1kc3MAAACBAMui93cyTI7R14Prqeet0joIvIWWGSw09hk6qDZvgKW2zzs5X1BTScrnLMnaKOgYtwDbFN4OQsBLNZu+PzcAqfwJBeTD4mGLRVk1k1zK6jXpdPr3V1sqQP7OEVa6MF6XsIf/Yu3zFvqpwkoNnrCcSrZxLMnnW83Ya6NG5d7WGBw/AAAAFQCHbVl3CZvAlhbRo/yMAY4do6Gg3QAAAIBb2u2D16qhuFq8B8UjBt1HSo7R/KPjRI0YzrCpJNoYjB743wARQMl5yDU67IuiIAzjxFZBiK5u+mTKpuO1v7MeTc9IFQ3ObR6N9PO1wUAZ1uv0KSesnxjgbZNIHwGwzJ88T43LEbugZleVEifbbIZl7puEgQdFko9zEgqGhOu3VQAAAIBKDmTnJZliffrAxCElJnnrM2SLo+Dl/cdtCDdI+3RBG7wTPWqMF8C9VNC5ssbi8QLDwl+OPrSIERPb3VXtjkVxQna36JK3od0aSx90VZkv7RPeL1Gt6Q/F+Td6D7p5ZCGjHaQkEk4kfPjTUnLxKn5FGxmY1HV7ceXo17AElmfmyg== test
+ssh-dss AAAAB3NzaC1kc3MAAACBALDWcFnpSWiwGbuyqVzo3ieAm2K9+BIUYjPlA69UCM1GulxzCj9ImbQDvMsuQ3tKpMH6925NoGU5G/atRnqXouJFNcOi43kqtHx8tE1vV9v8im3+gGsOF97pVgtBMHNQr8sjDN0FrtB5MvLMc9c5b/1lhrUj5PeupHIJzrhvAvsHAAAAFQCeztLdFSYoOE+Nq/XDQh0BuuuM+wAAAIEAh8p4L7O0vUW76jSJvE3v/yPG8bcBpZporUTvG2DqKP/rISyRblRoYlwmlaTzyrC0/HrUOqw/1fiyorr+KoxJVhOavlKVq/YSGpxmrgt4eS+VXr5BJyRGxDaPF4kTAu+2/FVMu4D1clK8BuR++phu9DWN32sWAzw2IYwOfhB0b+0AAACBAIUb/bDKE9DAGpG+zMyida0JTIIPHqWvkSa2dUXU9J+4iV+Bws+X3bEggZDeeDAKd9FmFTujek25ihIJD7tOu/CCFWEcUZ46aff/RQ5bfCKsJ2SyoSRvFGGal3541YvgK5A3DgzRjEGhlF5KfJ7HIcAdTq5CYA8uDBZngA8oeQfz test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCOSYk6+5VS4tJqK2Z095cPYHjlUT4Gqm2MM5xdzM2nK9bd4slUyz8fFYpWAIvu1G6Q5z9e/2Kfcme86lCjN/lidSNHQBpz6xW2WQTuTPfSPW2lBblRovlezhypdHTdMt1NB7B499py1BF1iLO5Y8URPCztTXawneMnUjvUp8g6dDKVDGNBssgwZ3TR8Fj/vrOkgafwtJAl/X4CJBXdt45dLre5eRXEoWDxTdW/GxUw/PtV/mW4Psheo3/RoIFequh2m7U8uSdXKvGqm0ZlpSTSoen7uELSd+9iY3E4ULdm03Pom/O3WO+DOCmRb5YUTNFKZUVop3DKizjEggy9mbb5W2iJp+6K31YWGWHCR/HhFr/61R6yQomXzHhbqAEGH1FsBmpRDa6c7a/b8Xl4w4kbBlOOs21G+tvL9l3hYqBfWpPE8A72WGdkzaOltvt9TEDroNI2yopNWTVXuUvJ2maFqiwf/4cYpGZqu3V4ccBpTJbhHQ3G3w5XVQ6Np42ez8= test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDIUGLfSbG/8i18Y53PL59UJ71RN4XTKJEvVljZM9K0HvFkc0wOlzTOXoOAEJiTIq6JXVWjq31BJoC/x20ySmaj6hSOs6NGrnyxXzSbM4E+PzJo74avh+NXZ6ojD3CKg7XuShgX5+PUqEiAffK3TdzRX4NlkIfiIaHi1O+Q5drSGwnoB1C5sNeimLDhO9IPo699OvQu1Obh06gygWh5pnu0NBuiv7rWwBx9S80fJvqhAQATxv2RWFDqcvn/T0WApJs5FCpyLxKozTpOenB9OGaEEz3mpg6H4KPL6UwhyC7YLr3rRKyjff08uELvdQzsO8q6tO8uOaQngLFhLe5lrlV test
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCszdi6vvJvN331KkQI2SVEtrwBHQunxWGfMP3HnSn4aH656Mcx/9a33V/W5KwKoSgVeRsHVnbjby10yP1PaU898hILUDFBazlJUyfxvMybiP6TC6LMomCFqoncaNJ+hR2BsEQmrIfmD+cvIFADz8zPtjDeOSAPQumP42C5uXJKNKYM8nmpCUrHo8qSvbVLTX/UC8mZweenxY21IxnKSZkkuLkRtqp7+p+xc6bYNfXcdb6LyrR/jmD2GsnEXSmWFPxvYGAErUZBSAMF2xYGG34l+lCB7Pzvpfv/mB0MK46YL+pdKC/B3JjDu2izt7cEBTlhd/TwzNah5OfsXtVJBNlYi9FU/Ee7A6+ylb5WnNasU165ngIVBBL5UNg8Hg/W6XJJmAVZg1PsRbikNNszKeaRrTC3X35PEr6uab5hlcORcAWV2FYoYWN8iIv0rFJyaE7InpxSHp/G3lE6XHddcJrq6BdmURez3jShrCYg7DdnE9Nx7dBX1rbJ46lLxKY5lQs= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGwG9ZwJjv29NIb0Cx1QsCboiidCStHoza+zQQMvs9nCBMRnABQL9ecKQeUGClj22ll8qRg6vWbHrQn8+ZUCgZg= test
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKjOhkR/tN6oSku7uJXEqDZh3HsYFeCi6F1rnDR1roSpLy2HQYGLPjalt+E+sQhayJBOLJd8j2idjIRNK9trrYA= test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBDTDzvrOOKrQoRhWJCf5ZyYk2ugd2wAayLt13XYBPJHArsgwCKMktyW6+dmKmskqnVDEPqQ42RBO0kcJ78y+3l2i1iNXr5dDSkJ/2J28L4YxzEOU6HhVn8wBA0wPYNpsgw== test
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBGYBJ8F7NOBihGiGXVBApYIXiLlNVW+SSYANC3okjqsax5eFcwyfQBRSWSJxAXKA1aV5vtYMN/nBwvg5kEgftHi3L3Pye4VA1PKyoa73E4T7AsAJ1eRrJp31ecSXi1f9HQ== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABdYMYEOU8AxcsHYPdHwtbndVLfJZsCFFQ8oMUt+xHi/MwISVkojGjk2FOtUeUnsc8SWXHEUC6GDSqy25+42GsxWQGL7/IyR2X0Iu9rUooKrKg4EsvQNEDfq9Joj7zuWOmAQBQK392fbXdYYILjSAaG5at9mhiw8Kr65ShK2IUv4n4dnA== test
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACWNfRwfrfQb+K8d5+VutrDWme+Pk6UyiDvbuS48bCg3e9ptaWHQVtQ6xm1yLYUeF/ZVWn20+s2DntLGKX8dFCaSwEHVmX4oj6hx2V5zprevf3GShfyz+HXt4qzoPzVMyOPY/po4d5oxM1RM7oWaqNwvuVhU9Zk5MbJSzKq3atWJv/+AQ== test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINKulAsCNhbGFNT4DlsMmQt57Y1/i40T2EWpeUWjiDL6 test
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2oQIAotMhMO8LyWecYBIXFlxSFRoxm8A0Hpbn1mEOJ test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADlrhnFj5z0wgxD0IY7ECoZE3/8v9cUWP7VJnyp0YPY0aLKBLnagj5/GMDV71bYW56Tl7AFx3EQvxYA= test
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADn6JKamPkXumrMxQnui53u1e7U0WkROtrmraifd6LEDfbEW78xEmNPSVZARg0ydaeNYzc+iEGG/cYA= test
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
deleted file mode 100644
index 45bee0de..00000000
--- a/src/test/resources/logback-test.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- ALL
- DENY
- DENY
-
-
- %msg%n
-
-
-
-
-
-
diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties
deleted file mode 100644
index 571d2905..00000000
--- a/src/test/resources/logging.properties
+++ /dev/null
@@ -1 +0,0 @@
-.level=OFF
diff --git a/src/test/resources/pkcs8_dsa b/src/test/resources/pkcs8_dsa
new file mode 100644
index 00000000..1b06e2b8
--- /dev/null
+++ b/src/test/resources/pkcs8_dsa
@@ -0,0 +1,9 @@
+-----BEGIN PRIVATE KEY-----
+MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAOGamTQ4i4AEWhMGssPosXwsaLBW
+IsldlycRQEfRCRlmi0hzdmYvuJJn+YQ2Jf1QJ49jTTxlCuFJLrDct+UxpittxRtD
+b+AdDpmhGp/zMeNdPBsfeLX9REfANyveQrfT9hqZ2gIbMOzJ0wRPgInmSRi22A0I
+t7vxGaw47J5bAEf/AhUA/0PUfHgleqi+2kK+BOGjTs2eSXcCgYAaUAApSPx+S6f+
+H49ml7sHdAQC4lVyKVQVh0RG55/rsIrd/dSYgo6LZ522vzNgxw2BmBeFJUpNZkQz
+IGmxN2FkTmlSgOxZkRH2oZA2j04vcskJgIiN8K1rhrpRUtJjcKXnF9YRn0eR4o1x
+ph74ySPmIWX12GULB4R1j4nysqDmbwQWAhRf04nB8DdbuA1JAbbkaSwORY/Mdw==
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_dsa.pub b/src/test/resources/pkcs8_dsa.pub
new file mode 100644
index 00000000..19adc149
--- /dev/null
+++ b/src/test/resources/pkcs8_dsa.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAOGamTQ4i4AEWhMGssPosXwsaLBWIsldlycRQEfRCRlmi0hzdmYvuJJn+YQ2Jf1QJ49jTTxlCuFJLrDct+UxpittxRtDb+AdDpmhGp/zMeNdPBsfeLX9REfANyveQrfT9hqZ2gIbMOzJ0wRPgInmSRi22A0It7vxGaw47J5bAEf/AAAAFQD/Q9R8eCV6qL7aQr4E4aNOzZ5JdwAAAIAaUAApSPx+S6f+H49ml7sHdAQC4lVyKVQVh0RG55/rsIrd/dSYgo6LZ522vzNgxw2BmBeFJUpNZkQzIGmxN2FkTmlSgOxZkRH2oZA2j04vcskJgIiN8K1rhrpRUtJjcKXnF9YRn0eR4o1xph74ySPmIWX12GULB4R1j4nysqDmbwAAAIBL1zviIOohyjKKILovL9rPlTLHlcdXkyd5S3PZImq3CXDY5mQFlxH8r/hEnwMBBTAHmun7ptcdH/R5kZVB6v7xsAdJXDYiYOyoLI8yL4aWbjs3flriPXVmCb0hCh4n6pSKVR5IHLzsABABlJTULxSkvWcKwMms7arQm46WR6MPlQ== test
diff --git a/src/test/resources/pkcs8_dsa_encrypted_hmacsha1 b/src/test/resources/pkcs8_dsa_encrypted_hmacsha1
new file mode 100644
index 00000000..959a4c42
--- /dev/null
+++ b/src/test/resources/pkcs8_dsa_encrypted_hmacsha1
@@ -0,0 +1,11 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBnzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIILw8uFzY0qcCAggA
+MB0GCWCGSAFlAwQBKgQQw6RQtTSUAB05u3PwE4Qt9wSCAVCIpjeuXs1WOfohxzCa
+AZYsQQ0hvJFa1DN5UTu0YwL8No71FAb86zoOcjKcrx96Vu+D7VnCCiJuYVpFalL3
+OoYpOpHY3m2ri24C31bR+2BY60GfCncK2LqlQcJbgQck4JGKJL1laN+9+6x1qXnQ
+72D0gEWDWSLHdgnB8lAwcuh/B6ZBwwqJYh4njOtjAwOWb7ynG1pKm8C4Vk55UwMi
+JT5gTpRMVONuj7w4NtROJmIe1eLAzZn9nZWo2y2dVzafCH0RcOZaEBJ4nBEYi0hf
+5eU0irwWXdoVzegPYR3j/xFSjVLSb14rR6XBjgF+kaH9KPIDTqKzCqXplrNK70+v
+H/IZbIgAKZh3YMU7GrBOArEBD9N4qdxJqO3OvgQ+WJbXXE5gauxgVFMYdXpUJXCV
+q+FdjhzDJAZzNlj4OsZx4YQqHroeixFV7sdg/T2/elypkCE=
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_dsa_encrypted_hmacsha1.pub b/src/test/resources/pkcs8_dsa_encrypted_hmacsha1.pub
new file mode 100644
index 00000000..be0e70c6
--- /dev/null
+++ b/src/test/resources/pkcs8_dsa_encrypted_hmacsha1.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAMui93cyTI7R14Prqeet0joIvIWWGSw09hk6qDZvgKW2zzs5X1BTScrnLMnaKOgYtwDbFN4OQsBLNZu+PzcAqfwJBeTD4mGLRVk1k1zK6jXpdPr3V1sqQP7OEVa6MF6XsIf/Yu3zFvqpwkoNnrCcSrZxLMnnW83Ya6NG5d7WGBw/AAAAFQCHbVl3CZvAlhbRo/yMAY4do6Gg3QAAAIBb2u2D16qhuFq8B8UjBt1HSo7R/KPjRI0YzrCpJNoYjB743wARQMl5yDU67IuiIAzjxFZBiK5u+mTKpuO1v7MeTc9IFQ3ObR6N9PO1wUAZ1uv0KSesnxjgbZNIHwGwzJ88T43LEbugZleVEifbbIZl7puEgQdFko9zEgqGhOu3VQAAAIBKDmTnJZliffrAxCElJnnrM2SLo+Dl/cdtCDdI+3RBG7wTPWqMF8C9VNC5ssbi8QLDwl+OPrSIERPb3VXtjkVxQna36JK3od0aSx90VZkv7RPeL1Gt6Q/F+Td6D7p5ZCGjHaQkEk4kfPjTUnLxKn5FGxmY1HV7ceXo17AElmfmyg== test
diff --git a/src/test/resources/pkcs8_dsa_encrypted_hmacsha256 b/src/test/resources/pkcs8_dsa_encrypted_hmacsha256
new file mode 100644
index 00000000..e71e348e
--- /dev/null
+++ b/src/test/resources/pkcs8_dsa_encrypted_hmacsha256
@@ -0,0 +1,12 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIf91MBHTU55UCAggA
+MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBCKkOxM17NPpYz8mDja3yvOBIIB
+ULDh7/2bZa2i5p529GCVrTjLYFWT9wLkkN6TSAGbVHeyjHm5hasrDlfK7Gni93Qt
+Hmaii/0PBvWkcf2mQPKujl0gwnPel+li28duaysYOCNwMgadlN9cTs2y/+yAXzoa
+q7UrQLxckpuDB3/hxn6WWDO7GRhthW3QmR6yzh2E0CzKhB2YU0tTGK7KHzGp+cS7
+Z0bwYlRlDE3YnDgi3Rpt3O5BT+uA0WOeSw1xd07jm/+okfhB3xHmM+vfO97476um
+2POcDsth0a66j3p1bZkNb4NHQEp8Ud+ST+4j90lb9Eu76bXGIOHubWRN+ytyITBn
+2O014+oOIdn00b38/yA/ej17fZPumEYh1fQHOlmOGi3fV9kEiTZYtiRYYET4keY4
+nduV4MVBjAFvtRWArEnkdeUqAqyEFbIzWADFxRZtfPCv6Pxr1So5DQatpe/UJiQC
+qA==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_dsa_encrypted_hmacsha256.pub b/src/test/resources/pkcs8_dsa_encrypted_hmacsha256.pub
new file mode 100644
index 00000000..b69d6df0
--- /dev/null
+++ b/src/test/resources/pkcs8_dsa_encrypted_hmacsha256.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBALDWcFnpSWiwGbuyqVzo3ieAm2K9+BIUYjPlA69UCM1GulxzCj9ImbQDvMsuQ3tKpMH6925NoGU5G/atRnqXouJFNcOi43kqtHx8tE1vV9v8im3+gGsOF97pVgtBMHNQr8sjDN0FrtB5MvLMc9c5b/1lhrUj5PeupHIJzrhvAvsHAAAAFQCeztLdFSYoOE+Nq/XDQh0BuuuM+wAAAIEAh8p4L7O0vUW76jSJvE3v/yPG8bcBpZporUTvG2DqKP/rISyRblRoYlwmlaTzyrC0/HrUOqw/1fiyorr+KoxJVhOavlKVq/YSGpxmrgt4eS+VXr5BJyRGxDaPF4kTAu+2/FVMu4D1clK8BuR++phu9DWN32sWAzw2IYwOfhB0b+0AAACBAIUb/bDKE9DAGpG+zMyida0JTIIPHqWvkSa2dUXU9J+4iV+Bws+X3bEggZDeeDAKd9FmFTujek25ihIJD7tOu/CCFWEcUZ46aff/RQ5bfCKsJ2SyoSRvFGGal3541YvgK5A3DgzRjEGhlF5KfJ7HIcAdTq5CYA8uDBZngA8oeQfz test
diff --git a/src/test/resources/pkcs8_ecdsa256 b/src/test/resources/pkcs8_ecdsa256
new file mode 100644
index 00000000..e7536772
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa256
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFQCSuRDPQE9G+4NK
+nZDeTFNJ0lJUy0aqUr3/hToG8yyhRANCAARsBvWcCY79vTSG9AsdULAm6IonQkrR
+6M2vs0EDL7PZwgTEZwAUC/XnCkHlBgpY9tpZfKkYOr1mx60J/PmVAoGY
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ecdsa256.pub b/src/test/resources/pkcs8_ecdsa256.pub
new file mode 100644
index 00000000..359fcf5c
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa256.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGwG9ZwJjv29NIb0Cx1QsCboiidCStHoza+zQQMvs9nCBMRnABQL9ecKQeUGClj22ll8qRg6vWbHrQn8+ZUCgZg= test
diff --git a/src/test/resources/pkcs8_ecdsa256_encrypted_scrypt b/src/test/resources/pkcs8_ecdsa256_encrypted_scrypt
new file mode 100644
index 00000000..4b5fe134
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa256_encrypted_scrypt
@@ -0,0 +1,7 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCEGCSsGAQQB2kcECzAUBAhlStjRMGQ7hwICQAAC
+AQgCAQEwHQYJYIZIAWUDBAECBBAdI6Cy/NhVCMbKL+x3uaw9BIGQgUJxrLtS+ygm
+BYp4b24PS568lpErhJy38FXl82atJimDxQhEECU8ZsMj5mdnGocGEyR9WvwFQ45Q
+MAuuPQO24WgvWcDLQXBF2c1ZtyJwrFLNEZ1eW2XxZqr3UEqyRgIPtUcjpOftTeH2
+BKksRQ1tixUBL4TuFX1ph1uuhO57pf4QDD/3cUeg829dg85qG9U1
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ecdsa256_encrypted_scrypt.pub b/src/test/resources/pkcs8_ecdsa256_encrypted_scrypt.pub
new file mode 100644
index 00000000..4f629d80
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa256_encrypted_scrypt.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKjOhkR/tN6oSku7uJXEqDZh3HsYFeCi6F1rnDR1roSpLy2HQYGLPjalt+E+sQhayJBOLJd8j2idjIRNK9trrYA= test
diff --git a/src/test/resources/pkcs8_ecdsa384 b/src/test/resources/pkcs8_ecdsa384
new file mode 100644
index 00000000..1eb4c26d
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa384
@@ -0,0 +1,6 @@
+-----BEGIN PRIVATE KEY-----
+MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDArsAu86PviKZkcTLOV
+i7Xch/I8gwEH+RPFb5VTH+N5USzfRqDzFbjSBQDyl8+uteOhZANiAAQ0w876zjiq
+0KEYViQn+WcmJNroHdsAGsi7dd12ATyRwK7IMAijJLcluvnZiprJKp1QxD6kONkQ
+TtJHCe/Mvt5dotYjV6+XQ0pCf9idvC+GMcxDlOh4VZ/MAQNMD2DabIM=
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ecdsa384.pub b/src/test/resources/pkcs8_ecdsa384.pub
new file mode 100644
index 00000000..fb1380f2
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa384.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBDTDzvrOOKrQoRhWJCf5ZyYk2ugd2wAayLt13XYBPJHArsgwCKMktyW6+dmKmskqnVDEPqQ42RBO0kcJ78y+3l2i1iNXr5dDSkJ/2J28L4YxzEOU6HhVn8wBA0wPYNpsgw== test
diff --git a/src/test/resources/pkcs8_ecdsa384_encrypted_scrypt b/src/test/resources/pkcs8_ecdsa384_encrypted_scrypt
new file mode 100644
index 00000000..2827d46b
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa384_encrypted_scrypt
@@ -0,0 +1,8 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBFDBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIAICpko7c3GsCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBFgQQYR9mSZAzK41nexevamz0sQSBwG37I4qWSkb7
+wpldjYIYd3YMX0h+E+9uGgxZeBW74bQ/yM2F2GoWjDnw38YEJZceNh+RA/COh6b4
+MqD+cIjmi5aXhtDKnnqyuYeEAZtRRpWEKO8xCwIkY0kG5dNBb4r9mJRbDGJU68zt
+bJGT2j5YXebwgszCt3UUhTXqF1bObYDdwx1SMOOMpkjn5PfGS9//EAF+xXBEXMLJ
+h802rFX81ZRe2d6WhW5ypZ6ZAmusbgdtlY00R/iKLvzznx/zkHeH3w==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ecdsa384_encrypted_scrypt.pub b/src/test/resources/pkcs8_ecdsa384_encrypted_scrypt.pub
new file mode 100644
index 00000000..0b2aa22e
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa384_encrypted_scrypt.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBGYBJ8F7NOBihGiGXVBApYIXiLlNVW+SSYANC3okjqsax5eFcwyfQBRSWSJxAXKA1aV5vtYMN/nBwvg5kEgftHi3L3Pye4VA1PKyoa73E4T7AsAJ1eRrJp31ecSXi1f9HQ== test
diff --git a/src/test/resources/pkcs8_ecdsa521 b/src/test/resources/pkcs8_ecdsa521
new file mode 100644
index 00000000..48da0fbd
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa521
@@ -0,0 +1,8 @@
+-----BEGIN PRIVATE KEY-----
+MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAu4Fi4AfgUdlwwYC3
+9ciPeVaUz8vZzqC1bXVtpPsRO5hCbtFrAQBdIQGyJ3chfDt3mi6UHToPXPEMGvSL
+weiJ+0KhgYkDgYYABABdYMYEOU8AxcsHYPdHwtbndVLfJZsCFFQ8oMUt+xHi/MwI
+SVkojGjk2FOtUeUnsc8SWXHEUC6GDSqy25+42GsxWQGL7/IyR2X0Iu9rUooKrKg4
+EsvQNEDfq9Joj7zuWOmAQBQK392fbXdYYILjSAaG5at9mhiw8Kr65ShK2IUv4n4d
+nA==
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ecdsa521.pub b/src/test/resources/pkcs8_ecdsa521.pub
new file mode 100644
index 00000000..add64a97
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa521.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABdYMYEOU8AxcsHYPdHwtbndVLfJZsCFFQ8oMUt+xHi/MwISVkojGjk2FOtUeUnsc8SWXHEUC6GDSqy25+42GsxWQGL7/IyR2X0Iu9rUooKrKg4EsvQNEDfq9Joj7zuWOmAQBQK392fbXdYYILjSAaG5at9mhiw8Kr65ShK2IUv4n4dnA== test
diff --git a/src/test/resources/pkcs8_ecdsa521_encrypted_scrypt b/src/test/resources/pkcs8_ecdsa521_encrypted_scrypt
new file mode 100644
index 00000000..ee46d85f
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa521_encrypted_scrypt
@@ -0,0 +1,10 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBVTBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIf54qi5gx2/wCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBKgQQKNwzbVYlv85zx1qRSDIuNwSCAQA6KsoUMRQS
+BHjCTbclDicOr16QQ5G0weLTjfy2xxaefnWlU0Kb8bZ795Z69+UfBcIPKeTi16EK
+QdamyrEqxioimHtLrSGY5wFXGD3SNXWOULKA7HIMwbvpD4w4ccuD2vv2uYmoMXeL
+s1l8eOC2gPb5Tip10bn9ZuHK5xEVMAAzFjO5ycT8d2j6ZUAE7EgJXdjCv6l+59iU
+texwnhXfX9Zla5K+wrMclWV4TIokjNfckPP4ZwBIrYsYG2SXtpszGWWBviZrb6Fz
+1DI/aApGZzw0MiZoYQ0HuzIE39TCdH/nvjMtWK1LZy/fNxipBtfVHOIB9LxpNyYO
+ijzcUbrY0bU2
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ecdsa521_encrypted_scrypt.pub b/src/test/resources/pkcs8_ecdsa521_encrypted_scrypt.pub
new file mode 100644
index 00000000..f5c9b1f9
--- /dev/null
+++ b/src/test/resources/pkcs8_ecdsa521_encrypted_scrypt.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACWNfRwfrfQb+K8d5+VutrDWme+Pk6UyiDvbuS48bCg3e9ptaWHQVtQ6xm1yLYUeF/ZVWn20+s2DntLGKX8dFCaSwEHVmX4oj6hx2V5zprevf3GShfyz+HXt4qzoPzVMyOPY/po4d5oxM1RM7oWaqNwvuVhU9Zk5MbJSzKq3atWJv/+AQ== test
diff --git a/src/test/resources/pkcs8_ed25519 b/src/test/resources/pkcs8_ed25519
new file mode 100644
index 00000000..7114dd30
--- /dev/null
+++ b/src/test/resources/pkcs8_ed25519
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIBdzIGiV1WyXNDyPVUcXZrG3jBYaXHG7TBZK6rrRYRUX
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ed25519.pub b/src/test/resources/pkcs8_ed25519.pub
new file mode 100644
index 00000000..6ad5b046
--- /dev/null
+++ b/src/test/resources/pkcs8_ed25519.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINKulAsCNhbGFNT4DlsMmQt57Y1/i40T2EWpeUWjiDL6 test
diff --git a/src/test/resources/pkcs8_ed25519_encrypted_scrypt b/src/test/resources/pkcs8_ed25519_encrypted_scrypt
new file mode 100644
index 00000000..ff0f5bb8
--- /dev/null
+++ b/src/test/resources/pkcs8_ed25519_encrypted_scrypt
@@ -0,0 +1,6 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGTME8GCSqGSIb3DQEFDTBCMCEGCSsGAQQB2kcECzAUBAhFrC34mnc2UgICQAAC
+AQgCAQEwHQYJYIZIAWUDBAECBBC4Yl2xDb6OxiknqsqFrNJSBECqIsjJq1c947Wj
+AVEwaAqDUkDPywSKR5R4b9lDAEclbUHfZAHDkDX9kk2+zwT1toPOuECGq3FN55BO
+wOUhPR0o
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ed25519_encrypted_scrypt.pub b/src/test/resources/pkcs8_ed25519_encrypted_scrypt.pub
new file mode 100644
index 00000000..51f914e6
--- /dev/null
+++ b/src/test/resources/pkcs8_ed25519_encrypted_scrypt.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2oQIAotMhMO8LyWecYBIXFlxSFRoxm8A0Hpbn1mEOJ test
diff --git a/src/test/resources/pkcs8_ed448 b/src/test/resources/pkcs8_ed448
new file mode 100644
index 00000000..0d44b40a
--- /dev/null
+++ b/src/test/resources/pkcs8_ed448
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MEcCAQAwBQYDK2VxBDsEOei82miP3EX0EZ0HhIhYRIP5r4+MqIPN/x+Qz/9LbyPb
+arD2I/6g3IU94gr6yne5fp3/coL8xBX+QA==
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ed448.pub b/src/test/resources/pkcs8_ed448.pub
new file mode 100644
index 00000000..b56426cf
--- /dev/null
+++ b/src/test/resources/pkcs8_ed448.pub
@@ -0,0 +1 @@
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADlrhnFj5z0wgxD0IY7ECoZE3/8v9cUWP7VJnyp0YPY0aLKBLnagj5/GMDV71bYW56Tl7AFx3EQvxYA= test
diff --git a/src/test/resources/pkcs8_ed448_encrypted_scrypt b/src/test/resources/pkcs8_ed448_encrypted_scrypt
new file mode 100644
index 00000000..0393dc02
--- /dev/null
+++ b/src/test/resources/pkcs8_ed448_encrypted_scrypt
@@ -0,0 +1,6 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGjME8GCSqGSIb3DQEFDTBCMCEGCSsGAQQB2kcECzAUBAhr6TolSPij1wICQAAC
+AQgCAQEwHQYJYIZIAWUDBAEqBBB73QBoaoKyPGPrWSkWZauEBFDiTfYOM29sHJFC
+psVERa9DH3Ir1vvCqAL/IldUx+UcdzjQxQkYqZeFiNZrFuNlDPua8fLlvuH2JNCK
+E7qNRbNDX/S/wk9ADpIrrkYFbmhTEg==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_ed448_encrypted_scrypt.pub b/src/test/resources/pkcs8_ed448_encrypted_scrypt.pub
new file mode 100644
index 00000000..4bc65e8f
--- /dev/null
+++ b/src/test/resources/pkcs8_ed448_encrypted_scrypt.pub
@@ -0,0 +1 @@
+ssh-ed448 AAAACXNzaC1lZDQ0OAAAADn6JKamPkXumrMxQnui53u1e7U0WkROtrmraifd6LEDfbEW78xEmNPSVZARg0ydaeNYzc+iEGG/cYA= test
diff --git a/src/test/resources/pkcs8_rsa b/src/test/resources/pkcs8_rsa
new file mode 100644
index 00000000..903916e1
--- /dev/null
+++ b/src/test/resources/pkcs8_rsa
@@ -0,0 +1,40 @@
+-----BEGIN PRIVATE KEY-----
+MIIG/AIBADANBgkqhkiG9w0BAQEFAASCBuYwggbiAgEAAoIBgQDCOSYk6+5VS4tJ
+qK2Z095cPYHjlUT4Gqm2MM5xdzM2nK9bd4slUyz8fFYpWAIvu1G6Q5z9e/2Kfcme
+86lCjN/lidSNHQBpz6xW2WQTuTPfSPW2lBblRovlezhypdHTdMt1NB7B499py1BF
+1iLO5Y8URPCztTXawneMnUjvUp8g6dDKVDGNBssgwZ3TR8Fj/vrOkgafwtJAl/X4
+CJBXdt45dLre5eRXEoWDxTdW/GxUw/PtV/mW4Psheo3/RoIFequh2m7U8uSdXKvG
+qm0ZlpSTSoen7uELSd+9iY3E4ULdm03Pom/O3WO+DOCmRb5YUTNFKZUVop3DKizj
+Eggy9mbb5W2iJp+6K31YWGWHCR/HhFr/61R6yQomXzHhbqAEGH1FsBmpRDa6c7a/
+b8Xl4w4kbBlOOs21G+tvL9l3hYqBfWpPE8A72WGdkzaOltvt9TEDroNI2yopNWTV
+XuUvJ2maFqiwf/4cYpGZqu3V4ccBpTJbhHQ3G3w5XVQ6Np42ez8CAwEAAQKCAYA0
+Ssi/VhpkMqO84EJlUUrkENdg2/amyh75Y6ihmhNa19LK0KPRF9Tb4eoc1Yo5Kbj2
+am/hO7nmyLk5J6dhuKYrmfF1UOKkmnpvI7azLMEPlAg78SE898KAta1cCNM2mJKS
+6saBM4YaaNgjBWV3yQy9y1X3PInUbVlcrZhOfzNC3FEPuJ1it/qGxjplAUPLVYJX
+ja9k4kJTi9Z4wm2Cbwmj9I6/pqvU2bLC6J1euTW9mMibFX2opshWmjWTDnvBV+ZX
+jT0i+D2p0sRHO2oo0O5WKvII6UI2csEM5VVCR7wReNxvIjCFgQP6+P+msIl7YSvm
+0Pz0RWbFVsbP+EqyAl8DDxLfq209Fq99V17T1lVu/4y9HEaI+4+hsO6JBCNNezK2
+nn9bq4GYlZ0xrxQuvEUG3BjaIJe0B7XIC2XUBA+530bImN8D70sO+3Q1gydB2+Sw
+uf1sYXMSfJwyy6qOsw+mJUNxYowDcCo44MRcB23XFQDLEb2MAFC3/lIvNKlQSEkC
+gcEA8P8UqV9J+ItSDjDtsA/un5T9DCy3zOJ8p9TMszJjuteCJw3huKFPG3oJIBiR
+uex2kwLiS6bNnZiiyW6eFy8e+w/xOVed3XKjculIhoT1ZL0IiBCsaDa/+L9hBqG1
+257k9yfgTl5j1ZppRzy0JMEE1Y/hQ5CZxxZjw+Am3XgRFzqpLqhd5vi0oGXgL1PX
+zHeiRp+oSnqr+JWw3DBhLbQ1SwbDXco0vyQj5nR1+EV7A/DeptPBeCn5Ox/p3g0R
+PcxjAoHBAM5QnQHYVeNFIC4zK4WC42wnSLaqzQS0YCx20NSs79xmeqiOhkkqbnEk
+2E/l4LKb0j2xtNEAIs389QfKoqwXlZB01Q8oOHMd6HKd7eelh+bkV7mig6rV1O64
+7TC1jTVlCKXL/jADVHd/DKCZ74C9AOubU8ONjSH0na6Stq/nYQrAj7aHKKJ2wBP2
+WRI3lj6Ma7Ebl3OtyXIay+gBegEMoAt/eJaJbcI4LirVl6vDTBXOGHtNSuJf9BEZ
+plgsvCdGdQKBwADi3z+UMyBv2rhko6sfE+CQWrHdxDtDpfO0C6CpEcbRHhBos6jL
+JxBRzZDJpleJsBHwU8a1cVIgCpE1D+3D5ZhEwb1VPiZfUoyMoRClkoVxUIO3k/q6
+INCYW5H7rECHgA4Mnn2LSVCyxapWZc8wyoTCh7CI6pfZ1DoXK+1qkJ2GQLMEOXws
+8/UXCtEBvOxqkDU0RxknBFTgsArPPrw9SmjhOHyyzqjZSCyDxx9HmiE0lI0GgFKh
+zOHTxzdi0upoQwKBwF0VAcFTWWywQ1SUwY36/6BMGQJRDS6SMtHcyVsqqw1FLxHd
+O7jG7A0gEnf5vubWZoRvKKUi6pEFD4f0ZHBAM7p4+6da/hzx4W3U0wEwLaB+ZRXb
+vIvW7brGtDzFrG/qpXwoDQacef9v0sKisOvkWgXyjlgsBQDM3Fdm2gWC3sV7G1u+
+nxatfPuEqasOaTrPH8dIK4yFW92fOV/zSpgumgaV/FHurxZxQmMQ+t0Dv9AWq/T1
+alO39uXALMybYkhWAQKBwCKNkCzsGc3yMzi32go5v9ysg1Pfj7qqIUzdxXWXFd71
+5e/1I4nfRi/6cZjmYQn6Br9oN/GkzMZXiPE3DUjwCvHPK3L9EqPPDGnxrFc05GnD
+2JsTzYT1XqlYraCgKLljlULjAdQgAm3YaXEoBY3o46yvG7/7N19OBF1j5SkcRHyk
+Pi7fBAKkIgBoPDCA9SO4Z/9aBmzImXmbkSQpawHOEYTk2xTebjYLl1oQiuy72b6s
+F1BlgpW6GHpeDNVV+j8vTw==
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_rsa.pub b/src/test/resources/pkcs8_rsa.pub
new file mode 100644
index 00000000..6e2a0178
--- /dev/null
+++ b/src/test/resources/pkcs8_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCOSYk6+5VS4tJqK2Z095cPYHjlUT4Gqm2MM5xdzM2nK9bd4slUyz8fFYpWAIvu1G6Q5z9e/2Kfcme86lCjN/lidSNHQBpz6xW2WQTuTPfSPW2lBblRovlezhypdHTdMt1NB7B499py1BF1iLO5Y8URPCztTXawneMnUjvUp8g6dDKVDGNBssgwZ3TR8Fj/vrOkgafwtJAl/X4CJBXdt45dLre5eRXEoWDxTdW/GxUw/PtV/mW4Psheo3/RoIFequh2m7U8uSdXKvGqm0ZlpSTSoen7uELSd+9iY3E4ULdm03Pom/O3WO+DOCmRb5YUTNFKZUVop3DKizjEggy9mbb5W2iJp+6K31YWGWHCR/HhFr/61R6yQomXzHhbqAEGH1FsBmpRDa6c7a/b8Xl4w4kbBlOOs21G+tvL9l3hYqBfWpPE8A72WGdkzaOltvt9TEDroNI2yopNWTVXuUvJ2maFqiwf/4cYpGZqu3V4ccBpTJbhHQ3G3w5XVQ6Np42ez8= test
diff --git a/src/test/resources/pkcs8_rsa_encrypted_hmacsha1 b/src/test/resources/pkcs8_rsa_encrypted_hmacsha1
new file mode 100644
index 00000000..2bb176b2
--- /dev/null
+++ b/src/test/resources/pkcs8_rsa_encrypted_hmacsha1
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIj8U2DOUAM48CAggA
+MB0GCWCGSAFlAwQBKgQQgkPCvl5Jsbu0bwVn+H4d1QSCBNCXNIf7CZi2cVPX3lbs
+r2D0aka8fzyVyazOWK1BTTyrr0lA3EIUDHDwp1tXoGXmTfyD4cto+pCwqCGLJOWr
+wwC4d0Av/Atdo+N4iePxrt2HBhDSWQsX7YyhntjBJD1Lk2LVt5qmlcxU+maizcLP
+afTdCOHIhRIbxf2dhrxOD4Vk6ccq4cvzDLRZ2r97H/saB5xQDyZkQn29jRFBMWuU
+nCbcVqgpfGV7shz1oecn8KyT6+B4Yxzv3SuxkUchZMX4FMj91Unr6B2XQ72X6ab8
+Ura6xaWuxIjkqTxKgwu4dVyujp3xTFA2DzIxdKogRurMLOvWBpAezOXr6whVrziT
+5gF0JfFyDevdNWAB0YM0MGgoCncI4lsR8hwaxXOTqh2Z8o/L3+ec9u7vckNOQinT
+fF0t7Gpup98Iu7uo3k2dqNtmUiHfX1PLhqtqdGhDjQTtjiUzHaOX82o3M3oGb5u4
+79c+R3snNtG8PlJDnR90YHns2YbRkJrFCqJ1qtxbN8qeWwUv5/Dc2Tmv2MwP9tUW
+34/0gmz9ylZAXK53mRBCdqPVboDKJd6rWK512Z41Yqire0T37qO0GrauEJMtVuCV
+dvO86jEc75sJcs/mGM0EGYWL9PlOphq/IHyFD1DHdA2WoieIyIg8o/DBsgw5xKW4
+wpu1Yaas3qG/X6XIMIGGlFl+H3g7gmSgmh1SjMsSnMFgVTMRmLgn1hnOaXNLrBDv
+nZlhrfVMKk4dCE9JfTnlC2J2OdoGE9bmW3c4IKLtlCTyoyZn9hlzfW6zprjV/Cz+
+ZtnCv40nO4GozXBlixXd+OPPRF7cLLBwPYzMs19zbqZcnF1ZYVZuEoarSG+yKMOi
+ZA9KPOwu8BVGoLW7WKG9XvTPpYeEUG1dNBnJyUGxg+m7CVM8f/2q4YBaJoZdmDBa
+3AxCAFckhJIk1SUGt3+C5+QBwPU2g905umZIZ8cH2X3w6T4M+bjYdDYj2+CfYV9R
+x1dSNwxMM4fVF4Q6/H1efyFjLeIpweSzqU9ZayxiDsVE+3LqPeJrcDOSDi4Qoaxz
+Lub/4KJDkrFw9FtDDkM4VS9FVy0BzbHayla3gWqmSBnjc/7TEuWEfKl2oULAklME
+aUAsKzw3pTXi63nFtS2nJyWDr/Te6peHmpXoTtXbUi5rIt2tcRqPEOowxjrSflnb
+Xv+1CWbTqGkmf/gXpapbcNgqKQQiR5lsBboXApwOutGy8rw5lhUc/88rI4oS44el
+vWhFdvBoQSuZLOXku8OoJvsv0HVLYa0apsDdCaI0bcO/0KZZKAy3VTaMZq5isgyT
+oAs4DHU7LY2SVFCWFt9Abv10Zzsjl2Ai5br6wfhP04tV9/VdC29MFN8Mv8PfA0H0
+cMgm8UdWwRFdDSYyqh+/sdfMXRsoXUU4aDWcisn7CAIOqfFj3dkUkfYZZaFeTQEs
+5k5pDn4xeyXX8wSyqjTiei7iK/rOmRPD4RyuOCG4KMmO8fJDNNuOQX2IhiKJSHGu
+BeMrXUDZ4zGmjy1Rk8gxK96HYZRV7uEmaV5deVYh3w1CejpK3nwN+uVCM2sAdLV5
++853Qp7/GzrCujzGbOUAp+h9nQy7YpdTRDeoffE2TKBc5jVXnvKRUS0JyVVnEtTG
+H8gn/MacxmPzp72vKecGoelMMg==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_rsa_encrypted_hmacsha1.pub b/src/test/resources/pkcs8_rsa_encrypted_hmacsha1.pub
new file mode 100644
index 00000000..76c1efdd
--- /dev/null
+++ b/src/test/resources/pkcs8_rsa_encrypted_hmacsha1.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDIUGLfSbG/8i18Y53PL59UJ71RN4XTKJEvVljZM9K0HvFkc0wOlzTOXoOAEJiTIq6JXVWjq31BJoC/x20ySmaj6hSOs6NGrnyxXzSbM4E+PzJo74avh+NXZ6ojD3CKg7XuShgX5+PUqEiAffK3TdzRX4NlkIfiIaHi1O+Q5drSGwnoB1C5sNeimLDhO9IPo699OvQu1Obh06gygWh5pnu0NBuiv7rWwBx9S80fJvqhAQATxv2RWFDqcvn/T0WApJs5FCpyLxKozTpOenB9OGaEEz3mpg6H4KPL6UwhyC7YLr3rRKyjff08uELvdQzsO8q6tO8uOaQngLFhLe5lrlV test
diff --git a/src/test/resources/pkcs8_rsa_encrypted_hmacsha256 b/src/test/resources/pkcs8_rsa_encrypted_hmacsha256
new file mode 100644
index 00000000..6008d876
--- /dev/null
+++ b/src/test/resources/pkcs8_rsa_encrypted_hmacsha256
@@ -0,0 +1,42 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIHbTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQINQ6pBY4N8X4CAggA
+MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBCj+GNhaK8p2R+feHgNRcWVBIIH
+EE01wtlgC5EVkPa/OgdN7MA26FSIDHTNqj+0rDhzvpgrw8R3IFsb7WZf6klE5Qns
+Ct/mZWt6ddMJwiI3xjPOQX6E4aJqgK5NanwftcuUjKICrOPseXEi8l+P0vwInn1t
+zaDxFgIqo+N7L/0MXiFbubk2eoUfpj/xPaGz2DxQqilQabJfWGy6S3VSw1yvQqKP
+q6IFJ9N+bRXbrrIRD3RgAkMfJr5MhFdwQPrWoUUr5U4s2JHY6r9hEaVn2UqCnopF
+FUj3aX2JQtLuJaTsRw4MmnDcPysob83w+njx9+1dldXXNW3hfWZ3y0RhFH7CfbAl
+h59yzS26rKweI0ir5Va+SVfdjHQWx8ICYr90rTt1tL79DHnmrwK48RJe0/mJ+ITc
+eQdA9X8VcI0mRQRdACHt84N+fzNG0/w7iiCK9jpS5hePO6cUlgkpC9/WoNrTSV7R
+NFTEmSp6LfTdMcWS0mt/8VdpwOcOlxe/UuXb8JSMk45ByutkYjrrvmDjx6hQLLhd
+urPeSYiC28k3zBHQB+PVdbrX5waHIQQkEykYjwQZR+ICh0fwveDPn8Xhl/ZslkTm
+xGmez+ZPssAtMLvlY+oDm43QjLaUBkwP8BH+vbhZ4Tmv2+Fv34+Dors4L4IvwXlf
+nun4UilFHoMbnZKWDTAQ2DvPU8DPnEVVCuE65nhOLuHhxdjMBc+Bl3+RdMuNGKb4
+fvW965RrDUNMkqQK0mqHVMtvv4eJiZAR4FQa2SzQ+9bHBv14L2uwxI/d8nNUFbgh
+imG4mnOJ7LacUDX3ewI/G2EgdJDVV7e1CJbTPu/EftX4uIkPite3JIcwF0ajr9+d
+tm3yYAr8jLp1ZR7vh89eBFj7NPAjG8FmxjEVMU11lovND7ioL/f+FITl5blUqZok
+4OCoOppa7BoPGULwB+fV5xiPa5V9KB4OqsJpIoCoMrxVMGsEkCgu358vdmbrMckg
+B9yMPFfU8dF+SI6Galfz3/iR3x6FKW4TiqgqkrZbpb7zs1HhnzHPPdvniSatgOEa
+QUSb2yZdO46++/2j+kfmhb7cx78ZaBQFMk1onv8sDBUY67LLD8ma3QkTJb3aKhpV
+gwK6NIv9D+F3kNrpnscfp7OsvZBxIXmMBsmH86TI3k/x0/tjTD3Q1VYSL3OAetvc
+uXz/WTide/T1EVSTdrv8H91QGypvxX9RIrf33KRZzCAdeGa7LABD2aNmhgif0gzi
+0yzilnfBjNZIZ18Zb0cbJYPr4Dvx3A8iqhCjJczN5yxe5sjQB+sxE6O1bQWOIG1r
+R1crXRjRTK3oauBiAfKTHmlmNUl805QBgjnMmeQLNGUOyVC0IdV3d/Q/dc4WEM9z
+QoAN6OPWNhZJLVNCLYl7kDtj3GcjHde/QzGJjkrB/ycP989MXk8B646m3uEYgVFP
+0HZseOF30gTvnLHNV99nDL16di6rr0St92hXtwOWR3NrGSWnzxSewP/d+adL2DEp
+TIF+WAFmbXr/iaAMPuiI2TegGiMf1YuWuth9vNN3E57l3vKpPXizh0q0617nCS1l
+yOkN/9ekjD4Cb8NjVdAxWlI9ibpO+ZNH7p+FzkxyiWbc5HcxGQnVcPpoZGiPpCMh
+TEwlaj0YAuqb6wWksEpOeb7FAKslGkQR2Azw3JH5hhpRKsVdJcVXVEKlzizazC0N
+wwe7Fm/CUdOiM5pW0/ZkvX13lRxxwWYRczpZ4hcu76DF8SMfO0Sks+BF1qNU87Ay
+Ic23Nc4Dr9YiTlf2/MQmmQBBZ6l+9Ju/NbZYSaPC69Mfkt+ENn8EYcX108Plaioc
+/rO1gkqE8g96ABvmddCRvqk0O9aV6odMCBBzeaV1X+teH2gkGTnhsE1QQhjScVxz
+LXU3kgtfeTZG0hwHI0ZZm4KKPn7gb+vNQ8oOz1Dm1lGaatUuiwuOMu88M/hqWwh7
+V2l61LEe/ugCGZuCJFOyDHZpDeaTtX90pl7ijDuUfVZnTBfGkoyoIQScz8mJkKnr
+yOV9oONevpF5Lf7uFihRpgWDBc1UsjV+lWeZo+IEra+W4hx5ADwqsThXKV9slhdq
+/xJlbmD1hvFRHhdSBbiZ6UUCmfP8XxIKDi4Ghfgd0ysrrVWDOZPuws0lg1WsgUOn
+kwHwfN0KXYfbdl+VQ3S51inRJUWAWDDQklAwBvWA+S5grhFNHRONk5OOBLvX8Atw
+86nFW63a0PR3X0AFzLAJZrqF+EZbeloQgX2EX8gx8XFIPTy7XoS1T8/Ezq8AIfXk
+JjF6aeBy8YAnRJn+4sPGAqX5QqktWcvIkD+kv2dZIADOhSRXxQq5ZA0WZchC6l18
+1lnAq+XXHq8NxupJ32rrZ1IRf1VIYCsJRMNguXKTWKZUHPAQxWhLuKW0ZBQJM3AE
+t0Q+gYvgja/p8WiC3l6WnioqJdWD5/FEsRGLy2GnWsvI
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/pkcs8_rsa_encrypted_hmacsha256.pub b/src/test/resources/pkcs8_rsa_encrypted_hmacsha256.pub
new file mode 100644
index 00000000..8647cd1c
--- /dev/null
+++ b/src/test/resources/pkcs8_rsa_encrypted_hmacsha256.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCszdi6vvJvN331KkQI2SVEtrwBHQunxWGfMP3HnSn4aH656Mcx/9a33V/W5KwKoSgVeRsHVnbjby10yP1PaU898hILUDFBazlJUyfxvMybiP6TC6LMomCFqoncaNJ+hR2BsEQmrIfmD+cvIFADz8zPtjDeOSAPQumP42C5uXJKNKYM8nmpCUrHo8qSvbVLTX/UC8mZweenxY21IxnKSZkkuLkRtqp7+p+xc6bYNfXcdb6LyrR/jmD2GsnEXSmWFPxvYGAErUZBSAMF2xYGG34l+lCB7Pzvpfv/mB0MK46YL+pdKC/B3JjDu2izt7cEBTlhd/TwzNah5OfsXtVJBNlYi9FU/Ee7A6+ylb5WnNasU165ngIVBBL5UNg8Hg/W6XJJmAVZg1PsRbikNNszKeaRrTC3X35PEr6uab5hlcORcAWV2FYoYWN8iIv0rFJyaE7InpxSHp/G3lE6XHddcJrq6BdmURez3jShrCYg7DdnE9Nx7dBX1rbJ46lLxKY5lQs= test
diff --git a/src/test/resources/ppkv2_dsa_unix.ppk b/src/test/resources/ppkv2_dsa_unix.ppk
new file mode 100644
index 00000000..de591a35
--- /dev/null
+++ b/src/test/resources/ppkv2_dsa_unix.ppk
@@ -0,0 +1,25 @@
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: none
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAO8/3Ymo6sXoRb7gfTBqwAYPO+ck8aKsoBdQIn2oD9cX
+2EIjXaoUXv/gK8tVN4EN0C8XADEFXpE3AQU5WhVZ11bv0AXOqGOreoP381pN72ap
+hh9o989Ee2PET+wEXZnZ4PmftMcuhtHWC8Pq0coht7CO6Xt56ItQuvj3nxMwYc0h
+vEFDOpHaFCyqiann9q3W5bLB9+ek2tJg5s1AVQKnlGCwn1fj+FXY1cRFDsp7AxGJ
+ZPDSNjq4awEZwECG/QLQYiWT0FVILFv68SxQeGk6OlOGip4dKkdXPIhMqHy0GT0d
+iBUm//UweV44S9IXPF9Xsiidq1Dn0vDe9e5OYNmV8kcAAAAVAKpXVF5GLMgMtlhS
+vjICPOy/A+T7AAABAQDEFLE91TMvZkCGeAHHXHkjF02m3CafJ7qLej8RoTEeZxV8
+Op/XlRluKNn97XF5G3DHp/DWGqBgawbBwqEq7l3V97zRHESMlivPr0UuCvjSmFR/
+on5nFCbl3RtZJOHOUkamtv51zBfk124uw7+bpnm9aiDNUs+AHNC3SLSyvdBbz6+s
+gt3kqD+FZyk+7TC/sOm+zuNmducFDDQIjldI7gx78b8D4BBx33+P5qW12PSlKGrQ
+7E1Rnk+o9WMSJosq/D7lV56waSV6/AJE9lruQr3LGEwN28DrvbVWen8KtvivGlJU
+wl1OK/hMBS5u+QZTIzN5M6iGB2hfJjBbF+LUONDNAAABAHBA/s/1ShLjRiIfenBf
+nInfjl3a3DSF9M3/o1fuY6EFkNs8MR4P2aeOfk0zE4gmO3vlT7TFs960tdUdIHm4
+TFOo+PFp4pjvVSO3K93cAjzOTwMI4sPhB3rRJxrDmx1CWVdnBrWQKO78kLpLf63/
+QB71qEe7MirH4FXZRLFP39gRcWknBtHmez92CFlzKzqLQXAMfaSbZ95lPh/bwgQ8
+OCuYExuB3aw3QmoQQdzSJRfWfuCEsUMmJ3Aub/yUMkpfe+hmQwkG2v730MXp/uDX
+fDXhZKkoh+v7Mhm04wzpxtB/C1dYpmfSP/i5ews4+9Lo9me6l54xy3+k05PPBBhn
+Yzw=
+Private-Lines: 1
+AAAAFBtGbtke8qQ6YY4xizrBFRlokJMi
+Private-MAC: 10e180bb416d61cf0196c6b1c9b75382f092c20c
diff --git a/src/test/resources/ppkv2_dsa_unix_encrypted.ppk b/src/test/resources/ppkv2_dsa_unix_encrypted.ppk
new file mode 100644
index 00000000..59293ec2
--- /dev/null
+++ b/src/test/resources/ppkv2_dsa_unix_encrypted.ppk
@@ -0,0 +1,25 @@
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAKU4cgWySBcfMzYy9aSOQwvH5JM1/M4UMNIB5BOZocC2
+FvJbEok0gTTbLCzGI9A0pe2bnr6EWx78v+aIH6p1GOmbhxZVY4+vkEUMH9Ee8v7X
+MYqz+CCefWWiAH2bXNa1v7LC9mImiuKmcDdHiVLJwrj/TH3c2EUYlcyQSUGRyOmi
+w5/auITPrAlmwhyB2ZXLC0X1qNKn+nJiwWxe+SsTXV6F38jbZo+fepbnVEqZ3wTU
+VnW2gpk0z+LRiLM+1mhASykpmRCyggxW7H2NyNGW+9jVTXD8WCqB/z0o+7PVCPS8
+5qpyfNi6PhH+etXiYnyG8iGI19BWZxDmMCG8Sgr+AnEAAAAVALkJetYWk3MacexW
+EA0Xa78/o9RfAAABAQCeNE28P6R13v2OUqUtBe6oYQPHOHzMFdEk+qJoXyzKmz7Q
+uQiCGuJ90YUST3fhvsBCOY6lPc6zGfeaVb/r2loa6zm3/0DoOXc/dnU4sbB0auoh
+PlwmjFXDwTQF5BRyxuRWtvPuNgKlgwFd/GcnIwlgW/jRIF3j5wopM3m12+yhZnW+
+gh575hjvf9edpra7Y1u1VR/jYEQRg+GuXFDvcGE3OTWpphAe0zfjwqIOMYYvx/JQ
+4HMnB+FxJ4svs5aUOPwBXhMCT9YPWEWVr+1GBVf/VlKTrbapNODYEp0tuL2z8Fva
+kgquJ2XGY74+6M1wbhrrDZ+LPd0Iuwcr7N0ghtVYAAABAEU47Jcr0BB3XEcmy/Ls
+L+hSKudgmczV6QD+fxelS4mY3//8INn+wMRyQzgix4B5yGL6OzQ2RBGlqho2TdfL
+o5UOjIoQzOpZsjZu2TLZezS5Lpx85ZVuLWwqezx1NZqGDXhtDz2tdUV8eNbePWb5
+788WXYXOW43ya6Z5RMoywkO7sZRziHH4I+WwlorUpDMgzzfGs2j91bpejsBYcSnq
+cUDcHJedbMmcUQ53OWocK+/MwoYi8+F/UBuwAoZ8wYauasMZ/ph2k8ygzWHCv7NN
+teyVAY3o5OvrML4sfu9kmgBIZKKfGZHeFptjQdQcOj7fTI3tuqocwiWVohYBF7la
+KvI=
+Private-Lines: 1
+WJAxySL5c2ikIb8JusI0EABRvX2f8O3tBbAuCx4o/i0=
+Private-MAC: 3384ce7d2ed81d15d76e9f80c3196813800d241c
diff --git a/src/test/resources/ppkv2_dsa_windows.ppk b/src/test/resources/ppkv2_dsa_windows.ppk
new file mode 100644
index 00000000..023f2c67
--- /dev/null
+++ b/src/test/resources/ppkv2_dsa_windows.ppk
@@ -0,0 +1,25 @@
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: none
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAIRPWjGVTl5F+y9uOPu8PPtFyjFy+Qclpqww/vdKq7GV
+AYPU5zdDHPvmEBZKfklfcROB5Ody3Bf7miR3RoLmmK9SZLDqEint+FdbvSWEooFY
+exZmYukhS9FeGUwttMOz4K4JIAPMVBxbGMPOL2AzPhE45BT2PPUpXMNZTLft2YHY
+hSIg30nsx+gc7tK7dkdY38pJ3Qtazc3jpyHnxT9o5qA1OdrA09nYHs9osneC4O29
+uru4Qz+foNP8XbGbkl1XxfooweQiElA9kvoOX+IgtJGgX2fuexAl0VcRmeb8lYxC
+0KOo9QL/HRW84nC8ZvwhiOXwbvN6X+tmKqTpcYQXgOcAAAAVAKearN3tBfL6HPzk
+CHzgGrUbSK1zAAABABW+7UNnhWAvSwiHShVSWm+eHjY56rss8OoYCvVRxDTLYhnZ
+s8GvjlHJTTvuPbNMiWYe/vqwKhBDkNJe2cw2on58GsJwLLmdhsZzGRuCZ5twFLus
+ZsjjRImjBTY5ZNYhdbhM4/527a7bSRLT+QPsedmaEvJHeSMHwDYwEb/WMsYnL7Xn
+QTaPRjetz1a64zWIYQU6PTHW+HRrQDT2Dh/knymQvcw3QC5/ZwNf9k1HS8FdE5HC
+9G9eQfPCGjiFGLfVfAKbq6HslLZO4Ea8WnFLm5CHz3+mFF+1fHXkahGxlOkPGMr/
+kSeIvcneiMAqUDINC7VcXShVAvATGyaVh324uwoAAAEBAIM8p/hq3+3N5V885fGy
+BuO6H9RFmmFsuA5c70CXm3A7CHLNh1+ql72WzaRs3DyTICexOtEy/m+2jFd2suKa
+Sa4F74OMQv4KaeeXrGuC8+EGdWFMN0Tf5UkvR/3igCFdxUbhfEa7qbMJBi5fOkkY
+1/OCldoHHWwWmp7uJA9oY7AVaFcwVyzOOQtUxBL6rtAE8Bcff88H2TFT2vf0WX/f
+KMKss69uzbphIIxnqnk152lA/gVWcT/mhCWa6NBIGJqKTNppKExTcLBcLlglqmLF
+i616A7gm8Y1+gi3dnDVu/hWiyz8uOSE31LB4oJGX4fkhOcKgwMJbB00F2ZGPT7JM
+4JU=
+Private-Lines: 1
+AAAAFQCNWViCVJUIHWjoUKL46EXL0dwMeg==
+Private-MAC: 93e11b58c7d05304857d602a2b0db8b8047414f3
diff --git a/src/test/resources/ppkv2_dsa_windows_encrypted.ppk b/src/test/resources/ppkv2_dsa_windows_encrypted.ppk
new file mode 100644
index 00000000..8bd2afe1
--- /dev/null
+++ b/src/test/resources/ppkv2_dsa_windows_encrypted.ppk
@@ -0,0 +1,25 @@
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAOex/Q9Z8ZKXJbCIQnLHKuJdhGNpdIIOsmFYpNGCClPz
+4ZTZmk0Dbw7sn2KrshVAbi2RXAIz4gJn+MfHRF7okwgAgN94mMP6VG+D4B2Zpgyy
+qWRYSSdV5AmbKc2VDX+DVK9qKmHlEX3Wjm4Yfw3s1kGN1/eWwI5MrHB7w5tA8Fw8
+iUfsRLnwTyGPBXbR1ATqT/obLAic6vogcpGojg1s61k8Rlz2bVMaYRu/RQsIdxvU
+Vi27jn00ClTaMGWKH1ZMhnmjedvN7qI7GcAUgNXFjOGa786aPXzJDC0JdNcmDoei
+9G/ynTJOpMMo8oy4bJLnVqL/kS8v5k+P4JeIhwvgp+MAAAAVALGpbWxdf1geBNR9
+INuHLoevUHjvAAABAQDijQTRDHymwb7TO5Lxy2P5MBvxlhTlRN4C+KIo5rnZMX+o
+FeEX5/hG18grUn7FYdx5/4AHP5EhK4GIcoKYJqnEjXvfSuvBvD2OxES8y9QI5Y0D
+ZSGyAsV0p0/9N6a/khkdJ5Zs9S8iFeeJkevIVsCskLRkz6rrBbsuiXB/N3ojtetE
+QfFuznFVgEJyBNrk7QuVvu43nWnzYaF98ah2o4C4HdjAds57XzyhDks5kJsi1mry
+l5jvbCSxcf4iZW9OsOHYcjifwfuM+hz2oYYf87obEIIMdOtsvHjMEfTmBqechUqq
+sRP6wzv0u6g8rReMBldaoG2HFGpwjvMM+PGU/zdWAAABAAhLKqKGhmXzW3ZugZft
+ycNwrxXFeVXFBywlC1qtDH8N/0aW4AvhWXBk2gw+rDXQudpNlVbtV9sT/+9JBM5i
+Q9txRfDjVeEzdOuXR2ch51p9Ep7KTYoSaBrBqL5KCxsz12G1+KJazMmlbpxQbdWY
+rAJrlRjTcHnYCDuhyUV/yCnrR+7VMAx5FyIduuRtYrCgo/+Z/Z9UklPU2596/pp7
+uozkCBR+RkOm7+9ldU+U9YiaUWymVmXhohFJvy9O6tqj3kZYQll2h7NnGpKVeIqp
+8wNW5BoCyVwV+fIuyCVsWg5vOKTBu5GR2Kc3AMc18Uq+WEeC/jsXZS53ItONrdNs
+QNk=
+Private-Lines: 1
+zn+CjCYuUbnPlsKh8Ge/gwCgzNkTwlBn4oeqV6/G3oY=
+Private-MAC: e0dc648a379bcbf2087192cb32fd1ab061ddba04
diff --git a/src/test/resources/ppkv2_ecdsa256_unix.ppk b/src/test/resources/ppkv2_ecdsa256_unix.ppk
new file mode 100644
index 00000000..1dd19011
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa256_unix.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCo/5qxk4V6Z
+ZGDFHDeVBr7ayBkc85+7SPuuY2OnBSrObqo06lw3nlH8dcloRzCt7qYvbmWTutlT
+ujgSOJuvd3A=
+Private-Lines: 1
+AAAAIEfIpXqum0vHuX3aJmumOiRTGEcPs8cB6B5cmKV7j8Pq
+Private-MAC: c2c907c13abcd1e429a8a53c162fb27fa32bf88d
diff --git a/src/test/resources/ppkv2_ecdsa256_unix_encrypted.ppk b/src/test/resources/ppkv2_ecdsa256_unix_encrypted.ppk
new file mode 100644
index 00000000..cbd6d7d5
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa256_unix_encrypted.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAJ3Mot6VLEk
+kG5HscBLY3geI20FnwrgQi2wzAKiktPDhF39ZavVlOMzvkIGgQDxmo8WoQllY52e
+d4e8+7KQ76M=
+Private-Lines: 1
+Giq4fU1ffVMQzpRS7vPtl4eViHxNmagS8TmYqnvb4KsMip0XwtRkRd1tqV9UGS2F
+Private-MAC: 9a55718d397e4bbbc0c00003bdcf5b8648da7f2c
diff --git a/src/test/resources/ppkv2_ecdsa256_windows.ppk b/src/test/resources/ppkv2_ecdsa256_windows.ppk
new file mode 100644
index 00000000..b571979d
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa256_windows.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCDseBAbWBVv
+O3A6gqqd2+Qm9uSTBPEFvvj/huI35w7iYhMmIjWAYwi9av867hF9GsRJaPGSkoWU
+IcA6QBXarAM=
+Private-Lines: 1
+AAAAIQCY9fEH+jqw1o/AtlCcSjksNFSuUILNENI6mcLywmdQLA==
+Private-MAC: 9ed0ea63718ce130ee11733dec5f91abfc076d30
diff --git a/src/test/resources/ppkv2_ecdsa256_windows_encrypted.ppk b/src/test/resources/ppkv2_ecdsa256_windows_encrypted.ppk
new file mode 100644
index 00000000..a57c1476
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa256_windows_encrypted.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKIrHi0RIW1g
+bd9+rJLTPJ5qAyXzNjEHidpxBzRnN22jDcnvBjqnAtfWPBjazs2KHSn/RP+Ktyej
+Y1Q+mMikDDk=
+Private-Lines: 1
+dYVv2mbM2lr/QhQ4Sro7ev9ztR0YAFD0ECI76rjyGRBTsbSH28KXEF4Vjq/eatWu
+Private-MAC: 7034973c6f35c7da66e86c2bed7d8e8367bfd8ba
diff --git a/src/test/resources/ppkv2_ecdsa384_unix.ppk b/src/test/resources/ppkv2_ecdsa384_unix.ppk
new file mode 100644
index 00000000..7f705c81
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa384_unix.ppk
@@ -0,0 +1,11 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBCTReLXxPo9
+3rpl1xlrrx7yYZgD0DzRieTvFO9C3a8Z8blHuZHNy2iljsIYeCPOtK9lcivx+faO
+rp5FdE2VDroGPdDY2ZAuwF4/UxBiGNNEXWBhRFkupBcM6RbyjvCXaA==
+Private-Lines: 2
+AAAAMH0T4NXp6wOqp8DWnEmXGlmAD+I5ytsSdlhznBdprZ2+wPZWcoa5XME85BUY
+p3HhlA==
+Private-MAC: df1b26fba219f717cf75002855565ce1b3c065b3
diff --git a/src/test/resources/ppkv2_ecdsa384_unix_encrypted.ppk b/src/test/resources/ppkv2_ecdsa384_unix_encrypted.ppk
new file mode 100644
index 00000000..20df843c
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa384_unix_encrypted.ppk
@@ -0,0 +1,11 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBRylqfIuL/B
+zq92j3g4UHd3TOlU2aG1U/2W76hrf1svwNPNe5d81xXA6j7Uh+AxOYfYrJRQr9Ko
+tRvd8WcxbikMZs5O/dKG+xx8WmTcgvu+jVW4lCbZrZk4S7/hqIlVjw==
+Private-Lines: 2
+mKo1enxDPaVSSEiOYuEL76/0NHTZLDCZuwy/KVXskXb2gwUu4cdbr7Vwj1IHnkBN
+GIBLGOWIDva5Spqj0XdDLA==
+Private-MAC: 4666bb8eeacc4a0b137edf9e7a32632298f83910
diff --git a/src/test/resources/ppkv2_ecdsa384_windows.ppk b/src/test/resources/ppkv2_ecdsa384_windows.ppk
new file mode 100644
index 00000000..5c66ebf4
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa384_windows.ppk
@@ -0,0 +1,11 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBPATBzcVXx8g
++unFg+8RAEvjiY/ZSb98vuIQ6MAtFCkNU2zgUHI9m/Jn9FitFd/F7KlIh4G5/wMB
+2dGhks/FYSk+OxgAmhjif6lKLfD5veRCLhRnVbhTQm1fOmU0Sy+FSA==
+Private-Lines: 2
+AAAAMQDHKqb0OEYb1K4KBaXYiXqkC+ybUGbleKdkx4V4VPfkDavMdGsOoFsyquuv
+CRh3/b8=
+Private-MAC: 8ef4699bc3c514b09b56ff672f30237da3d0956b
diff --git a/src/test/resources/ppkv2_ecdsa384_windows_encrypted.ppk b/src/test/resources/ppkv2_ecdsa384_windows_encrypted.ppk
new file mode 100644
index 00000000..3d26f178
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa384_windows_encrypted.ppk
@@ -0,0 +1,11 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBGxZq9s9JfIv
+KluYcWj/f+OuiV0bJ9RLE8rT+8qF0syMvzZZVRqLmF+a9VxJ4uxxRVvmMQ1CXb5T
+GtLVdBv1SrviKWMoaBw/sosSMr8v3eoQ04f4bs32fx8/dN4JQbCZEg==
+Private-Lines: 2
+aTUgs7URruvHriTmaMe12Y4Ow5smlllupJ1BnErif5PwHd6MrXFCzYq0grADQKpS
+0QaIGRrtXq4Hb7vvq+c6Mg==
+Private-MAC: 79f4ff32f403112ce42a5206d0da4b9e53edf33a
diff --git a/src/test/resources/ppkv2_ecdsa521_unix.ppk b/src/test/resources/ppkv2_ecdsa521_unix.ppk
new file mode 100644
index 00000000..0f84e82c
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa521_unix.ppk
@@ -0,0 +1,12 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp521
+Encryption: none
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACuthNYwDJn
+4I/b0fdhJdD3ykWe5gMBS91H9qWt0NHh4QTYxVyu3npbLveRVr9AWoF36UYIVF0Z
+3WjFV4u4fOPapQFNlGDf1DNn8aipy/5iUKY02z+AxnWIIjToN0IPPizMw3LApwmy
+0UaBSw7sZyfxNHD0Nu8wXsP8A6HG+Y01zSMAqg==
+Private-Lines: 2
+AAAAQXaGiZwhRZG+dlWCB8h9+wc0Qer7IjpJ3kiRyYXLTku+FloU3F0MXJiIOnFV
+31joL2SC2QEHHLEmvBXMT48R8AYy
+Private-MAC: 187e49c327364d784c54b3221adf18ad544339c3
diff --git a/src/test/resources/ppkv2_ecdsa521_unix_encrypted.ppk b/src/test/resources/ppkv2_ecdsa521_unix_encrypted.ppk
new file mode 100644
index 00000000..e9d350a3
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa521_unix_encrypted.ppk
@@ -0,0 +1,12 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp521
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAEUP4llO/qJ
+cKrD5gZ6L+8+JzEU5RLICud1jgVh+QgtuuAZQUztdee3GEPmR+C9Mv5cAHg138OC
+NcyRI1eukjFNDgAq3xeh0V6BwjZZur+v5zyTnXQASBwuI+iofZZqS0z8lsM48eIl
+uTZuo3siTlv6uvFLs4ovN/cg67QyGIpsKusESQ==
+Private-Lines: 2
+4OKuO5v+JHd96qUoo18Tb440u71nbpZNpXqSgBPr2VK5OAxVy+lqeccRtpAcUyXu
+TU8TTrQGHTxGMNMMUxun9HDcV02oZ8It2MsywvtfTRM=
+Private-MAC: e5aeb1290b219010b58cde9376db795b17dc1755
diff --git a/src/test/resources/ppkv2_ecdsa521_windows.ppk b/src/test/resources/ppkv2_ecdsa521_windows.ppk
new file mode 100644
index 00000000..4527fdbb
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa521_windows.ppk
@@ -0,0 +1,12 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp521
+Encryption: none
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGIVlZVYKq7
+1fKe0Uxj73A5WPmvTuohTARpvnJ0VB5zIHFEtHRuI7Qkn6pRGv8rbKDRKygv1kZc
+6rFZ2fgfhLxXcQHG1LwWM8x+XXoWMJM0C0kpCEErKd/tCZstQqg4v2JO0iG5+wmo
+Z90Cru912t24fhFGfVeFIbLN/xqznIm0dZTN/Q==
+Private-Lines: 2
+AAAAQQn76HAtrPDaRmvvO6XRFvuSRNt/onJPOFRF9pOFDZzBWbllnPf0vR5h7uOo
+xn0UoPNYc1rQAd9s0a6LvEpMRUCJ
+Private-MAC: d1deafe89f187d0f75d3d6eee2d292c5ee5f9dc1
diff --git a/src/test/resources/ppkv2_ecdsa521_windows_encrypted.ppk b/src/test/resources/ppkv2_ecdsa521_windows_encrypted.ppk
new file mode 100644
index 00000000..676d5876
--- /dev/null
+++ b/src/test/resources/ppkv2_ecdsa521_windows_encrypted.ppk
@@ -0,0 +1,12 @@
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp521
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACcIEokqqdX
+r1cB4vJksHNPuIb4HhwaRqaCi22yZa1slGr5+KQhWc6TaRP2yEsXaskuwThVy5hN
+jfONP/uB5IMj6QCxnR3bA/knuB2n7YRFlbIICLmCxr6QI5bz271MxCB+ccJO1L8s
+SfEsS6IcxH7O3rYv7hDadwcY9t5VmsJ3t7A67w==
+Private-Lines: 2
+plCduQfYJ2kW2Gbi2B6Rb0I0PXb+YgMTClhaOFJwpqV+z3GY9YK0VEwYj05cZya1
+HjGOtG69IWuPjrI523dcNYOeegI2BAYhFJLq05T4R/M=
+Private-MAC: 3e73192522ebf55488bfefe8d5033294dc39a972
diff --git a/src/test/resources/ppkv2_ed25519_unix.ppk b/src/test/resources/ppkv2_ed25519_unix.ppk
new file mode 100644
index 00000000..e3fa8763
--- /dev/null
+++ b/src/test/resources/ppkv2_ed25519_unix.ppk
@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAINknjInRhIeQB0yCD/N+ilxpTNwD2OGA776pYHI8
+bOdZ
+Private-Lines: 1
+AAAAIKg8xC9HSZbf6mY5WU6F7OWr9VGuDnJWLayXM3omacpf
+Private-MAC: ce1310f32622d44cb5d90198d482772bdd6bab72
diff --git a/src/test/resources/ppkv2_ed25519_unix_encrypted.ppk b/src/test/resources/ppkv2_ed25519_unix_encrypted.ppk
new file mode 100644
index 00000000..e511c694
--- /dev/null
+++ b/src/test/resources/ppkv2_ed25519_unix_encrypted.ppk
@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIFYLKsn61Pg/YYmumVe7oXik0o+Ca/P3vvN0Om3K
+CHqo
+Private-Lines: 1
+QGanXsZXxLK4SWcL9Y1JnGZxcbfBlZD4h2wi4YsyefXbhaTKCnX/+7SaRdXmFRB9
+Private-MAC: 30b7b90fe3c142891eebd05cbdb9c5e06c6ba448
diff --git a/src/test/resources/ppkv2_ed25519_windows.ppk b/src/test/resources/ppkv2_ed25519_windows.ppk
new file mode 100644
index 00000000..72eeaf00
--- /dev/null
+++ b/src/test/resources/ppkv2_ed25519_windows.ppk
@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIHh90f1nUVIpoKQoYxtD66cTGgLZ33D3hCxVqS/N
+5R3u
+Private-Lines: 1
+AAAAIH3OoVCpAhXZns3AaYJE7WXYQVUsY8RB7lwdJ9FXFS1u
+Private-MAC: 5942475d67b84bc1c89ad2fdb502dd8537835e7b
diff --git a/src/test/resources/ppkv2_ed25519_windows_encrypted.ppk b/src/test/resources/ppkv2_ed25519_windows_encrypted.ppk
new file mode 100644
index 00000000..579c149a
--- /dev/null
+++ b/src/test/resources/ppkv2_ed25519_windows_encrypted.ppk
@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAILhbcrIlE43dp+IlosYszkS0c4zpwC60ec1MRwZu
+xwvr
+Private-Lines: 1
+ARj/EPcb2v9tt/miQAJGKAf3Iz8HkzMHl6Egc5M5VCu+mkRRnB1DIN6wNYtp7ncY
+Private-MAC: 39813a46a634fd277b5a8bf379c804223df99027
diff --git a/src/test/resources/ppkv2_ed448_unix.ppk b/src/test/resources/ppkv2_ed448_unix.ppk
new file mode 100644
index 00000000..cb99a2fb
--- /dev/null
+++ b/src/test/resources/ppkv2_ed448_unix.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ssh-ed448
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADnTDrr21+87QbcPF+2Ayh61ot/Jtf0c9yTy126ki8Um
+/UUEW6uy87s/9gpS0OHyoH3GO3WgsUoe/AA=
+Private-Lines: 2
+AAAAObILm/ALMj5npvYMwKLrb2gJu3ZK1pSO78zy6XPjSJ2SxwAFNcVi1ig9elS/
+T8ZoChnKOv3P0qarAA==
+Private-MAC: 55827a80d4951757730ab061c617901d2c3dcb7b
diff --git a/src/test/resources/ppkv2_ed448_unix_encrypted.ppk b/src/test/resources/ppkv2_ed448_unix_encrypted.ppk
new file mode 100644
index 00000000..53134039
--- /dev/null
+++ b/src/test/resources/ppkv2_ed448_unix_encrypted.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ssh-ed448
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADnwdqOa0pIeeorEzsEY7kskGeyZfhpkYZtbA/iR5Nyp
+0cbPZnl9UFsfKQIYKJwPWwnim8BXTkEq6YA=
+Private-Lines: 2
+/A3mDpDv6Qz0Ou4TR1tZEKsJDYbC5lIotFETja/Q0ZGpRTBjUWZ1xlVZAQwZhDqe
+bIXTpngJcoMuU9ca+FlhAw==
+Private-MAC: 8c73debd89d60515a535a88c0e05cbd828592cd4
diff --git a/src/test/resources/ppkv2_ed448_windows.ppk b/src/test/resources/ppkv2_ed448_windows.ppk
new file mode 100644
index 00000000..dc328645
--- /dev/null
+++ b/src/test/resources/ppkv2_ed448_windows.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ssh-ed448
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADk5uIOJgx7OfFkbkRV43yMP37dcnIVemLPjlaH/WRKj
+LTwueFIgN5Mc4UYcIZtlbAaUhiiEvW7aYQA=
+Private-Lines: 2
+AAAAOSPpYS2oZf4SDKYbuFSxf+CZzYA5LC7mVwEObSg/NcsadJM3l7chtEgC+dPI
+OnzXuIwAaMzPSs+KAA==
+Private-MAC: 14cf577bd1d0851fb5bd13c5a92695e0a04ccecb
diff --git a/src/test/resources/ppkv2_ed448_windows_encrypted.ppk b/src/test/resources/ppkv2_ed448_windows_encrypted.ppk
new file mode 100644
index 00000000..6b8ad500
--- /dev/null
+++ b/src/test/resources/ppkv2_ed448_windows_encrypted.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-2: ssh-ed448
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADkVNtCwBIBWO41k+QYmuMromfii6QbjVKRxjxbTYGPD
+aw8u+gSCHAnqgaxi9qkc7HU0U01BJI5tJgA=
+Private-Lines: 2
+eUusIDlLKMS0TWcSwgfEx+SSDzlYX7QNXkkCZeOv/oc2Tx6/Jtsv7CnwDrVybF3f
+jbKnojgYqJ6OEV1uC0D8gg==
+Private-MAC: 216027b1e8ec6ff47ee2ac9f69e6f743206f8076
diff --git a/src/test/resources/ppkv2_rsa_unix.ppk b/src/test/resources/ppkv2_rsa_unix.ppk
new file mode 100644
index 00000000..3051221e
--- /dev/null
+++ b/src/test/resources/ppkv2_rsa_unix.ppk
@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: none
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQDsUXXjApfYYCtRK2LrymKvcZpS4cf6agyO
+j1/VW48g1GVtJM4g5kOjHWRqqsQZJCb8R9CvChCsJp4LPOS3tC36dSS52loeMoti
+7Z3GtKORjPGKDO/byt7V68l02KZdaO6U91D73a7y9v0g+JsLtR4xkZMd1zXLK3Nn
+T43rtLg116YBsLxAxbAC8eeC1xBF7MNzdLbmg8Gp9dA6iNnDlUhDsf2/FJ8a43Wb
+U8inaNJh6eGd6NtL1fb9iDWho+JEhw7WbyoNrBAf4YlqO+3Y0oO1A4ezOspv3Vx8
+CkoZTrtyCzVCPEIV/3wSK40VQaMxt4ptbPwQK9mSUI2dG3kuzkxV
+Private-Lines: 14
+AAABAQCKb84XhlVdtDir/Dae2pilHm/BTfLQh3+DpAoH+vMF0Gb6YS0/qKTG0Vka
+A5+M+ti7wXZGlCbgMl8zRiDWXP7yUd2J4pgYSJPPCFP9K6UGhwKYVKvOnjGcL9x8
+Ts5muinJqngOk/cA7h+rSPfLC/b8IsOEH6artnCMfNYu1ldzchjmwt8u57xw0Yc1
+JqIqAnQD0ZFv50JsZzdHy719IgMFiqYnulAgK+mwGG6Fcbcz0Mi29dcqPkpDzFNz
++5Dm0oV+oipg4oSMHBpF4EaEBXPshSoPCMvid2IBYqZk/0P1VeHeTfD12Tz6J2ag
+8bMxYh+BzBh9Lk23qLgbiy+zMOKhAAAAgQD3mVM5gnJvON4be5GoQHPsoZCLapFm
+ykfGxbtexc/96T7k9iISHMzzFFss3+kf+wTiM+M/eWMCGMNfeDVqCLQKXvijhnGJ
+zFsaEd2BAem7r0yBT35o0jcSNskHOFP9ZLmv1cx+QS3AMvW1d/Wgrdzg/n/N3oSy
+6xG0lwYAP18CqwAAAIEA9FYmUBuUGP2fIev5INsIS6ffOiTiSGRjz2yRRc0SDVSL
+PLknhdPrPDFJKvhOshkr+A3n2t/oqGI+ktbOuUxI7uhlPDL3D6Te/1ysObT0rXGR
+9cV/SE8NKWe83J6wG21u0AZIgiOYKAgmUFSju93Pj22s8ECmmSTh0QwJV3If7P8A
+AACBAPZa+3GZW7AwkHCPy2zjnCC49nnjVc/ZqodJiQ+95T4QQK8MzqlrQ+bTsoAS
++nN4WA9eOdvF6WQVrt5D/oLleA0I+c7iYd7JAOyiAVV75rHbBxpEUNgzP+VKymk4
+nR8N5IcbZJ/XXeUzQX7HaiRZansWPb+PvhbuYvKNsLD8pjr7
+Private-MAC: 456c364af11080a5032d3f01cf0adb5c425a13f6
diff --git a/src/test/resources/ppkv2_rsa_unix_encrypted.ppk b/src/test/resources/ppkv2_rsa_unix_encrypted.ppk
new file mode 100644
index 00000000..319fdbe6
--- /dev/null
+++ b/src/test/resources/ppkv2_rsa_unix_encrypted.ppk
@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCXX5GPy2zMByhViM5aW+lVH3oPVBVvsfXj
+b7Afy2ldttHHkDMhoQKMNUScBTdyLwHrje6c0C99aYNkSl5TLXePWx4nIkNFrEyh
+OfooY8CHPTuhCjfy+4A/psxeE5aoI5YGfffSr0uOVOUdhV3X3p1SmQsybzSiFz0T
+HULrtirxoCbf5a1UGNafYoXPvtSS3/qGJnsnbxE/gL2mTEvOyLI7o3ffFIgbfOra
+nbuow/HSboX/Uc9i2FsbJR4i6K5A6KB3NIL3gFJHxzjumJB4QXt7mUXzo0FWnOXL
+ORFrzYd1Rf26AVcUc24wwStVAOtXO3EIr/AylwgsqWqoStOzoRFB
+Private-Lines: 14
+8ObwsQzQGN/DN+sRAL8BwisB6emytEjK0EMer4XyScCaZWg0rKrGwM8HnLaenAVk
+kyNcE6/Pe7/jPLJjqG2qpDo7eFSWML1JZdrByEbzsJL0lSKEIgJQEWw3ozl7eLD0
+Mmhm5WC3qivygw8OGXM6IZyT8WnjsUZGFUlqt7o2cZ7hPlrVFoIumn4mWlFA96EY
+wjyWEsQiYPVtwuKIudwXrLWD/BcFOpKajNJ2xiZHDe15zPvJPTXCUiLP0/1OqEFC
+5MhUWbQ26igKx2rMjtx0fN2I+U9eVBIFuNnqAh/nQ0gMet55H5fBkSwWUVSA8Wt1
+SQBTlbtWx828EAeycBdLnUxJzhNWmBMz4ljda8cBecKTKHzAhRkxA7KgBiml3omE
+jUR4ZpxFoaL/y3tdMI4EyNnOOgpaHARamAqIrMB5JUEXNuYw+aOOkQlXmi+2JwuI
+2nggRicIV1+Tlo2Zi9lb723J43xlvcdSpULo8acFw60ybn9/Z+lkQjHRN1W/AR0d
+xzB/ytvIAFO9pHiRJrBkJ8K7GBGOR5XIrE1mn64zh+lgxhGdPyw73f6uBeeDKl+N
+QtPI9xAtX+2ENsJCXzbdVF6GSYCNQ/PomZz04LiyzvGPLe2xyt5EgGw5eR7ii7cA
+YdJsFgOSyqZm12NnuDFGlJdIHiB+nK5g0ZG3Q5KvI9xhJvYFaeJfGplP8ky3B9SI
+BP1xRUAzbmdxs81LUJMQtNZGZFC491iId4LAGy2SPGkiJ9OsxZ7UM3mtX8LBd+Ju
+R2OQZwMLJgGXkAd3bEphQwTFUnIAYHql6KTVbzxoZU+rjFrgFaphWCoD8HqOYx0W
+ELK8ereV7vQVFngbQIiazZaD/RyYbYkLJdBO+PE9Sv9LMqr4H8tiTrRCDg2EwYk1
+Private-MAC: e00e7d3fc277d5fe759477456f348ae71c78fbfe
diff --git a/src/test/resources/ppkv2_rsa_windows.ppk b/src/test/resources/ppkv2_rsa_windows.ppk
new file mode 100644
index 00000000..1b534e78
--- /dev/null
+++ b/src/test/resources/ppkv2_rsa_windows.ppk
@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: none
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCdPnSRJDt/YuU5Oj1gq6JKYsJacWbgsdU+
+vpzAT7IiIVOoGB7oPi8EGcZSt1kIJBc9Lf70uRYCbCmq3v3EOLGpSdhV16oKQygS
+gSZawq2YwYHzHnonp6akLfQQXwE/nnubX3S6iYAaJSrJ50APhKfQXYsz4CH2alrz
+Yb9Y2TDl4hKfxYABgMza3O02BFooK2tbxy2+0fbOKB/qjamGVE/CoMcv4ChB9FDG
+dUr0AVy7BYtfELAZ1dPrUh7lMCXa6JRRhFtdRWQGKmfM1nH/KpuzB+yZsl8kfHf4
+7qiX/mwTQM87JMxm7HuV8pHHr7DNFs9g30CreF+hbY1Cx+IKh+eR
+Private-Lines: 14
+AAABAQCG7sc8nWjpAUZOe2mcAOx9BI5e6h0sB65D73G3nSvxGcQd5MTw6huSW0PS
+Sz99OusuNsAn5IO8hHClDkGZFkVuTc30q+JgeAx1BJqTG6e4A6WtqKOOT9Ex5bUg
+L0Z0/1x2kc0rHT7uMKKtK2HPbzhKF1uSomzCdbWiUGjQp1/Mg38Hvy1KETfjAb4l
+4skIgqvCp/iIqd/2o+qonID3VYGRohHuf1BfGcFuwAXjwy1UQztPopHn6eclSLKd
+J/YpZHkw5t4UNN6NZ/vpTlU0H4DJPmXFsW9lPT6qTRjrT91vumumwlV5vaEFOZdZ
+a/b4KSh0QRs8Ae/X+g+nIRfT5t75AAAAgQDdovwknLlpl70yMktt2fvFZ1z/Tx22
+lc2Gs7wgrRlj76R7Y0RhrpRVCt/Y+Z6trocw6maHrtYzpF5IUFSteSoYvKqIT5kq
+HDCsxnD6wlH9jmKoVovGRLpDNzSlNtLlUVdmkyTQmbJGX/YjWb4/B93e3kzkFe5e
+PqGuI3YjR8Y0NwAAAIEAtZ+mZsJUuJOImvE37AbBljd6LlxRyVXqaTuXx5hetJ7U
+VK6tbLmMUXSL5F0f2Ftg6+i093Qc2HsaE3+Dt+LkkmiPEM+o9FIK8xwgey/S8RQk
+uGdaP84Q+cqIC0MMxOUuk3MWK1yKRDOZFYgjkfZPvhywY7d7scCPgzzZjoW6bncA
+AACAVZHXu+Xc6VTaLjAwlLj4aV6vgcFBlJVP1GSBnMdnRqHrqYWMpmodsh6xjT+K
+DkX0wkheNiqcxFs4FCIsGePGDdo4kofnYo6aH1a7jjAcZMQWgS9DxRgJQstYvGfr
+qnaDGse0dmD/w6tyqEmeBP6AGZJBWx9SvzJ441NtAv3+r2c=
+Private-MAC: 5c60a7b96b51ae767b879915d64328bfbe2867a6
diff --git a/src/test/resources/ppkv2_rsa_windows_encrypted.ppk b/src/test/resources/ppkv2_rsa_windows_encrypted.ppk
new file mode 100644
index 00000000..7f86ebf5
--- /dev/null
+++ b/src/test/resources/ppkv2_rsa_windows_encrypted.ppk
@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCPJDCMNOM7Huv+NNeP626Slq54aPceMLEa
+oqlfvLJiQf11b8jXcKfFgLlp/f0Ltk10p+5hAGlzBw/nlYRU85i7Eq75kSb6LPcQ
+3SNY1/NkrNcZcFf4p/AKeC0s1qgbypvjWy7Zk8AUooPFrhIo/kKaPMouq4hcyENz
+2VKgpdJ21SzTxjDASj7zd9DZP8uc1Lsg3ftbr8Mi+MFFQRnFPLv/w2zIoLo6xwO0
+UplUIyAGAsRPDdRZQ50h9b5BFyFLnXUlwI/Jrpgs2J31Vg8J2j2Y3BFDgn/Mf/P9
+Rsvdo4fp/y+W195198VQ2gsH2+Az+3Id6ySsGhztBP5Pl2Z2K5Pt
+Private-Lines: 14
+RU1YnbiIpjM2kSpm91PPq6ZEsvAJiM1ZBle98bdLIOI1KhrXzcP5GlDz0khoUHMF
+8uFn3zS9pAV3t6UC38KYBH5/sRni1wDf6nfnJ12VrOxeSE4q+5lC8e5MipOyOx1b
+RfwDb6o7tzUl8RHm+xmzihCHilUQB/05sDHy4JwMcN6QgA9hm23drSiCO8itd9tw
+fhidYahW0JkGrQf5rNkgYgCssMfAqcecurng+RWTU/J8M0BMbp8AYEUVtPvOK95J
+0kWz/lFG1d6oC1d5bcRL0LL4Oz6PWaVNf7+cjBRkJGfqJYtNi0zEN4R5MW8QTKye
+C29Dqy2/+JLjaa2xVYbdFqwIgePENEJIu0/g+k6tAhMN0G8kLaApsclKEtYCdd83
+wlNdpFvPr31Xna1y5mfpXDgfEYYUf+t3vcD0+IluYMRzkDwv/BN6QGB1q00NmWZ7
+iWZv0NZzf2pVo7hFJC83pKXfnkhqaLlLywfyu8RYEmA3LwdFe45Dt0oS7xP6DnAq
+bzgC3L+tMGoHrrKUhefqV8ma4m3rnBzobS8OCzwIRK8Jio1BgQuYX4EU4eu2DEmU
+eVJENZDT03uNzvh6G5ews809qz3T04Gyj7gQLgh560BwZ+Q8CVm9LVyynht+DMgH
+1CEGi7I8pW8aPZDcgY8EyS02NAf+kHUtHF9Y3nO2T0VyiBrHwyF13B6kuQATuWPu
+SRJpQlcMytjdwAx/iPzQBg2Gdbc1hmPVzFi9N4LWpTLncSVdwMWBAEKF+qZBkXx+
+BjBal45UoAD8JiNa/jfRVU/BHclLEr0DU0gGeUcP+u1WGv1srHjrrDbpujj7qQfu
+kIyq2p6IPkaCjz+6wHoZzlyY1fHGXS8c9qpMw7f/VohWDUkLP26hCErF+eiAtq1K
+Private-MAC: f5e53c305287fa9b6b02422ee5de0dfee3ad1a5c
diff --git a/src/test/resources/ppkv3_dsa_unix.ppk b/src/test/resources/ppkv3_dsa_unix.ppk
new file mode 100644
index 00000000..9ac88344
--- /dev/null
+++ b/src/test/resources/ppkv3_dsa_unix.ppk
@@ -0,0 +1,25 @@
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: none
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAM6Xy9zencF2KDhq1r0gTXBdA4AL5Rtg5GYd4tx6qlSx
+k3ZX634ude5bV8Io2UrsgvIDQHSfj+1J4sDpWnhMtT12KucddwAzLhb+/5gYEr1U
+CZtPn1WlsvOXrJ9pmAt/MB1KppyG38UX3QK6Z04oWspqHWJBfO0oquUUGx5Ua9JS
+neJCrzk++Sd1GkhjZElR51Hm7MKV9KZejn/tUZQK2FRjpfP50GsIlAs1v31OkY1g
+lhgs90/6v7PyV1ljp27Nw1aNL+CxOjA/wYnFo+jE5+EsW81oT8C06n4yrcFySWFR
+96wOfb2fAuCl4K9Ma4jN+7J0RLMY1++KzCsAtppi4KMAAAAVAMbFE/Q0VL6xGj97
+5l/7AqEVxWH7AAABAQCbU609B2d7d5EipWyjlETAs64vaKBXOAREcQr8HQfAkWmS
+UBr//brBdNWeA4TVla4hyKL1CBiP1Hi6pDQ12lCB+J30ikRqdi45y2MvOR8OfEDy
+3sn94sLwswARnMwdfHWrPepfyVdqOnIlhsYWzLpc5EhfOThvL8YZ78tPTCqyui8C
+Oi3TznOFfW/do0ydEEmfQwJTlHi6dpFiokh+mpQVOlstehmgKzrolLEjccS/vnxS
+qsdWDwKErbtg0bYhhV3rztiMcMozoBblgCSDiVmvGBAO8GnIoyX5/J+UfuwWYQKY
+rtuZRfB4hRyRU4zKopPfQrZkc1Jummk959KgfeTIAAABAQCayyiO1dYHPHF6g21s
+wekxrukaXix6sab4m+OLm0bNRa+7nI3A09wG+otKajznna/mdjcIaWuhdG5S4+l+
+aPypIUWTdM0krUu0A+9Pu9YpIgVgEFKVQX9PBcP5+WHanMaeYmLOCEP1WqFMJ5Hi
+RCJmssOKwZJygH/KEMIGaDGhYnSmN6R+41j8sDAoY9/00WlSTKqpmVGTe7bJGWom
+nhT08RsATIyvM2i/J13qrh5UfGprj6GsaE9q+Zjg47oMdZIRSgyp4jw9coQoR68T
+emOmkODmXaoixfrGT1FC9LbqOmN/iDwv0QvLUen3RTbbB2lE3LwObBXJRpd44Rje
+Mo7c
+Private-Lines: 1
+AAAAFE1NvhJa+HevZ3y1nFhhtzEh3Yi3
+Private-MAC: de384c789bdafb1acfb4ad240ede6a24ba92b4c45d22906b0ae348f5caeb4302
diff --git a/src/test/resources/ppkv3_dsa_unix_encrypted.ppk b/src/test/resources/ppkv3_dsa_unix_encrypted.ppk
new file mode 100644
index 00000000..0630252f
--- /dev/null
+++ b/src/test/resources/ppkv3_dsa_unix_encrypted.ppk
@@ -0,0 +1,30 @@
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBALvd0uyCDZFuKcpphdcnMEYVXnoy16rW5XIFhlxmvVSU
+DDz3DAGyXISyhyQ43BhI8R0OeOr6VpivujFu0srXrcxcgsAy6yv/6uAvaeLfekc/
+JErS7OqVqACFmt5ISkEBcvWKnUseFRwoy2KCeiEo2GyKwyV00caLnYb1Th2CUNRF
+ZcH5p5XLj3gwFbPf7EdkKV0PHWV4E5ptFVVT/aRijCJRQn//FlSNlG6iOfUF4IRU
+pz05qivVGaHLWNOw1fSXK1Pc53qQqCAaEict3arhQCwSEKl1xktSNBDVmeapLPj7
+LNcZIGvAA52Tpg3qR1rp/8a+bdtteNkbRg4Gfx2uBuMAAAAVAKFdLforDVztYQzh
+7pFjN8qAjogVAAABACTsvZOw0aPNwdQFiaQik1F08HQrjlp298vgbjIZFrgABE4x
+euWMydEcxAKKEhy7Y9Go4l9shUddkHgk3Y+OLvAsifowqfc8Fpsqxh1WKLxnw9NO
+5rjenO+kNeiwhX/oWa1VBMOa/O+iGTvCy1XcW3u0E1LqGVAx3DdkP6EHV91ma3d1
+LWih/RR8ClERNaovnETash8NQCHAmKejIObneLtNRqyaiNohYK0t5EAtP4JutVwr
+snj3ZoSKrCZct2wVrMUkkqCsKcKygCsr29uk1xpVYBkv9zUn6V199ZB/uXanRLcL
+/vq0UYWGV9nXU0shVanMwHNQaMk8oWnd6hTPB6EAAAEADwMN1uDvQqkyKel83xNG
+4Er3LsSCx6iARNWpaV1ZLNrtf2h6MnYCKdSdyVgRjIL5coBQB5yZb/+CXm9QgysH
++K5FHXgnh4hjz8W8/Magj+qm3/SKDTmbLAsNvvvenQvayTWUGNQlfQET/tHvGRy7
+gz4r95phWGmxS8Zu1NkT0RHl6xxcSqWmvvjG/m2MuNo+5lfKFa2+j+Is59HwN3ZK
+5umpPlJl4uM5SkyFD+iUbptOVCXdVV4xRhIzYy4wllADQQHkbNQtNGJT7JEHuot9
+2J3FWOCEQVw4F06j8NsdghKWIwGSlcHLHAbA8zZDq11R6vnhOFDwmfekxCwLXaBl
+GQ==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: e10608eedddc7992eff0f91aeac654b4
+Private-Lines: 1
+0+i6uWp69WUFuFgBYYmfzrEw7wjMgKINMsfXSRh+JL8=
+Private-MAC: f962cf010508ca8b62f2562881179e2d70afe18bbea3d48939d98b695710cb93
diff --git a/src/test/resources/ppkv3_dsa_windows.ppk b/src/test/resources/ppkv3_dsa_windows.ppk
new file mode 100644
index 00000000..a2f92132
--- /dev/null
+++ b/src/test/resources/ppkv3_dsa_windows.ppk
@@ -0,0 +1,25 @@
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: none
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAOGfXe0SXEr7JIMEZQo6rkTc6CfPIWsgqDYem72p84pn
+6KykGD6RonyqDEfDoS0XpwIY2sCZLmwauEhTQtBZpMyix4sD49w+SjaHUP1aoAT7
+iPFvsXXGNw9/jgK5a67NzWxmcl5XMQG/VDjtVLWzNfl9OXHMSNmGUKQmlPZKXRrm
+06SYVZMDsRbgSJgi6PNOatuVvfR+b2hK+ppMpQNKnHXe/vpgcblZ1ViPUTH4vG+0
+4PkidIbwHIIl5Yv69lUYOcycxR3yzd8ZC3a7qzVkxet+tGWCD4/PzfWEP+/G2TWX
+bRuxV6YEaq9POmI0JuZa7CAZ8iA/OFtYua3TB4cUsSsAAAAVAJzJoUVy/fnPi6Uu
+PLR8cCyhqB3HAAABAQDH61jRVXoAqKJRHgIWhBsrQMmAvOqVHyrALSlAchNT5cso
+4e7Rd9ajJFg+hVZGKu5PoQdhp/8ERk8mnABKue6r9qO/6qT8NvCRuI0Wb8Sxgw5q
+/4hzo4BVdx4UhLqTRl9rF7B5wLSjV75HIsqW61ZcbT9+VdYpRaNg8/Te1jfqwgrT
+ndaD7jhkhU2P0Z7vHSRlzaeSksF6b+SRFu+bnQ6neBxZZTuMrSfKOSpp2ltLUvWr
+Q48GJ0I/uE1HbQehz9Tn0Q3VA2BjpfKgj8bLE5YFokveuqjFULF7EiL4CmAt6qhi
+QOl/f6zKR4gaLUjkVuvU/uyTr/da7/FfOJouETpFAAABAERldAO0JWHLr1nClDX+
+ZPN8XbbpjSaWO+fFLQO0A7QE/2gKJ8lt5qi39GOOhmUO10johHaOupqRqe4iCRnB
+xMJeLxXmS2KbcwDke+IBRaid81ccocpcPOrpov582DjJuwZ0394Akyrc7uMonjBy
++JJaPwpXHQS4SXRe7NybuEE4S+DsV6DNx9WG5n5PFh4YELbzdP2oJZ3hyZ2MiZgo
+02ShziEDPI1vQ4BBAsknp6Ubzt40Gt1yjyNMbGpv8IIRZzu+H8CIRnzbCYKT8mRK
+e0HvTG48c6la0/wvvG62zfmbFYgkw0tQxgekpf3ojNpRJZ1vcQPgtvQ5IjkCYSF/
+ss4=
+Private-Lines: 1
+AAAAFB8185dI+D57PqqRGfFcBGxjParZ
+Private-MAC: bdd028dd46f482f8aad5a4ba4a40db927350d41f129f6fb80c3907cb51a22d20
diff --git a/src/test/resources/ppkv3_dsa_windows_encrypted.ppk b/src/test/resources/ppkv3_dsa_windows_encrypted.ppk
new file mode 100644
index 00000000..088f99ce
--- /dev/null
+++ b/src/test/resources/ppkv3_dsa_windows_encrypted.ppk
@@ -0,0 +1,30 @@
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAIYJqKE0jtG+HpbHurIu+kVIuQLrMrgXhL7woINHOHGf
+b/0O85u+3DsSy+s7f/M9bWUIqRtt2+JV2J/V28mDgF7IEeiP3OjSYspmoxnCy8d6
+g313NcYYoCqG9f/yWrzSXQtK3hD8+KNqm68GQx2L6lceyyHn3CgUFTwHw0MNntOf
+Ak94PCoM5wOZCfqx9sZfTVk+uQb++aG8xcAqPoPQdTgaqrFa9lrp2Ul8RhM/eDpC
+mByg8I5tIsbk17IVzS0gLp9slfInhwr2UjX2wIOlVOnNmS8UHLAwhDtdtHcWYdFb
+EMpO9hFMJCz39JqnJ7g8qjct/bfMxAEqhwlGpgbYrz0AAAAVAL66GQDORVX2on1i
+c6drzCN/9f4ZAAABAA8eU3kzUacUj50ggFzpTLuBBztILvjIpNGnXy7SUG5EWCZX
+LvBPTjJf9idxL/0P42AlJqKpaZSsdDLeaPbyUsQ0B9R+eTVDKQd5po5kBmWcxqMJ
+h0f1XU1CYFZgN2cvzmtyx9euekEka/j1mqGLuXHRpX0vak/wsy3T8mEPlqLr09c6
+hPUDG5U8NXEy3aWS5gRRj7wPDLAi1sFGo4ReUfjn6LJglbCi71tSirnV0dJJvVGp
+XA17RzetDgZmIjdDur6XdGz+6B07puj/OUTTCFSFzJC0wNKGS/OjY9+59ziFj+M7
+LCA7HUAlYeL4n83EsZp4fHI09JyuLJppPo/4xxAAAAEAU2ThrVHZ3fT8JuXT4dgJ
+VtgoBTQVf1vbzKA/o5yrOCKj+pPLFv+mXSJxLWvdSUeHkaRZF76hCwSpy6K+Wt9s
+RSuL6qTPog8TT7Yhk0YgfMd/wXwYlhhoH6HqRIIQRHd1bIOn+n1DUWQHS2w6Ksd6
+jnVDd5cte+sBRNFeNjRJPUg2dzFqYQUgf5bfDu6/mhISduez7BctGb0cWtVPyjzQ
+ivJRUVnLiUJJE3V0saEC2FYOalknyW2x99tolUn4YwOLjUuQm1oKZ+BEbXlgGe4p
+Ke/JxB3nYHT78wP7ArnhImPGK8b0+/JD4nAA15GEkFLRV/u9qyZJe/iwbpqKz2vB
+/Q==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: b2f8dee868310abc40ef9d96abcd67cf
+Private-Lines: 1
+HUZSXpBSW1DZJtQ9dCmjGPKRfqwKXlOFleqL2zzeK7Q=
+Private-MAC: a7217ec852a154487b30da65849d2f25a78d822881b739471164df5fa24c08ef
diff --git a/src/test/resources/ppkv3_ecdsa256_unix.ppk b/src/test/resources/ppkv3_ecdsa256_unix.ppk
new file mode 100644
index 00000000..406de6b2
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa256_unix.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLoiF7se+l2K
+wZh9oxhfTASh5diPDNvYbVgS98Q4yPE9uExiDDMeX2Nu9XLtnevuTWrmcAF6u6/o
+zI3BIkeHiBQ=
+Private-Lines: 1
+AAAAIE+1J+zx/1g2rtov3b16NoLXn/cW+jdeTQrXgllPzlC/
+Private-MAC: 9549460921fa6dc12d723f3985618f1c8fb719fca05f388a3405f4c3eee7f7f8
diff --git a/src/test/resources/ppkv3_ecdsa256_unix_encrypted.ppk b/src/test/resources/ppkv3_ecdsa256_unix_encrypted.ppk
new file mode 100644
index 00000000..5cf8a899
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa256_unix_encrypted.ppk
@@ -0,0 +1,15 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGrSiZUMuXa3
+aTNDF/H+rTZb4wUBapMIf9QXzfoJI6hOYPcCtHGH8KChdeZ2U9W3ohzX/WZIX8IQ
+9fpxcGyvpbc=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 712b0ba1fdfd732b3dbdf061acadeb51
+Private-Lines: 1
++RPUh/TDOJB+zgkGpLmCpmoaiQpD69WlNiES+pPbUQ1SF+fxZMhvme6k57zH6iem
+Private-MAC: 58dd68fa08d7fdb217db1a69cc970320bd3fa45706b8a5b2a1feaa03efb25cb9
diff --git a/src/test/resources/ppkv3_ecdsa256_windows.ppk b/src/test/resources/ppkv3_ecdsa256_windows.ppk
new file mode 100644
index 00000000..2c2b6179
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa256_windows.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDMTBUib+twu
+te16i5Rs2zKlVDMam5/RMWuFcXs2dvIs0kLP5N06diHYHBqvMPT9N336KMChz0/O
+4S/dXR1pwHE=
+Private-Lines: 1
+AAAAICNn2sFEDQ05pZvx8Wjcd+m/fHnlumgo7Rr7nE7XZd/0
+Private-MAC: a49e1637cc50016094abd341f817036d512f54f6a024947725e3ce9a2eacf67a
diff --git a/src/test/resources/ppkv3_ecdsa256_windows_encrypted.ppk b/src/test/resources/ppkv3_ecdsa256_windows_encrypted.ppk
new file mode 100644
index 00000000..3d059c69
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa256_windows_encrypted.ppk
@@ -0,0 +1,15 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMQNLejYwJxw
+VlAGIVi55Lb0zfkz18CEAwqDtrr6xs2sz+vqRU1xadOgEzHeWovBqCXdC9wVja20
+2PyupH5QPsg=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 43ab4dfef809a010f8b46898613e7791
+Private-Lines: 1
+kChNCwDStowc8bfgLq8ZvT5PWqIe6tDfu4Cs6gwPAv5V9LOYJZhD0VzCLE4NsJg6
+Private-MAC: d1bca120202371a62af6c718c51d15cef782baa2d5bf87beb99585eef66569d3
diff --git a/src/test/resources/ppkv3_ecdsa384_unix.ppk b/src/test/resources/ppkv3_ecdsa384_unix.ppk
new file mode 100644
index 00000000..976bfdb4
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa384_unix.ppk
@@ -0,0 +1,11 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp384
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBnR7wyGL+IO
+T7A7vKr4Do1OGlsMhBFN28GuKUohpkd5rvvZfe/9UNbdobFa38KmLWpxzAlL9Go3
+AK6+7OpWz6HE8JHdrJGyRYoS87XVv4oIIzmp8B8GThUYiDt8Qh8FmA==
+Private-Lines: 2
+AAAAMQDFnuMXVDcejvYI0FcivZkI2cZ750s1tLgc9U/zQUFIYyybb8oHM4UsPeko
+qZKs9xM=
+Private-MAC: 3c36b58ea113af96aac49762d118793c374fcb399f293a3a79d9241e37bb4ab8
diff --git a/src/test/resources/ppkv3_ecdsa384_unix_encrypted.ppk b/src/test/resources/ppkv3_ecdsa384_unix_encrypted.ppk
new file mode 100644
index 00000000..0adb9e71
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa384_unix_encrypted.ppk
@@ -0,0 +1,16 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp384
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBGdfKrIhLUFt
+iMfBOFdF1LulPAF3OKNqlahGbA0bf/LXPf7tmbJUs+GvozsqYAbsMhed1vmwfBwp
+aQdIG97JRSK4MK4yIrCjDjGwqmakX/hg/WUQZPi17463t9zSzFZ8xA==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 72bcd381a51296c255fb63a40cc2113e
+Private-Lines: 2
+ROTdHYRD0zldAv4hvurg1k/BV7QnFgj7IdvsC5OA/kDSJmiXs0yJqyYKzdydS1xW
+DrZV8KwWZO4GmvXUH0byDg==
+Private-MAC: 4139eedb961ba84cb1375927930738cbe9ecf7feff471ae44a567b4264b5bf5c
diff --git a/src/test/resources/ppkv3_ecdsa384_windows.ppk b/src/test/resources/ppkv3_ecdsa384_windows.ppk
new file mode 100644
index 00000000..aaf79d09
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa384_windows.ppk
@@ -0,0 +1,11 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp384
+Encryption: none
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNShRribjCP+
+pkCkAMwdj2LjxNrZjIKHtylcTdohEgfHJTXzub4tbq6odNS3izPwgMEfX6M6Vl5N
+BIstk3c34+kDuJ/a+Cyn7lZ9HZPdDuaKE66PNp3zGK9linfzn3iFBA==
+Private-Lines: 2
+AAAAMEWKgeXFZVyRT28hwsvRRB7H/ZhnII8d9uP0uhPPU4742NGWdsDIYtlEAW+N
+54PC1A==
+Private-MAC: cdca5f9ec10f19900830e393483c95a2f96056545ce560334f37e75c35169ccd
diff --git a/src/test/resources/ppkv3_ecdsa384_windows_encrypted.ppk b/src/test/resources/ppkv3_ecdsa384_windows_encrypted.ppk
new file mode 100644
index 00000000..b0a7c050
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa384_windows_encrypted.ppk
@@ -0,0 +1,16 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp384
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKGDqZyyEHaK
+kg0ijDH9nRu86AiXmUNJcKWC05ksthEMheDRMyZtyqzIIwQI9Goum+Tktu3LeQS4
+fBh9aNHwMP2xN5uvj/uUYxN1dmQmLQ4NsQhpz+DY5TtxHJ/tUKvNpA==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 8de0354d6f5f58738c3756f7691e9b0d
+Private-Lines: 2
+JuLWiPoGeL31caJQj5mk+sUE8BK4JsOqEZt24rIjNMcFU/zVrVD/GxnACvjwRAH7
+mlL0AuHbi/5L0pFOLaz4/w==
+Private-MAC: 477fcc9f7ae260b3ba4715476a56c04871808e9e5353802dfa7b5caa3914c424
diff --git a/src/test/resources/ppkv3_ecdsa521_unix.ppk b/src/test/resources/ppkv3_ecdsa521_unix.ppk
new file mode 100644
index 00000000..fb418278
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa521_unix.ppk
@@ -0,0 +1,12 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp521
+Encryption: none
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADwW+/zw0OO
+BYVS2jEOym6OctYKqb1/uSTegImb7XOcklV6HZSnkGDrp89gzXL6AgoFWMQkySgR
+93l1j22kCwxGWQFQBLalq5ekPGpqBiE31QNot0QKPat2f4O8bw0DO5sUNPmH7ImK
+myxAjHGiCPhi5egoIgrgzHoGnx1ynMr32Pntkg==
+Private-Lines: 2
+AAAAQgDp0Jv8HmHaLVbfLM9oSHMOjGKJ3/uZY9gC8IDuBovmnapY10H8f1Uo9GD9
+Fpo7ZBTXNqAaHAh+KWvFYmUq5TVLWg==
+Private-MAC: de2f0fc2577ac8c553ab52b838f127f3426c8ca9de22cfcbd11cbf5dfe8ff6f6
diff --git a/src/test/resources/ppkv3_ecdsa521_unix_encrypted.ppk b/src/test/resources/ppkv3_ecdsa521_unix_encrypted.ppk
new file mode 100644
index 00000000..0ff44668
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa521_unix_encrypted.ppk
@@ -0,0 +1,17 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp521
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGCln1Y+fp/
+uQTIVhjoNHuuRGVSzHc8shdMTGUQz0q+TpDSEf+SViws/gS0+V73QggV5TcUqOIO
+ra4uD8mGSieuUwHea4ieXw+cbcts9lpYdjWkFmgSGwYfIVyosFZ+aE07We7fNRqS
+UpH5gyBpdSpFKAiBTwtv73FOIwz88I82E4HVQw==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 2eb44562d24a9caf29194f4b74ad0af9
+Private-Lines: 2
+yJ0r68xbDPwIcsHmNsDMTQaWCPbEPF043Sm1oJJ48fVS3/Mid6iBTMffqbM4QdHs
+uVTyA5l4/Rh9GKgb1NZr+NMNthCmbDF34gEAbgc332k=
+Private-MAC: 3234726c3bd4ced9ad412585c8fd287fdf011c655e0d51e441be33180e571bc3
diff --git a/src/test/resources/ppkv3_ecdsa521_windows.ppk b/src/test/resources/ppkv3_ecdsa521_windows.ppk
new file mode 100644
index 00000000..e688ff30
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa521_windows.ppk
@@ -0,0 +1,12 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp521
+Encryption: none
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAA37JVxrRSM
+TbtG8SUUuPzzwjsrumeJD+100IbXoAcXK0mPbGei04TdyMTpfy1SDWL3YND5CSzc
+uHMOjApNKrxJ6wB8A5LE178vFKb7kvYo5SJq+YAbv7utQqlMH4sdKdywGwvdSTSZ
+XCu+mzx9ebdoyRsmPS7vBrJYKcQPHiBDa9iVMA==
+Private-Lines: 2
+AAAAQgFaFuLM1IvKBh6hChBTqbN2px8shq+rd1YV59GcnV6zVlaEgbZ9Lpdz3SZc
+nKVT9/BjJ8Q0IRoko/jl5+fGsVp5Eg==
+Private-MAC: 517745eb8ed80dad2395e80999921c3abcb4690fb01b2bb73e4e232f177fdaed
diff --git a/src/test/resources/ppkv3_ecdsa521_windows_encrypted.ppk b/src/test/resources/ppkv3_ecdsa521_windows_encrypted.ppk
new file mode 100644
index 00000000..66bfc879
--- /dev/null
+++ b/src/test/resources/ppkv3_ecdsa521_windows_encrypted.ppk
@@ -0,0 +1,17 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp521
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAAKniAtk8UO
+BIVa/348YbbZAyiLgJKpbH+2rEB0tHG7YYf+fAAGAZJsE7K7bl68XzunBT1Hguyt
+UpcjzNgOS2hx2AAVJT1ObdU0IK8+B0DTXrntG8BMKpDjpHZJr/RQLEDljHt6kc35
+5TLoKZygF7vUujNjceWFjSmBlFeCbr5Fl4KoXw==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 9c4e53a5c080d688ac149f396ba7872e
+Private-Lines: 2
+mpMJCAaqeTZ13LGQo68IepcXDAd0iq9nYG2pp9V+6dOVtXMHZVDVFsCYhD/vCeaj
+lb247g8fSDyxQGvwk/CwVtq6uZXO/d+0W5K3ZgFOnVI=
+Private-MAC: ec2a55039d54f981a8dc3126e820eac4c358800abdae76d00b91dd7226ec3374
diff --git a/src/test/resources/ppkv3_ed25519_unix.ppk b/src/test/resources/ppkv3_ed25519_unix.ppk
new file mode 100644
index 00000000..8206d31d
--- /dev/null
+++ b/src/test/resources/ppkv3_ed25519_unix.ppk
@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIFrJ3kAPWlhD8uR/Mo1dqhqtJIYACuH6vIh7/oP+
+r8UY
+Private-Lines: 1
+AAAAIMTyr4DIkFlwt9d07Iw2GX2KJ9F90y/GciEu20RwqF0o
+Private-MAC: 37c84944792d7dc96a40c3d92e6a1d3d32a5a238568503d48580bc032a23a678
diff --git a/src/test/resources/ppkv3_ed25519_unix_encrypted.ppk b/src/test/resources/ppkv3_ed25519_unix_encrypted.ppk
new file mode 100644
index 00000000..e72cd5dd
--- /dev/null
+++ b/src/test/resources/ppkv3_ed25519_unix_encrypted.ppk
@@ -0,0 +1,14 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAINnWA1NU5+6rlguJ4vDwB7Ro3wTO+ntaSPfmWIXj
+p384
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: aa1e739cbaa9d86e771fdf499ad93a59
+Private-Lines: 1
+oFIxq7q4Xb3LfYANl6QhPbx/3poc5/tvVG5Fj0BHer9iHh5GrofY3D2P7JcS/j6P
+Private-MAC: c819ba8f6fc792f60400ad71bf577cc9baba13ffa696c610dca162564a405f6b
diff --git a/src/test/resources/ppkv3_ed25519_windows.ppk b/src/test/resources/ppkv3_ed25519_windows.ppk
new file mode 100644
index 00000000..a61f1be9
--- /dev/null
+++ b/src/test/resources/ppkv3_ed25519_windows.ppk
@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIGGj5a3dN4+iyqsAXenA/CzejFmBNyZinnYHhXXc
+jbBV
+Private-Lines: 1
+AAAAIDWvlS89N/MQ+7GMxk5N09vo7X6W8szaJ1mu6C4s9WsE
+Private-MAC: 4eb0763c03faad6289f10d2dfdfeaed6b024b959cad06e1e042000b97e176745
diff --git a/src/test/resources/ppkv3_ed25519_windows_encrypted.ppk b/src/test/resources/ppkv3_ed25519_windows_encrypted.ppk
new file mode 100644
index 00000000..99b825c8
--- /dev/null
+++ b/src/test/resources/ppkv3_ed25519_windows_encrypted.ppk
@@ -0,0 +1,14 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIEy7J6rEkUafMJmtqE8SpWZg/uYx6EcME5FH3E3H
+B3rR
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: b26d22df894298dac8ce32d3189e35d8
+Private-Lines: 1
+mDG3fado6OIW/+xkwWRYgi2/bU29GW1KQvM0otOqWKcJPpoySjtPuuja43FNyh1Z
+Private-MAC: f61593c72cbad92ea1443a8c3b844b8fe52f4feb43f0c03e6f73b84401dbd2a6
diff --git a/src/test/resources/ppkv3_ed448_unix.ppk b/src/test/resources/ppkv3_ed448_unix.ppk
new file mode 100644
index 00000000..4130052a
--- /dev/null
+++ b/src/test/resources/ppkv3_ed448_unix.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-3: ssh-ed448
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADkhFs87N5abAoXAcsLiYTDOCujVJngP6C9j5dSttY/E
+BgCQBMK60a0vt1lF7b7Drby5kqg7+IvkGgA=
+Private-Lines: 2
+AAAAOaA4VTeL/Vg12PIVqPhDcT9qfUT0dIZolnGVRBbU71tD9DyyLbykR5LADjMG
+YVuHR3tKi1qkRyxoAA==
+Private-MAC: c2623760d10dbe581b7dc8dad41d3a40f437fb9ac2787e4a877c654776dfbae5
diff --git a/src/test/resources/ppkv3_ed448_unix_encrypted.ppk b/src/test/resources/ppkv3_ed448_unix_encrypted.ppk
new file mode 100644
index 00000000..f516024a
--- /dev/null
+++ b/src/test/resources/ppkv3_ed448_unix_encrypted.ppk
@@ -0,0 +1,15 @@
+PuTTY-User-Key-File-3: ssh-ed448
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADldNmjRx57BM40rrc4DabuF0L+RhEkOcDC8/jAhtbdT
+O0X+sqgITai5gSk1JGlIrCRIF83Kvs0TKIA=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 42327c0d92de84745042deb048cb888b
+Private-Lines: 2
+MD0ereDMhZNnTbZvgK5h0GlTlXZbdVNJfzaKrHth4q+ZMuZK3ONfTHbMgRKtsuFj
+vRCgMJAJdq1qNCflJavE/w==
+Private-MAC: 9bc62748d045aef6be0de10ecba4b626e41504636a7faf284f6ebac39a66d249
diff --git a/src/test/resources/ppkv3_ed448_windows.ppk b/src/test/resources/ppkv3_ed448_windows.ppk
new file mode 100644
index 00000000..4ff9898b
--- /dev/null
+++ b/src/test/resources/ppkv3_ed448_windows.ppk
@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-3: ssh-ed448
+Encryption: none
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADkVoUhABT+RHfYEGAMKySSyUSoJFj6HHcz87U+io4Dp
+e4ye88DGUU2Lnxfux/WQq61DPHWxQHo4GoA=
+Private-Lines: 2
+AAAAObCkYvIfcF/UyhGmrFg1BIzY+m/1BjSOl/G7BXbjb/afcugNYfD8Mx6c3kLa
+05UFTkTX858guX/lAA==
+Private-MAC: 6f19bb57b0ef41d6abdd1c555a6247507db32e5d022032c9744f3f735bceefe5
diff --git a/src/test/resources/ppkv3_ed448_windows_encrypted.ppk b/src/test/resources/ppkv3_ed448_windows_encrypted.ppk
new file mode 100644
index 00000000..10228846
--- /dev/null
+++ b/src/test/resources/ppkv3_ed448_windows_encrypted.ppk
@@ -0,0 +1,15 @@
+PuTTY-User-Key-File-3: ssh-ed448
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADkdO1HUptQ2ksEmblqqTM8loY2Q/Ep003qRxW753XVa
+z8eh9CejJvTO2FYtyHNib5syBD814By7C4A=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: adfd1dcbca4e55bb379eee8123a41943
+Private-Lines: 2
+7v3Io0Ub3fTnNaE9qHuJ3Ua8KCPBQvDoznZ+ppbJdVV/gDrczoa2teEIQ6qonpYA
+pIjHDQ/56tyVpt/yvkJGAg==
+Private-MAC: 538f86533f231dce675843a305cf28bfe29f73434cb589bd08e6b9410f209a45
diff --git a/src/test/resources/ppkv3_rsa_unix.ppk b/src/test/resources/ppkv3_rsa_unix.ppk
new file mode 100644
index 00000000..5297884e
--- /dev/null
+++ b/src/test/resources/ppkv3_rsa_unix.ppk
@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: none
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCclEvT+gGh6exRwmgPhsN+PLMpcNltztLQ
+1x8iKOsxCCbXWXRxfvvG/ye8lp4kEOWIZ1224UoG1401aNBVTGNUT7SVyAFSdnHG
+7AGgFnLwJcxrsEHLX/f+gj/m4syNJfAg1PrS4O8IeaEnTdql9x4cYkzd+Jpa/OHZ
+Lgltc8SHezhmxrc5Ylm1IMHu6gxJb0WwQ1mIZ44IAIaYvNflal7dP8v//bY+6kvS
+5iARk6hDfiR/wH3926r7U0pB3siNDdz5yq7rjvTg6igtOhQDSnd9Drp0yHjBCyQo
+iDqP1P2jhyfz1iDdf6u71CFhZK20rfbsUUHCjWilB00onvjjIDYJ
+Private-Lines: 14
+AAABAHB5omK58xRA+e3dLW0BzEpRBg3dr4JOkGdOMGIUbKYDCguliZzBr9DJltzE
+gQK9VHSAc/Qbr1Zs3lWgXg732V5GBx2U10ZKKP7Qp3Y8ygGx1T8CFLEn1ffvzkFn
+Z7J4rx6WfzqeM3auEFIwfcC8W9fd0QOeQhrcDsw4YrNJ9sGuYIa/bo3VDNr4sbmc
+odnbHSmxQFam9BV7bfxFcC3CPusvh4BdrT3vkQjlFR+RQOU6nDPpQ3Z3SXp12k3k
+EoHClZuNKP1YCci8+uRWeobOo6rsJKJsmzFYnjNdPDH9ytQm2N7Pg93gB7Sqp27S
+56XLoRSGzCeZqrW1Uc38wj/qX2UAAACBAO/PhzZFQOSOKydp+4d2U0ZM8YumRYYN
+OBCd3XtypcThMjwj09smbZI2bMm7xGySwEW5VIRmry8swvy3bNFtybhlEV3/+SDl
+spCCTKWUZykSCPJvWrt21LMe+DYvZRJhPyhjQvfStIjlDgyuJQVaPghx3KBaglvU
+e6OGaBLbu9+nAAAAgQCnJldXgo0q9RXeSOJtiXKg8Y5ykLGALpkWudU6VdYGurbU
+Q5Bu3gO3MyFeRV/GhHEZ3av0i63tfNBeOcWafm3OW1xaLaJDioHQ/KKlCfs5nXMC
+Z6Qp+56mhk48unpdvOWSe3qorazrgo4XefKa/9nn8O6Im4DDOLPzAWxu8jJyzwAA
+AIEAoX8jMlUD2McXRGBbRu3UoXRir0amRYONcYJN+kCeiptJl3WNETgni2Vt5Cgw
+45mrIDQ3I+HkqhU+0cw6p8n9m4JkBQ7naWpyA32ofxdRPa9h0he5fZj+tDs3IQRY
+OLh5cZMOxHsymrnHRCgLkddr+P976niOWEFc73MFvpxM3qE=
+Private-MAC: 61abe803ffbf8f7ebd9c899ba3ef9279f08b433e9095eb248a28a38b45e6367e
diff --git a/src/test/resources/ppkv3_rsa_unix_encrypted.ppk b/src/test/resources/ppkv3_rsa_unix_encrypted.ppk
new file mode 100644
index 00000000..833eef7d
--- /dev/null
+++ b/src/test/resources/ppkv3_rsa_unix_encrypted.ppk
@@ -0,0 +1,31 @@
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCMTxeJiE+rerxDpf3UhJRVvaWulraY8VZr
+yHhSPnHCODQw3MsQ0X6ECmTs8c3YeNRBRhiaB6zOQorUFqFbAwO2XIFQgDGk9qSJ
+F1kwsVXKnReFqUQ4MQGjRt5odjHc4+00KnmfdLUubvLGJmzHYJ4Ia1+EEyzkUik4
+nVTxdreXrvFEIEej/QHJlhVSqsL2oKXKVMLAJ6OvGt9K1pFSJRCyedHVmR/z3tfs
+iFqpw0R2P2WhhkK9jXG47tJcG9v5HPrGJqd3D+W982/YUFNFvLMVkq5qU4wEDFVh
+1u7Z95dABtZm2TwzhrYfxdAIcYbZLpbFozWX+f2mEQRAbWws2YE7
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 331886f5ea02ebe8d0aac746a408ede7
+Private-Lines: 14
+EpXqXpaS5gbssePDuppbh72EaQb4J17ReVx664sVo479mR07xjj1R24hP20H3FUr
+zYif4C0NuWWqyUCZ4ULyjW5+08m3SgQ5WOva9bPLryWpI6CTDTZ8wVQqkmCgweKt
+JtPVOtcrdCKze7udLa/Ah0PxNkFzsHNfu1aQfG6MnQ9p9orwzfy5A8pm36sxigwj
+i6hHKMdcJY4+eA7Df5CkccVBiZlKr4nTChoSsSmBnLMKNSwR2EftW2H1uk9UzjxU
+5VozBBeTkOqU9p6ph7pez5tDIZPN3krzQETIz1dXhTRrueard5l4slHV7N6W/k7t
+JynUfkhiGfhwKEjD9GutdJ4oTTe1SNz/3ndVzRS/10uSCPEcn2SdL7QzxYG/lY3W
+dAyS6hL8bR3V6NoxdTx/RWUqVrTfI0/dZpUN+h1iSsApB3M8Nth0nFhgsmveSPvT
+K//weuOgfB7AtO0Jn+KG81sW0cBK4AlSr80BzfOhKpkgG5OciSw+P1VRE5eq3bzp
+iRgbn8yKmvVXbY4NSX2Nued4C2z8MLMv1jHPyg7UOPhb52LyBGqWdT5iOURqWF/6
+Az37Tag7iDe8K/tFNBQUcazHNKFPnQpXPYJDo3ucRE2tV8txrhQws3WRvAh6n/Ag
+Sb6Rz9OymLeSh/VAqcrWHb7PanGZGIo9XeWXRKKhU9SF0+vqqdPFCbVsleyVdoa4
+rIB7zOG15Rr81FaIVh2axTsKsMCIMiMNTOO6p55JT7ViVJr566LxgNA2FBZMEdhV
+lXIfxz9JlLaP7a4/rTt4sp00PVorVjx/CeptKCRyptHYA2nM6In/tMHkkxd88l8d
+6JESwNXAZz9JTWplWdMa0e0GjZ7hcazms0gWzd/5pMPAOjnixqqpEulGzmEX8EmK
+Private-MAC: 1120af9034ed0384d24f636c7dcf926fdca1de80490322bc616b7f6bee225098
diff --git a/src/test/resources/ppkv3_rsa_windows.ppk b/src/test/resources/ppkv3_rsa_windows.ppk
new file mode 100644
index 00000000..5b25a317
--- /dev/null
+++ b/src/test/resources/ppkv3_rsa_windows.ppk
@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: none
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCyu2eTD/3HRZnFhKMiy7YLGW7WUsT1uEgR
+CUufHFbjwZqSdUMXplB7wD5g/iombD36oZtFGpZWjiReT8hh2k2dR5pzF7Hg9yTZ
+osuBu8YRSTjwicc/NHTcDfl5kQFSGz40azFXWn1xJqPRQesDGZ2rQVloCxW771AV
+S5hZvAybtM/qf6En7PxHJE4Eg7klvjH2bzhacDQch56u/ij+/ipwtqkWt40k6sMQ
+7xtRIQH4AUNlqQ+3qpZxKfAl+NCbJ/V8Mj9BM3WuyeD6DYPwBT1mHXug9KWi7aii
+2c/UvDv1xpF7u0mTRSW4Dkm7FTEbX8iPP1QF3ze87YEaF1qRkeMf
+Private-Lines: 14
+AAABABoV6fb3xYU47kl6o0inzKjwDFIxgFeQjswVvDi7rR7Omd24SZOBDYwSMch8
+L/CtqZoTzhKW38xAMKSnJkrJzrwewDb+BVMdHv64mNUVb8IEGtBYe4EJCek4DOom
+NLgkC78gyfHC3j4OcqhO45rvOSjOf9sEiyaDtf91qgWmwXxT9yfLeSXn+tbEBpez
+1sUHw0Pk+Io2bG+jwrCEeA/sf0zBfoLAREAe9CcdZmhKpoXRadaN3falLpwH5fOk
+wCnnjdxRCFgLpDZXsc3iA34hhM/mO7h2bnghe6h2KSrqgWJRdGL/+OpclJOx+l8s
+L+n9n1/SMJtfLZ8mn6iZd74GAlEAAACBAPtVtmpNFNxw4S+arkhJGo5qplFQ6RNj
+bbaCQWv7g7sYfZQuzRyq+ar4hn4Y71x8mLJKqlBNebtfctq0SzCSvFc/jOLqX+LH
+/haOrM7WKh8QFVQ8UwtZZdzXImViEw4W1HGJvoLe/MksakTl6afYabLBsDUI04Fd
+RekXhcy7sC5ZAAAAgQC2DLMio0Bgvz+hPotfHuSrBb49Gyqc/Nzt3jvFzvqFywU0
+lpXPjy/nCN7fEVuCusLaEM5KY0giQaLna/xDfcoFmpdfQR79E2NFu88nRf1nr4e/
+y/2etw0Pl1Eh/roWDjBi5bFpDBU3eDjF01ylcIbDTb6klJ4PON3gdHJx4WWeNwAA
+AIBFMs9P/P9EIFebF91b4wvJTG1xS3b6aLE0jIHNv4inD/6u/4R8U3meGMlvzeZQ
+xI56n6Hk8IaCEXehAj+VAfp9z2YHtPoAfUD8mbCKdBzgpW1P1AeOTIzvsi3+bHbm
+ZM+e6cTsQGaZ9OJSRYCmGT7LiTRm6PQwyq+4FEP1t9VF3w==
+Private-MAC: 20cf1acc614037e76bbc47942988e0aa3bc5582df1a173be2c468149139a406a
diff --git a/src/test/resources/ppkv3_rsa_windows_encrypted.ppk b/src/test/resources/ppkv3_rsa_windows_encrypted.ppk
new file mode 100644
index 00000000..a1190ad6
--- /dev/null
+++ b/src/test/resources/ppkv3_rsa_windows_encrypted.ppk
@@ -0,0 +1,31 @@
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: aes256-cbc
+Comment: test
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCnTnSVyVw7nyqnxy/WWUUuFswXUsdwvNto
+jMVuqltV3CTDHfX6+uMu+XBC6Gqr/AzHvwzOPvzR+/k+m5fV5zLvjfe3zTU1gs5E
+mOtu3YbOnn6il/IvVvU9ILq/5Dg89aIbSYG9R28iu8KciZxHOOpjgFpmoCiMAg8x
+4OWRHc6irKj5VKTXMJC+5pmb4QZlSKal7KPqKyAsKII6IxMjpPNJejZm/Omw3QZ1
+9YIN02ttbM8rwoLuPLi1DoZiAUvF7J3XTEUcDAn5y5upltk1Dx+bWhuth/sYZ26q
+ksxPr+KmtHwJ5R8pWJ4CXtENlwxQiAWftVWW8MeQlyN17qPW+Z9x
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 8bfc7fec5e9a97b5c271a2ff59bbd42a
+Private-Lines: 14
+BdVaIWyb2IRejqogeZj7d55V+oiRVB87REDIRT+2nsqmRpv8d9wkEZxa2hZD4JJI
+yPJoLHyl6LwKrGWwwCDXf0DHTePNAAASAcp5G60XY/rAz/DMtYUfjFKYfOB3SjUr
+G1hjswV+RHm1OT9g5lz7tkeeF5jd3JkZ+6vdqeTxKrosUdqkLkN9SDbVtyLnEnfK
+up5RVoMN6jd8UZG90HIsuwR8/G4UwYPwKpj/5RwjKUx26yKPyOCl7tZcrZbA8+fw
+UNsODwlFSNdU4roo1SrmVKaDQ5LtV+HXunIOTuxt0p/YfvFcLupgaWgMysXzmhja
+sAQyZt5RB7rJOlWXKVOXdd0UWwYOhYTgTItY7tljns8SffOQMt0j+DoLZdMJq1Ej
+qWIy53yqCUs1Nyjqv9J9ptUCUiDP0xM50Qeob2YpFK1gpOpPTBOD1GrhF9P33uYT
+lhlsz8Wvh4U6txuO3DaPkfmNn4t/0lVdBh6x2Tv1x7OWsebmUQy1Pj5ffJJ6N4wt
+UbGdAeiJ2IsGzHHRThmBrjCTxI14QTjIrNpl4Ntbj7x2vPXHVoDfgUJrkeFD8NxW
+NaTn+WdYRtod3wLcdM6YAf+rBtGs4gGWK+eKPlepJhia9onRc7Esue/svHYTVgm4
+jdPhQyHPvunLfN+ryD1yZkCRfir0c4HP7sniolfhVYM2UBvzBj+HiauEEWXetFeM
+96Cdian5ouUtwvKmoIR0K5KT7g7iDtH94WreGK0Le13EGV9gwEk33HEenbcpn7Db
+0ScHiwhHp4sXsiXh75b1kEWAJZVe8B6xbwLegIZesXvDztCMYq1RToBJ7rrMdEoN
+uRkinI6LFI5JzxhLDRpsmfSEJQM1SqxRmyoBVn6Jrd02giL1CxuadULsNc4LV4B9
+Private-MAC: 7531943a8f9d37c31a318dbacb9de07b14346541d5b100bd665b36a195daf60f
diff --git a/src/test/scala/Adler32Test.scala b/src/test/scala/Adler32Test.scala
deleted file mode 100644
index c25a86a2..00000000
--- a/src/test/scala/Adler32Test.scala
+++ /dev/null
@@ -1,72 +0,0 @@
-/* -*-mode:scala; c-basic-offset:2; indent-tabs-mode:nil -*- */
-package com.jcraft.jsch.jzlib
-
-import org.scalatest._
-import org.scalatest.flatspec._
-import matchers.should._
-
-import java.util.zip.{Adler32 => juzAdler32}
-
-class Adler32Test extends AnyFlatSpec with BeforeAndAfter with Matchers {
- private var adler: Adler32 = _
-
- before {
- adler = new Adler32
- }
-
- after {
- }
-
- behavior of "Adler32"
-
- it must "be compatible with java.util.zip.Adler32." in {
- val buf1 = randombuf(1024)
- val juza = new juzAdler32
- val expected = {
- juza.update(buf1, 0, buf1.length)
- juza.getValue
- }
- val actual = getValue(List(buf1));
-
- actual should equal (expected)
- }
-
- it can "copy itself." in {
- val buf1 = randombuf(1024)
- val buf2 = randombuf(1024)
-
- val adler1 = new Adler32
-
- adler1.update(buf1, 0, buf1.length);
-
- val adler2 = adler1.copy
-
- adler1.update(buf2, 0, buf1.length);
- adler2.update(buf2, 0, buf1.length);
-
- val expected = adler1.getValue
- val actual = adler2.getValue
-
- actual should equal (expected)
- }
-
- it can "combine adler values." in {
-
- val buf1 = randombuf(1024)
- val buf2 = randombuf(1024)
-
- val adler1 = getValue(List(buf1));
- val adler2 = getValue(List(buf2));
- val expected = getValue(List(buf1, buf2));
-
- val actual = Adler32.combine(adler1, adler2, buf2.length)
-
- actual should equal (expected)
- }
-
- private def getValue(buf:Seq[Array[Byte]]) = synchronized {
- adler.reset
- buf.foreach { b => adler.update(b, 0, b.length) }
- adler.getValue
- }
-}
diff --git a/src/test/scala/CRC32Test.scala b/src/test/scala/CRC32Test.scala
deleted file mode 100644
index a6aabdce..00000000
--- a/src/test/scala/CRC32Test.scala
+++ /dev/null
@@ -1,72 +0,0 @@
-/* -*-mode:scala; c-basic-offset:2; indent-tabs-mode:nil -*- */
-package com.jcraft.jsch.jzlib
-
-import org.scalatest._
-import org.scalatest.flatspec._
-import matchers.should._
-
-import java.util.zip.{CRC32 => juzCRC32}
-
-class CRC32Test extends AnyFlatSpec with BeforeAndAfter with Matchers {
- private var crc: CRC32 = _
-
- before {
- crc = new CRC32
- }
-
- after {
- }
-
- behavior of "CRC32"
-
- it must "be compatible with java.util.zip.CRC32." in {
- val buf1 = randombuf(1024)
- val juza = new juzCRC32
- val expected = {
- juza.update(buf1, 0, buf1.length)
- juza.getValue
- }
- val actual = getValue(List(buf1));
-
- actual should equal (expected)
- }
-
- it can "copy itself." in {
- val buf1 = randombuf(1024)
- val buf2 = randombuf(1024)
-
- val crc1 = new CRC32
-
- crc1.update(buf1, 0, buf1.length);
-
- val crc2 = crc1.copy
-
- crc1.update(buf2, 0, buf1.length);
- crc2.update(buf2, 0, buf1.length);
-
- val expected = crc1.getValue
- val actual = crc2.getValue
-
- actual should equal (expected)
- }
-
- it can "combine crc values." in {
-
- val buf1 = randombuf(1024)
- val buf2 = randombuf(1024)
-
- val crc1 = getValue(List(buf1));
- val crc2 = getValue(List(buf2));
- val expected = getValue(List(buf1, buf2));
-
- val actual = CRC32.combine(crc1, crc2, buf2.length)
-
- actual should equal (expected)
- }
-
- private def getValue(buf:Seq[Array[Byte]]) = synchronized {
- crc.reset
- buf.foreach { b => crc.update(b, 0, b.length) }
- crc.getValue
- }
-}
diff --git a/src/test/scala/DeflateInflateTest.scala b/src/test/scala/DeflateInflateTest.scala
deleted file mode 100644
index eaadb26d..00000000
--- a/src/test/scala/DeflateInflateTest.scala
+++ /dev/null
@@ -1,337 +0,0 @@
-/* -*-mode:scala; c-basic-offset:2; indent-tabs-mode:nil -*- */
-package com.jcraft.jsch.jzlib
-
-import org.scalatest._
-import org.scalatest.flatspec._
-import matchers.should._
-
-import JZlib._
-
-class DeflateInflateTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
- val comprLen = 40000
- val uncomprLen = comprLen
- var compr:Array[Byte] = _
- var uncompr:Array[Byte] = _
-
- var deflater: Deflater = _
- var inflater: Inflater = _
- var err: Int = _
-
- before {
- compr = new Array[Byte](comprLen)
- uncompr = new Array[Byte](uncomprLen)
-
- deflater = new Deflater
- inflater = new Inflater
-
- err = Z_OK
- }
-
- after {
- }
-
- behavior of "Deflter and Inflater"
-
- it can "deflate and infate data in the large buffer." in {
- val data = "hello, hello!".getBytes
-
- err = deflater.init(Z_BEST_SPEED)
- err should equal (Z_OK)
-
- deflater.setInput(uncompr)
- deflater.setOutput(compr)
-
- err = deflater.deflate(Z_NO_FLUSH)
- err should equal (Z_OK)
-
- deflater.avail_in should equal (0)
-
- deflater.params(Z_NO_COMPRESSION, Z_DEFAULT_STRATEGY)
- deflater.setInput(compr)
- deflater.avail_in = comprLen/2
-
- err = deflater.deflate(Z_NO_FLUSH)
- err should equal (Z_OK)
-
- deflater.params(Z_BEST_COMPRESSION, Z_FILTERED)
- deflater.setInput(uncompr)
- deflater.avail_in = uncomprLen
-
- err = deflater.deflate(Z_NO_FLUSH)
- err should equal (Z_OK)
-
- err = deflater.deflate(JZlib.Z_FINISH);
- err should equal (Z_STREAM_END)
-
- err = deflater.end
- err should equal (Z_OK)
-
- inflater.setInput(compr)
-
- err = inflater.init
- err should equal (Z_OK)
-
- var loop = true
- while(loop) {
- inflater.setOutput(uncompr)
- err = inflater.inflate(Z_NO_FLUSH)
- if(err == Z_STREAM_END) loop = false
- else err should equal (Z_OK)
- }
-
- err = inflater.end
- err should equal (Z_OK)
-
- val total_out = inflater.total_out.asInstanceOf[Int]
-
- total_out should equal (2*uncomprLen + comprLen/2)
- }
-
- it can "deflate and infate data in the small buffer." in {
- val data = "hello, hello!".getBytes
-
- err = deflater.init(Z_DEFAULT_COMPRESSION)
- err should equal (Z_OK)
-
- deflater.setInput(data)
- deflater.setOutput(compr)
-
- while(deflater.total_in < data.length &&
- deflater.total_out < comprLen){
- deflater.avail_in = 1
- deflater.avail_out = 1
- err = deflater.deflate(Z_NO_FLUSH)
- err should equal (Z_OK)
- }
-
- do {
- deflater.avail_out = 1
- err = deflater.deflate(Z_FINISH)
- }
- while(err != Z_STREAM_END);
-
- err = deflater.end
- err should equal (Z_OK)
-
- inflater.setInput(compr)
- inflater.setOutput(uncompr)
-
- err = inflater.init
- err should equal (Z_OK)
-
- var loop = true
- while(inflater.total_out
- loop = false
- case Z_NEED_DICT =>
- dictID should equal (inflater.getAdler)
- err = inflater.setDictionary(dictionary, dictionary.length);
- err should equal (Z_OK)
- case _ =>
- err should equal (Z_OK)
- }
- }
- while(loop)
-
- err = inflater.end
- err should equal (Z_OK)
-
- val total_out = inflater.total_out.asInstanceOf[Int]
- val actual = new Array[Byte](total_out)
- System.arraycopy(uncompr, 0, actual, 0, total_out)
-
- actual should equal (hello)
- }
-
- it should "support the sync." in {
- val hello = "hello".getBytes
-
- err = deflater.init(Z_DEFAULT_COMPRESSION)
- err should equal (Z_OK)
-
- deflater.setInput(hello)
- deflater.avail_in = 3;
- deflater.setOutput(compr)
-
- err = deflater.deflate(Z_FULL_FLUSH)
- err should equal (Z_OK)
-
- compr(3) = (compr(3) + 1).asInstanceOf[Byte]
- deflater.avail_in = hello.length - 3;
-
- err = deflater.deflate(Z_FINISH)
- err should equal (Z_STREAM_END)
- val comprLen= deflater.total_out.asInstanceOf[Int]
-
- err = deflater.end
- err should equal (Z_OK)
-
- err = inflater.init
- err should equal (Z_OK)
-
- inflater.setInput(compr)
- inflater.avail_in = 2
-
- inflater.setOutput(uncompr)
-
- err = inflater.inflate(JZlib.Z_NO_FLUSH)
- err should equal (Z_OK)
-
- inflater.avail_in = comprLen-2
- err = inflater.sync
-
- err = inflater.inflate(Z_FINISH)
- err should equal (Z_DATA_ERROR)
-
- err = inflater.end
- err should equal (Z_OK)
-
- val total_out = inflater.total_out.asInstanceOf[Int]
- val actual = new Array[Byte](total_out)
- System.arraycopy(uncompr, 0, actual, 0, total_out)
-
- "hel"+new String(actual) should equal (new String(hello))
- }
-
- behavior of "Inflater"
-
- it can "inflate gzip data." in {
- val hello = "foo".getBytes
- val data = List(0x1f, 0x8b, 0x08, 0x18, 0x08, 0xeb, 0x7a, 0x0b, 0x00, 0x0b,
- 0x58, 0x00, 0x59, 0x00, 0x4b, 0xcb, 0xcf, 0x07, 0x00, 0x21,
- 0x65, 0x73, 0x8c, 0x03, 0x00, 0x00, 0x00).
- map(_.asInstanceOf[Byte]).
- toArray
-
- err = inflater.init(15 + 32)
- err should equal (Z_OK)
-
- inflater.setInput(data)
- inflater.setOutput(uncompr)
-
- val comprLen = data.length
-
- var loop = true
- while(inflater.total_out BAOS, ByteArrayInputStream => BAIS}
-
-import JZlib._
-
-class DeflaterInflaterStreamTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
-
- before {
- }
-
- after {
- }
-
- behavior of "Deflter and Inflater"
-
- it can "deflate and infate data one by one." in {
- val data1 = randombuf(1024)
- implicit val buf = new Array[Byte](1)
-
- val baos = new BAOS
- val gos = new DeflaterOutputStream(baos)
- data1 -> gos
- gos.close
-
- val baos2 = new BAOS
- new InflaterInputStream(new BAIS(baos.toByteArray)) -> baos2
- val data2 = baos2.toByteArray
-
- data2.length should equal (data1.length)
- data2 should equal (data1)
- }
-
- behavior of "DeflterOutputStream and InflaterInputStream"
-
- it can "deflate and infate." in {
-
- (1 to 100 by 3).foreach { i =>
-
- implicit val buf = new Array[Byte](i)
-
- val data1 = randombuf(10240)
-
- val baos = new BAOS
- val gos = new DeflaterOutputStream(baos)
- data1 -> gos
- gos.close
-
- val baos2 = new BAOS
- new InflaterInputStream(new BAIS(baos.toByteArray)) -> baos2
- val data2 = baos2.toByteArray
-
- data2.length should equal (data1.length)
- data2 should equal (data1)
- }
- }
-
- behavior of "Deflter and Inflater"
-
- it can "deflate and infate nowrap data." in {
-
- (1 to 100 by 3).foreach { i =>
-
- implicit val buf = new Array[Byte](i)
-
- val data1 = randombuf(10240)
-
- val baos = new BAOS
- val deflater = new Deflater(JZlib.Z_DEFAULT_COMPRESSION,
- JZlib.DEF_WBITS,
- true)
- val gos = new DeflaterOutputStream(baos, deflater)
- data1 -> gos
- gos.close
-
- val baos2 = new BAOS
- val inflater = new Inflater(JZlib.DEF_WBITS, true)
- new InflaterInputStream(new BAIS(baos.toByteArray), inflater) -> baos2
- val data2 = baos2.toByteArray
-
- data2.length should equal (data1.length)
- data2 should equal (data1)
- }
- }
-
- it can "deflate and infate nowrap data with MAX_WBITS." in {
- implicit val buf = new Array[Byte](100)
-
- List(randombuf(10240),
- """{"color":2,"id":"EvLd4UG.CXjnk35o1e8LrYYQfHu0h.d*SqVJPoqmzXM::Ly::Snaps::Store::Commit"}""".getBytes) foreach { data1 =>
-
- val deflater = new Deflater(JZlib.Z_DEFAULT_COMPRESSION,
- JZlib.MAX_WBITS,
- true)
-
- val inflater = new Inflater(JZlib.MAX_WBITS, true)
-
- val baos = new BAOS
- val gos = new DeflaterOutputStream(baos, deflater)
- data1 -> gos
- gos.close
-
- val baos2 = new BAOS
- new InflaterInputStream(new BAIS(baos.toByteArray), inflater) -> baos2
- val data2 = baos2.toByteArray
-
- data2.length should equal (data1.length)
- data2 should equal (data1)
- }
- }
-}
diff --git a/src/test/scala/WrapperTypeTest.scala b/src/test/scala/WrapperTypeTest.scala
deleted file mode 100644
index 09ee0b81..00000000
--- a/src/test/scala/WrapperTypeTest.scala
+++ /dev/null
@@ -1,210 +0,0 @@
-/* -*-mode:scala; c-basic-offset:2; indent-tabs-mode:nil -*- */
-package com.jcraft.jsch.jzlib
-
-import org.scalatest._
-import org.scalatest.flatspec._
-import matchers.should._
-
-import scala.language.reflectiveCalls
-
-import java.io.{ByteArrayOutputStream => BAOS, ByteArrayInputStream => BAIS}
-
-import JZlib._
-
-class WrapperTypeTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
- val data = "hello, hello!".getBytes
-
- val comprLen = 40000
- val uncomprLen = comprLen
- var compr:Array[Byte] = _
- var uncompr:Array[Byte] = _
- var err: Int = _
-
- val cases = /* success */ /* fail */
- List((W_ZLIB, (List(W_ZLIB, W_ANY), List(W_GZIP, W_NONE))),
- (W_GZIP, (List(W_GZIP, W_ANY), List(W_ZLIB, W_NONE))),
- (W_NONE, (List(W_NONE, W_ANY), List(W_ZLIB, W_GZIP))))
-
- before {
- compr = new Array[Byte](comprLen)
- uncompr = new Array[Byte](uncomprLen)
-
- err = Z_OK
- }
-
- after {
- }
-
- behavior of "Deflater"
-
- it can "detect data type of input." in {
- implicit val buf = compr
-
- cases foreach { case (iflag, (good, bad)) =>
- val baos = new BAOS
- val deflater = new Deflater(Z_DEFAULT_COMPRESSION, DEF_WBITS, 9, iflag)
- val gos = new DeflaterOutputStream(baos, deflater)
- data -> gos
- gos.close
-
- val deflated = baos.toByteArray
-
- good map { w =>
- val baos2 = new BAOS
- val inflater = new Inflater(w)
- new InflaterInputStream(new BAIS(deflated), inflater) -> baos2
- val data1 = baos2.toByteArray
- data1.length should equal (data.length)
- data1 should equal (data)
- import inflater._
- (avail_in, avail_out, total_in, total_out)
- } reduceLeft { (x, y) => x should equal (y); x }
-
- bad foreach { w =>
- val baos2 = new BAOS
- val inflater = new Inflater(w)
- try {
- new InflaterInputStream(new BAIS(deflated), inflater) -> baos2
- fail("unreachable")
- }
- catch {
- case e:java.io.IOException =>
- }
- }
- }
- }
-
- behavior of "ZStream"
-
- it can "detect data type of input." in {
- cases foreach { case (iflag, (good, bad)) =>
- val deflater = new ZStream
-
- err = deflater.deflateInit(Z_BEST_SPEED, DEF_WBITS, 9, iflag)
- err should equal (Z_OK)
-
- deflate(deflater, data, compr)
-
- good foreach { w =>
- val inflater = inflate(compr, uncompr, w)
- val total_out = inflater.total_out.asInstanceOf[Int]
- new String(uncompr, 0, total_out) should equal (new String(data))
- }
-
- bad foreach { w =>
- inflate_fail(compr, uncompr, w)
- }
- }
- }
-
- behavior of "Deflater"
-
- it should "support wbits+32." in {
-
- var deflater = new Deflater
- err = deflater.init(Z_BEST_SPEED, DEF_WBITS, 9)
- err should equal (Z_OK)
-
- deflate(deflater, data, compr)
-
- var inflater = new Inflater
- err = inflater.init(DEF_WBITS + 32)
- err should equal (Z_OK)
-
- inflater.setInput(compr)
-
- var loop = true
- while(loop) {
- inflater.setOutput(uncompr)
- err = inflater.inflate(Z_NO_FLUSH)
- if(err == Z_STREAM_END) loop = false
- else err should equal (Z_OK)
- }
- err = inflater.end
- err should equal (Z_OK)
-
- var total_out = inflater.total_out.asInstanceOf[Int]
- new String(uncompr, 0, total_out) should equal (new String(data))
-
- deflater = new Deflater
- err = deflater.init(Z_BEST_SPEED, DEF_WBITS + 16, 9)
- err should equal (Z_OK)
-
- deflate(deflater, data, compr)
-
- inflater = new Inflater
- err = inflater.init(DEF_WBITS + 32)
- err should equal (Z_OK)
-
- inflater.setInput(compr)
-
- loop = true
- while(loop) {
- inflater.setOutput(uncompr)
- err = inflater.inflate(Z_NO_FLUSH)
- if(err == Z_STREAM_END) loop = false
- else err should equal (Z_OK)
- }
- err = inflater.end
- err should equal (Z_OK)
-
- total_out = inflater.total_out.asInstanceOf[Int]
- new String(uncompr, 0, total_out) should equal (new String(data))
- }
-
- private def deflate(deflater: ZStream,
- data: Array[Byte], compr: Array[Byte]) = {
- deflater.setInput(data)
- deflater.setOutput(compr)
-
- err = deflater.deflate(JZlib.Z_FINISH)
- err should equal (Z_STREAM_END)
-
- err = deflater.end
- err should equal (Z_OK)
- }
-
- private def inflate(compr: Array[Byte],
- uncompr: Array[Byte],
- w: WrapperType) = {
- val inflater = new ZStream
- err = inflater.inflateInit(w)
- err should equal (Z_OK)
-
- inflater.setInput(compr)
-
- var loop = true
- while(loop) {
- inflater.setOutput(uncompr)
- err = inflater.inflate(Z_NO_FLUSH)
- if(err == Z_STREAM_END) loop = false
- else err should equal (Z_OK)
- }
- err = inflater.end
- err should equal (Z_OK)
-
- inflater
- }
-
- private def inflate_fail(compr: Array[Byte],
- uncompr: Array[Byte],
- w: WrapperType) = {
- val inflater = new ZStream
-
- err = inflater.inflateInit(w)
- err should equal (Z_OK)
-
- inflater.setInput(compr)
-
- var loop = true
- while(loop) {
- inflater.setOutput(uncompr)
- err = inflater.inflate(Z_NO_FLUSH)
- if(err == Z_STREAM_END) loop = false
- else {
- err should equal (Z_DATA_ERROR)
- loop = false
- }
- }
- }
-}
diff --git a/src/test/scala/package.scala b/src/test/scala/package.scala
deleted file mode 100644
index 1ff940bd..00000000
--- a/src/test/scala/package.scala
+++ /dev/null
@@ -1,40 +0,0 @@
-/* -*-mode:scala; c-basic-offset:2; indent-tabs-mode:nil -*- */
-package com.jcraft.jsch
-
-import scala.language.implicitConversions
-import scala.language.postfixOps
-import scala.language.reflectiveCalls
-
-import java.io._
-
-package object jzlib {
- implicit def readIS(is: InputStream) = new {
- def ->(out: OutputStream)
- (implicit buf: Array[Byte] = new Array[Byte](1024)) = {
- LazyList.
- continually(is.read(buf)).
- takeWhile(-1 !=).
- foreach(i => out.write(buf, 0, i))
- is.close
- }
- }
-
- // reading a resource file
- implicit def fromResource(str: String ) = new {
- def fromResource: Array[Byte] =
- io.Source.
- fromURL(getClass.getResource(str))(io.Codec.ISO8859).
- map(_.toByte).
- toArray
- }
-
- implicit def readArray(is: Array[Byte]) = new {
- def ->(out: OutputStream)(implicit buf: Array[Byte]) = {
- new ByteArrayInputStream(is) -> (out)
- }
- }
-
- def randombuf(n: Int) = (0 to n).map{ _ =>
- util.Random.nextLong().asInstanceOf[Byte]
- }.toArray
-}