From 48816f59e364f97fd580c652a282006f92b9d1b2 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Thu, 9 Jun 2022 09:07:46 -0500 Subject: [PATCH 1/2] Replaced PKCS5 parsing with PKCS8 - Moved tests for PEM-encoded PKCS1 files to PKCS8 - Removed PKCS5 Key File implementation --- .../java/net/schmizz/sshj/DefaultConfig.java | 3 - src/main/java/net/schmizz/sshj/SSHClient.java | 2 +- .../sshj/userauth/keyprovider/KeyFormat.java | 3 +- .../userauth/keyprovider/KeyProviderUtil.java | 12 +- .../userauth/keyprovider/PKCS5KeyFile.java | 272 ------------------ .../userauth/keyprovider/PKCS8KeyFile.java | 4 +- .../keyprovider/FileKeyProviderSpec.groovy | 2 +- .../sshj/keyprovider/KeyProviderUtilTest.java | 6 +- .../sshj/keyprovider/PKCS5KeyFileTest.java | 91 ------ .../sshj/keyprovider/PKCS8KeyFileTest.java | 65 ++--- .../resources/keyformats/{pkcs5 => pkcs1-rsa} | 0 .../pkcs1-rsa-encrypted} | 0 12 files changed, 43 insertions(+), 417 deletions(-) delete mode 100644 src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java delete mode 100644 src/test/java/net/schmizz/sshj/keyprovider/PKCS5KeyFileTest.java rename src/test/resources/keyformats/{pkcs5 => pkcs1-rsa} (100%) rename src/test/resources/{rsa.pk5 => keyformats/pkcs1-rsa-encrypted} (100%) diff --git a/src/main/java/net/schmizz/sshj/DefaultConfig.java b/src/main/java/net/schmizz/sshj/DefaultConfig.java index ce43c33f1..e6086c12c 100644 --- a/src/main/java/net/schmizz/sshj/DefaultConfig.java +++ b/src/main/java/net/schmizz/sshj/DefaultConfig.java @@ -36,11 +36,9 @@ import net.schmizz.sshj.transport.kex.DHGexSHA1; import net.schmizz.sshj.transport.kex.DHGexSHA256; import net.schmizz.sshj.transport.kex.ECDHNistP; -import net.schmizz.sshj.transport.random.BouncyCastleRandom; import net.schmizz.sshj.transport.random.JCERandom; import net.schmizz.sshj.transport.random.SingletonRandomFactory; import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; -import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile; import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile; import org.slf4j.Logger; @@ -162,7 +160,6 @@ protected void initFileKeyProviderFactories(boolean bouncyCastleRegistered) { setFileKeyProviderFactories( new OpenSSHKeyV1KeyFile.Factory(), new PKCS8KeyFile.Factory(), - new PKCS5KeyFile.Factory(), new OpenSSHKeyFile.Factory(), new PuTTYKeyFile.Factory()); } diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java index f531012a5..0829f3fde 100644 --- a/src/main/java/net/schmizz/sshj/SSHClient.java +++ b/src/main/java/net/schmizz/sshj/SSHClient.java @@ -551,7 +551,7 @@ public KeyProvider loadKeys(String location, char[] passphrase) * Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported: * diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyFormat.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyFormat.java index 6b3f5e2a4..f74e6e1d3 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyFormat.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyFormat.java @@ -16,10 +16,9 @@ package net.schmizz.sshj.userauth.keyprovider; /** -* @version $Id:$ +* Key File Formats */ public enum KeyFormat { - PKCS5, PKCS8, OpenSSH, OpenSSHv1, diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java index dd94e73cd..b1ba25d56 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java @@ -27,9 +27,9 @@ public class KeyProviderUtil { *

* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package. * - * @param location + * @param location File Path to key * @return name of the key file format - * @throws java.io.IOException + * @throws java.io.IOException Thrown on file processing failures */ public static KeyFormat detectKeyFileFormat(File location) throws IOException { @@ -45,7 +45,7 @@ public static KeyFormat detectKeyFileFormat(File location) * @param privateKey Private key stored in a string * @param separatePubKey Is the public key stored separately from the private key * @return name of the key file format - * @throws java.io.IOException + * @throws java.io.IOException Thrown on file processing failures */ public static KeyFormat detectKeyFileFormat(String privateKey, boolean separatePubKey) throws IOException { @@ -60,7 +60,7 @@ public static KeyFormat detectKeyFileFormat(String privateKey, boolean separateP * @param privateKey Private key accessible through a {@code Reader} * @param separatePubKey Is the public key stored separately from the private key * @return name of the key file format - * @throws java.io.IOException + * @throws java.io.IOException Thrown on file processing failures */ public static KeyFormat detectKeyFileFormat(Reader privateKey, boolean separatePubKey) throws IOException { @@ -94,10 +94,8 @@ private static KeyFormat keyFormatFromHeader(String header, boolean separatePubK } else if (separatePubKey) { // Can delay asking for password since have unencrypted pubkey return KeyFormat.OpenSSH; - } else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) { - return KeyFormat.PKCS8; } else { - return KeyFormat.PKCS5; + return KeyFormat.PKCS8; } } else if (header.startsWith("PuTTY-User-Key-File-")) { return KeyFormat.PuTTY; diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java deleted file mode 100644 index a88b1b9a1..000000000 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (C)2009 - SSHJ Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.schmizz.sshj.userauth.keyprovider; - -import com.hierynomus.sshj.common.KeyAlgorithm; -import com.hierynomus.sshj.transport.cipher.BlockCiphers; -import net.schmizz.sshj.common.Base64; -import net.schmizz.sshj.common.ByteArrayUtils; -import net.schmizz.sshj.common.IOUtils; -import net.schmizz.sshj.common.KeyType; -import net.schmizz.sshj.transport.cipher.*; -import net.schmizz.sshj.transport.digest.Digest; -import net.schmizz.sshj.transport.digest.MD5; - -import java.io.BufferedReader; -import java.io.EOFException; -import java.io.IOException; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.security.*; -import java.security.spec.*; -import java.util.Arrays; - -/** - * Represents a PKCS5-encoded key file. This is the format typically used by OpenSSH, OpenSSL, Amazon, etc. - */ -public class PKCS5KeyFile extends BaseFileKeyProvider { - - public static class Factory - implements net.schmizz.sshj.common.Factory.Named { - - @Override - public FileKeyProvider create() { - return new PKCS5KeyFile(); - } - - @Override - public String getName() { - return "PKCS5"; - } - } - - /** - * Indicates a format issue with PKCS5 data - */ - public static class FormatException - extends IOException { - - FormatException(String msg) { - super(msg); - } - } - - /** - * Indicates a problem decrypting the data - */ - public static class DecryptException - extends IOException { - - DecryptException(String msg) { - super(msg); - } - } - - protected byte[] data; - - protected KeyPair readKeyPair() - throws IOException { - - BufferedReader reader = new BufferedReader(resource.getReader()); - try { - String line = null; - Cipher cipher = new NoneCipher(); - StringBuffer sb = new StringBuffer(); - byte[] iv = new byte[0]; // salt - while ((line = reader.readLine()) != null) { - if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) { - int end = line.length() - 17; - if (end > 11) { - String s = line.substring(11, line.length() - 17); - if ("RSA".equals(s)) { - type = KeyType.RSA; - } else if ("DSA".equals(s)) { - type = KeyType.DSA; - } else if ("DSS".equals(s)) { - type = KeyType.DSA; - } else { - throw new FormatException("Unrecognized PKCS5 key type"); - } - } else { - throw new FormatException("Bad header; possibly PKCS8 format?"); - } - } else if (line.startsWith("-----END")) { - break; - } else if (type != null) { - if (line.startsWith("Proc-Type: ")) { - if (!"4,ENCRYPTED".equals(line.substring(11))) { - throw new FormatException("Unrecognized Proc-Type"); - } - } else if (line.startsWith("DEK-Info: ")) { - int ptr = line.indexOf(","); - if (ptr == -1) { - throw new FormatException("Unrecognized DEK-Info"); - } else { - String algorithm = line.substring(10, ptr); - if ("DES-EDE3-CBC".equals(algorithm)) { - cipher = BlockCiphers.TripleDESCBC().create(); - } else if ("AES-128-CBC".equals(algorithm)) { - cipher = BlockCiphers.AES128CBC().create(); - } else if ("AES-192-CBC".equals(algorithm)) { - cipher = BlockCiphers.AES192CBC().create(); - } else if ("AES-256-CBC".equals(algorithm)) { - cipher = BlockCiphers.AES256CBC().create(); - } else { - throw new FormatException("Not a supported algorithm: " + algorithm); - } - iv = Arrays.copyOfRange(ByteArrayUtils.parseHex(line.substring(ptr + 1)), 0, cipher.getIVSize()); - } - } else if (line.length() > 0) { - sb.append(line); - } - } - } - if (type == null) { - throw new FormatException("PKCS5 header not found"); - } - ASN1Data asn = new ASN1Data(data = decrypt(Base64.decode(sb.toString()), cipher, iv)); - switch (type) { - case RSA: { - KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.RSA); - asn.readNext(); - BigInteger modulus = asn.readNext(); - BigInteger pubExp = asn.readNext(); - BigInteger prvExp = asn.readNext(); - PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp)); - PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp)); - return new KeyPair(pubKey, prvKey); - } - case DSA: { - KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.DSA); - asn.readNext(); - BigInteger p = asn.readNext(); - BigInteger q = asn.readNext(); - BigInteger g = asn.readNext(); - BigInteger pub = asn.readNext(); - BigInteger prv = asn.readNext(); - PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g)); - PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g)); - return new KeyPair(pubKey, prvKey); - } - default: - throw new IOException("Unrecognized PKCS5 key type: " + type); - } - } catch (NoSuchAlgorithmException e) { - throw new IOException(e); - } catch (InvalidKeySpecException e) { - throw new IOException(e); - } finally { - reader.close(); - } - } - - @Override - public String toString() { - return "PKCS5KeyFile{resource=" + resource + "}"; - } - - private byte[] getPassphraseBytes() { - CharBuffer cb = CharBuffer.wrap(pwdf.reqPassword(resource)); - ByteBuffer bb = IOUtils.UTF8.encode(cb); - byte[] result = Arrays.copyOfRange(bb.array(), bb.position(), bb.limit()); - Arrays.fill(cb.array(), '\u0000'); - Arrays.fill(bb.array(), (byte) 0); - return result; - } - - private byte[] decrypt(byte[] raw, Cipher cipher, byte[] iv) throws DecryptException { - if (pwdf == null) { - return raw; - } - Digest md5 = new MD5(); - int bsize = cipher.getBlockSize(); - int hsize = md5.getBlockSize(); - int hnlen = bsize / hsize * hsize + (bsize % hsize == 0 ? 0 : hsize); - do { - md5.init(); - byte[] hn = new byte[hnlen]; - byte[] tmp = null; - byte[] passphrase = getPassphraseBytes(); - for (int i = 0; i + hsize <= hn.length; ) { - if (tmp != null) { - md5.update(tmp, 0, tmp.length); - } - md5.update(passphrase, 0, passphrase.length); - md5.update(iv, 0, iv.length > 8 ? 8 : iv.length); - tmp = md5.digest(); - System.arraycopy(tmp, 0, hn, i, tmp.length); - i += tmp.length; - } - Arrays.fill(passphrase, (byte) 0); - byte[] key = Arrays.copyOfRange(hn, 0, bsize); - cipher.init(Cipher.Mode.Decrypt, key, iv); - Arrays.fill(key, (byte) 0); - byte[] decrypted = Arrays.copyOf(raw, raw.length); - cipher.update(decrypted, 0, decrypted.length); - if (ASN1Data.MAGIC == decrypted[0]) { - return decrypted; - } - } while (pwdf.shouldRetry(resource)); - throw new DecryptException("Decryption failed"); - } - - class ASN1Data { - static final byte MAGIC = (byte) 0x30; - - private byte[] buff; - private int index, length; - - ASN1Data(byte[] buff) throws FormatException { - this.buff = buff; - index = 0; - if (buff[index++] != MAGIC) { - throw new FormatException("Not ASN.1 data"); - } - length = buff[index++] & 0xff; - if ((length & 0x80) != 0) { - int counter = length & 0x7f; - length = 0; - while (counter-- > 0) { - length = (length << 8) + (buff[index++] & 0xff); - } - } - if ((index + length) > buff.length) { - throw new FormatException("Length mismatch: " + buff.length + " != " + (index + length)); - } - } - - BigInteger readNext() throws IOException { - if (index >= length) { - throw new EOFException(); - } else if (buff[index++] != 0x02) { - throw new IOException("Not an int code: " + Integer.toHexString(0xff & buff[index])); - } - int length = buff[index++] & 0xff; - if ((length & 0x80) != 0) { - int counter = length & 0x7f; - length = 0; - while (counter-- > 0) { - length = (length << 8) + (buff[index++] & 0xff); - } - } - byte[] sequence = new byte[length]; - System.arraycopy(buff, index, sequence, 0, length); - index += length; - return new BigInteger(sequence); - } - } -} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java index a58acb091..300e4aa1e 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java @@ -39,7 +39,9 @@ import java.io.IOException; import java.security.KeyPair; -/** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */ +/** + * Key File implementation supporting PEM-encoded PKCS8 and PKCS1 formats with or without password-based encryption + */ public class PKCS8KeyFile extends BaseFileKeyProvider { public static class Factory diff --git a/src/test/groovy/com/hierynomus/sshj/userauth/keyprovider/FileKeyProviderSpec.groovy b/src/test/groovy/com/hierynomus/sshj/userauth/keyprovider/FileKeyProviderSpec.groovy index 3a10e338b..7c85faa4b 100644 --- a/src/test/groovy/com/hierynomus/sshj/userauth/keyprovider/FileKeyProviderSpec.groovy +++ b/src/test/groovy/com/hierynomus/sshj/userauth/keyprovider/FileKeyProviderSpec.groovy @@ -60,7 +60,7 @@ class FileKeyProviderSpec extends Specification { where: format | keyfile - KeyFormat.PKCS5 | "src/test/resources/keyformats/pkcs5" + KeyFormat.PKCS8 | "src/test/resources/keyformats/pkcs8" KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh" } } diff --git a/src/test/java/net/schmizz/sshj/keyprovider/KeyProviderUtilTest.java b/src/test/java/net/schmizz/sshj/keyprovider/KeyProviderUtilTest.java index 3df8961b7..2db7e4145 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/KeyProviderUtilTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/KeyProviderUtilTest.java @@ -35,9 +35,9 @@ public void testOpenSsh() throws IOException { } @Test - public void testPkcs5() throws IOException { - KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs5")); - assertEquals(KeyFormat.PKCS5, format); + public void testPkcs1Rsa() throws IOException { + KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs1-rsa")); + assertEquals(KeyFormat.PKCS8, format); } @Test diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PKCS5KeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PKCS5KeyFileTest.java deleted file mode 100644 index 8644bc10e..000000000 --- a/src/test/java/net/schmizz/sshj/keyprovider/PKCS5KeyFileTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C)2009 - SSHJ Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.schmizz.sshj.keyprovider; - -import net.schmizz.sshj.common.KeyType; -import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; -import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile; -import net.schmizz.sshj.userauth.password.PasswordFinder; -import net.schmizz.sshj.userauth.password.Resource; -import net.schmizz.sshj.util.KeyUtil; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; - -import static org.junit.Assert.assertEquals; - -public class PKCS5KeyFileTest { - - static final FileKeyProvider rsa = new PKCS5KeyFile(); - - static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469"; - static final String pubExp = "23"; - static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b"; - - final char[] correctPassphrase = "passphrase".toCharArray(); - final char[] incorrectPassphrase = "incorrect".toCharArray(); - - @Before - public void setUp() - throws UnsupportedEncodingException, GeneralSecurityException { - rsa.init(new File("src/test/resources/id_rsa")); - } - - @Test - public void testKeys() - throws IOException, GeneralSecurityException { - assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic()); - assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate()); - } - - @Test - public void testType() - throws IOException { - assertEquals(rsa.getType(), KeyType.RSA); - } - - final PasswordFinder givesOn3rdTry = new PasswordFinder() { - int triesLeft = 3; - - @Override - public char[] reqPassword(Resource resource) { - if (triesLeft == 0) - return correctPassphrase; - else { - triesLeft--; - return incorrectPassphrase; - } - } - - @Override - public boolean shouldRetry(Resource resource) { - return triesLeft >= 0; - } - }; - - @Test - public void retries() - throws IOException, GeneralSecurityException { - FileKeyProvider rsa = new PKCS5KeyFile(); - rsa.init(new File("src/test/resources/rsa.pk5"), givesOn3rdTry); - assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic()); - assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate()); - } -} diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java index c615c77a8..5730c8686 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java @@ -15,67 +15,62 @@ */ package net.schmizz.sshj.keyprovider; +import com.hierynomus.sshj.common.KeyAlgorithm; import com.hierynomus.sshj.common.KeyDecryptionFailedException; import net.schmizz.sshj.common.KeyType; -import net.schmizz.sshj.common.SecurityUtils; -import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; import net.schmizz.sshj.userauth.password.PasswordFinder; import net.schmizz.sshj.userauth.password.PasswordUtils; import net.schmizz.sshj.util.KeyUtil; -import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; public class PKCS8KeyFileTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - static final FileKeyProvider rsa = new PKCS8KeyFile(); - static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469"; static final String pubExp = "23"; static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b"; - @Before - public void setUp() - throws UnsupportedEncodingException, GeneralSecurityException { - if (!SecurityUtils.isBouncyCastleRegistered()) - throw new AssertionError("bouncy castle needed"); - rsa.init(new File("src/test/resources/id_rsa")); + @Test + public void testKeys() throws GeneralSecurityException, IOException { + final PKCS8KeyFile provider = new PKCS8KeyFile(); + provider.init(new File("src/test/resources/id_rsa")); + assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic()); + assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate()); + assertEquals(provider.getType(), KeyType.RSA); } @Test - public void testKeys() - throws IOException, GeneralSecurityException { - assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic()); - assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate()); + public void testPkcs1Rsa() throws IOException { + final PKCS8KeyFile provider = new PKCS8KeyFile(); + provider.init(getReader("pkcs1-rsa")); + assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm()); + assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm()); } @Test - public void testType() - throws IOException { - assertEquals(rsa.getType(), KeyType.RSA); + public void testPkcs1Encrypted() throws IOException, GeneralSecurityException { + final PKCS8KeyFile provider = new PKCS8KeyFile(); + provider.init(getReader("pkcs1-rsa-encrypted"), PasswordUtils.createOneOff("passphrase".toCharArray())); + assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic()); + assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate()); } @Test public void testPkcs8Rsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); provider.init(getReader("pkcs8-rsa-2048")); - assertEquals("RSA", provider.getPublic().getAlgorithm()); - assertEquals("RSA", provider.getPrivate().getAlgorithm()); + assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm()); + assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm()); } @Test @@ -83,34 +78,32 @@ public void testPkcs8RsaEncrypted() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); final PasswordFinder passwordFinder = PasswordUtils.createOneOff("passphrase".toCharArray()); provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder); - assertEquals("RSA", provider.getPublic().getAlgorithm()); - assertEquals("RSA", provider.getPrivate().getAlgorithm()); + assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm()); + assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm()); } @Test - public void testPkcs8RsaEncryptedIncorrectPassword() throws IOException { - expectedException.expect(KeyDecryptionFailedException.class); - + public void testPkcs8RsaEncryptedIncorrectPassword() { final PKCS8KeyFile provider = new PKCS8KeyFile(); final PasswordFinder passwordFinder = PasswordUtils.createOneOff(String.class.getSimpleName().toCharArray()); provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder); - provider.getPrivate(); + assertThrows(KeyDecryptionFailedException.class, provider::getPrivate); } @Test public void testPkcs8Ecdsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); provider.init(getReader("pkcs8-ecdsa")); - assertEquals("ECDSA", provider.getPublic().getAlgorithm()); - assertEquals("ECDSA", provider.getPrivate().getAlgorithm()); + assertEquals(KeyAlgorithm.ECDSA, provider.getPublic().getAlgorithm()); + assertEquals(KeyAlgorithm.ECDSA, provider.getPrivate().getAlgorithm()); } @Test public void testPkcs8Dsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); provider.init(getReader("pkcs8-dsa")); - assertEquals("DSA", provider.getPublic().getAlgorithm()); - assertEquals("DSA", provider.getPrivate().getAlgorithm()); + assertEquals(KeyAlgorithm.DSA, provider.getPublic().getAlgorithm()); + assertEquals(KeyAlgorithm.DSA, provider.getPrivate().getAlgorithm()); } private Reader getReader(final String filename) { diff --git a/src/test/resources/keyformats/pkcs5 b/src/test/resources/keyformats/pkcs1-rsa similarity index 100% rename from src/test/resources/keyformats/pkcs5 rename to src/test/resources/keyformats/pkcs1-rsa diff --git a/src/test/resources/rsa.pk5 b/src/test/resources/keyformats/pkcs1-rsa-encrypted similarity index 100% rename from src/test/resources/rsa.pk5 rename to src/test/resources/keyformats/pkcs1-rsa-encrypted From 502e76e5b498f80e1aee5b816f504d65d34fd5f6 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Wed, 13 Jul 2022 08:05:29 -0500 Subject: [PATCH 2/2] Added PKCS8 test to retry password after initial failure --- .../sshj/keyprovider/PKCS8KeyFileTest.java | 64 ++++++++++++++----- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java index 5730c8686..7a54d3caf 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java @@ -21,14 +21,13 @@ import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; import net.schmizz.sshj.userauth.password.PasswordFinder; import net.schmizz.sshj.userauth.password.PasswordUtils; +import net.schmizz.sshj.userauth.password.Resource; import net.schmizz.sshj.util.KeyUtil; import org.junit.Test; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; +import java.net.URL; import java.security.GeneralSecurityException; import static org.junit.Assert.assertEquals; @@ -39,6 +38,8 @@ public class PKCS8KeyFileTest { static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469"; static final String pubExp = "23"; static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b"; + static final String KEY_PASSPHRASE = "passphrase"; + static final String INCORRECT_PASSPHRASE = String.class.getSimpleName(); @Test public void testKeys() throws GeneralSecurityException, IOException { @@ -52,7 +53,7 @@ public void testKeys() throws GeneralSecurityException, IOException { @Test public void testPkcs1Rsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); - provider.init(getReader("pkcs1-rsa")); + provider.init(getFile("pkcs1-rsa")); assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm()); } @@ -60,7 +61,7 @@ public void testPkcs1Rsa() throws IOException { @Test public void testPkcs1Encrypted() throws IOException, GeneralSecurityException { final PKCS8KeyFile provider = new PKCS8KeyFile(); - provider.init(getReader("pkcs1-rsa-encrypted"), PasswordUtils.createOneOff("passphrase".toCharArray())); + provider.init(getFile("pkcs1-rsa-encrypted"), PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray())); assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic()); assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate()); } @@ -68,7 +69,7 @@ public void testPkcs1Encrypted() throws IOException, GeneralSecurityException { @Test public void testPkcs8Rsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); - provider.init(getReader("pkcs8-rsa-2048")); + provider.init(getFile("pkcs8-rsa-2048")); assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm()); } @@ -76,8 +77,8 @@ public void testPkcs8Rsa() throws IOException { @Test public void testPkcs8RsaEncrypted() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); - final PasswordFinder passwordFinder = PasswordUtils.createOneOff("passphrase".toCharArray()); - provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder); + final PasswordFinder passwordFinder = PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray()); + provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder); assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm()); } @@ -85,15 +86,46 @@ public void testPkcs8RsaEncrypted() throws IOException { @Test public void testPkcs8RsaEncryptedIncorrectPassword() { final PKCS8KeyFile provider = new PKCS8KeyFile(); - final PasswordFinder passwordFinder = PasswordUtils.createOneOff(String.class.getSimpleName().toCharArray()); - provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder); + final PasswordFinder passwordFinder = PasswordUtils.createOneOff(INCORRECT_PASSPHRASE.toCharArray()); + provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder); assertThrows(KeyDecryptionFailedException.class, provider::getPrivate); } + @Test + public void testPkcs8RsaEncryptedRetryPassword() throws IOException { + final PKCS8KeyFile provider = new PKCS8KeyFile(); + final PasswordFinder passwordFinder = new PasswordFinder() { + private boolean retryEnabled = true; + + @Override + public char[] reqPassword(Resource resource) { + final char[] password; + if (retryEnabled) { + password = INCORRECT_PASSPHRASE.toCharArray(); + } else { + password = KEY_PASSPHRASE.toCharArray(); + } + return password; + } + + @Override + public boolean shouldRetry(Resource resource) { + final boolean shouldRetry = retryEnabled; + if (retryEnabled) { + retryEnabled = false; + } + return shouldRetry; + } + }; + provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder); + assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm()); + assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm()); + } + @Test public void testPkcs8Ecdsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); - provider.init(getReader("pkcs8-ecdsa")); + provider.init(getFile("pkcs8-ecdsa")); assertEquals(KeyAlgorithm.ECDSA, provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.ECDSA, provider.getPrivate().getAlgorithm()); } @@ -101,17 +133,17 @@ public void testPkcs8Ecdsa() throws IOException { @Test public void testPkcs8Dsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); - provider.init(getReader("pkcs8-dsa")); + provider.init(getFile("pkcs8-dsa")); assertEquals(KeyAlgorithm.DSA, provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.DSA, provider.getPrivate().getAlgorithm()); } - private Reader getReader(final String filename) { + private File getFile(final String filename) { final String path = String.format("/keyformats/%s", filename); - final InputStream inputStream = getClass().getResourceAsStream(path); - if (inputStream == null) { + final URL resource = getClass().getResource(path); + if (resource == null) { throw new IllegalArgumentException(String.format("Key File [%s] not found", path)); } - return new InputStreamReader(inputStream); + return new File(resource.getPath()); } }