diff --git a/src/main/java/net/schmizz/sshj/DefaultConfig.java b/src/main/java/net/schmizz/sshj/DefaultConfig.java index ca446afb..9b950da9 100644 --- a/src/main/java/net/schmizz/sshj/DefaultConfig.java +++ b/src/main/java/net/schmizz/sshj/DefaultConfig.java @@ -63,6 +63,8 @@ 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.PKCS8KeyFile; +import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -126,7 +128,7 @@ public class DefaultConfig protected void initFileKeyProviderFactories(boolean bouncyCastleRegistered) { if (bouncyCastleRegistered) { - setFileKeyProviderFactories(new PKCS8KeyFile.Factory(), new OpenSSHKeyFile.Factory()); + setFileKeyProviderFactories(new PKCS8KeyFile.Factory(), new OpenSSHKeyFile.Factory(), new PuTTYKeyFile.Factory()); } } diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java new file mode 100644 index 00000000..c0f2c3ea --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java @@ -0,0 +1,411 @@ +package net.schmizz.sshj.userauth.keyprovider; + +import org.bouncycastle.util.encoders.Hex; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.HashMap; +import java.util.Map; + +import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.userauth.password.PasswordFinder; +import net.schmizz.sshj.userauth.password.PasswordUtils; +import net.schmizz.sshj.userauth.password.PrivateKeyFileResource; +import net.schmizz.sshj.userauth.password.PrivateKeyReaderResource; +import net.schmizz.sshj.userauth.password.PrivateKeyStringResource; +import net.schmizz.sshj.userauth.password.Resource; + +/** + *

Sample PuTTY file format

+ *
+ * PuTTY-User-Key-File-2: ssh-rsa
+ * Encryption: none
+ * Comment: rsa-key-20080514
+ * Public-Lines: 4
+ * AAAAB3NzaC1yc2EAAAABJQAAAIEAiPVUpONjGeVrwgRPOqy3Ym6kF/f8bltnmjA2
+ * BMdAtaOpiD8A2ooqtLS5zWYuc0xkW0ogoKvORN+RF4JI+uNUlkxWxnzJM9JLpnvA
+ * HrMoVFaQ0cgDMIHtE1Ob1cGAhlNInPCRnGNJpBNcJ/OJye3yt7WqHP4SPCCLb6nL
+ * nmBUrLM=
+ * Private-Lines: 8
+ * AAAAgGtYgJzpktzyFjBIkSAmgeVdozVhgKmF6WsDMUID9HKwtU8cn83h6h7ug8qA
+ * hUWcvVxO201/vViTjWVz9ALph3uMnpJiuQaaNYIGztGJBRsBwmQW9738pUXcsUXZ
+ * 79KJP01oHn6Wkrgk26DIOsz04QOBI6C8RumBO4+F1WdfueM9AAAAQQDmA4hcK8Bx
+ * nVtEpcF310mKD3nsbJqARdw5NV9kCxPnEsmy7Sy1L4Ob/nTIrynbc3MA9HQVJkUz
+ * 7V0va5Pjm/T7AAAAQQCYbnG0UEekwk0LG1Hkxh1OrKMxCw2KWMN8ac3L0LVBg/Tk
+ * 8EnB2oT45GGeJaw7KzdoOMFZz0iXLsVLNUjNn2mpAAAAQQCN6SEfWqiNzyc/w5n/
+ * lFVDHExfVUJp0wXv+kzZzylnw4fs00lC3k4PZDSsb+jYCMesnfJjhDgkUA0XPyo8
+ * Emdk
+ * Private-MAC: 50c45751d18d74c00fca395deb7b7695e3ed6f77
+ * 
+ * + * @version $Id:$ + */ +public class PuTTYKeyFile implements FileKeyProvider { + + public static class Factory + implements net.schmizz.sshj.common.Factory.Named { + + @Override + public FileKeyProvider create() { + return new PuTTYKeyFile(); + } + + @Override + public String getName() { + return "PuTTY"; + } + } + + private byte[] privateKey; + private byte[] publicKey; + + private KeyPair kp; + + protected PasswordFinder pwdf; + + protected Resource resource; + + @Override + public void init(Reader location) { + this.resource = new PrivateKeyReaderResource(location); + } + + public void init(Reader location, PasswordFinder pwdf) { + this.init(location); + this.pwdf = pwdf; + } + + @Override + public void init(File location) { + resource = new PrivateKeyFileResource(location.getAbsoluteFile()); + } + + @Override + public void init(File location, PasswordFinder pwdf) { + this.init(location); + this.pwdf = pwdf; + } + + @Override + public void init(String privateKey, String publicKey) { + resource = new PrivateKeyStringResource(privateKey); + } + + @Override + public void init(String privateKey, String publicKey, PasswordFinder pwdf) { + init(privateKey, publicKey); + this.pwdf = pwdf; + } + + @Override + public PrivateKey getPrivate() + throws IOException { + return kp != null ? kp.getPrivate() : (kp = this.readKeyPair()).getPrivate(); + } + + @Override + public PublicKey getPublic() + throws IOException { + return kp != null ? kp.getPublic() : (kp = this.readKeyPair()).getPublic(); + } + + /** + * Key type. Either "ssh-rsa" for RSA key, or "ssh-dss" for DSA key. + */ + @Override + public KeyType getType() throws IOException { + return KeyType.fromString(headers.get("PuTTY-User-Key-File-2")); + } + + public boolean isEncrypted() { + // Currently the only supported encryption types are "aes256-cbc" and "none". + return "aes256-cbc".equals(headers.get("Encryption")); + } + + private Map payload + = new HashMap(); + + /** + * For each line that looks like "Xyz: vvv", it will be stored in this map. + */ + private final Map headers + = new HashMap(); + + + protected KeyPair readKeyPair() throws IOException { + this.parseKeyPair(); + if(KeyType.RSA.equals(this.getType())) { + final KeyReader publicKeyReader = new KeyReader(publicKey); + publicKeyReader.skip(); // skip this + // public key exponent + BigInteger e = publicKeyReader.readInt(); + // modulus + BigInteger n = publicKeyReader.readInt(); + + final KeyReader privateKeyReader = new KeyReader(privateKey); + // private key exponent + BigInteger d = privateKeyReader.readInt(); + + final KeyFactory factory; + try { + factory = KeyFactory.getInstance("RSA"); + } + catch(NoSuchAlgorithmException s) { + throw new IOException(s.getMessage(), s); + } + try { + return new KeyPair( + factory.generatePublic(new RSAPublicKeySpec(n, e)), + factory.generatePrivate(new RSAPrivateKeySpec(n, d)) + ); + } + catch(InvalidKeySpecException i) { + throw new IOException(i.getMessage(), i); + } + } + if(KeyType.DSA.equals(this.getType())) { + final KeyReader publicKeyReader = new KeyReader(publicKey); + publicKeyReader.skip(); // skip this + BigInteger p = publicKeyReader.readInt(); + BigInteger q = publicKeyReader.readInt(); + BigInteger g = publicKeyReader.readInt(); + BigInteger y = publicKeyReader.readInt(); + + final KeyReader privateKeyReader = new KeyReader(privateKey); + // Private exponent from the private key + BigInteger x = privateKeyReader.readInt(); + + final KeyFactory factory; + try { + factory = KeyFactory.getInstance("DSA"); + } + catch(NoSuchAlgorithmException s) { + throw new IOException(s.getMessage(), s); + } + try { + return new KeyPair( + factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)), + factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g)) + ); + } + catch(InvalidKeySpecException e) { + throw new IOException(e.getMessage(), e); + } + } + else { + throw new IOException(String.format("Unknown key type %s", this.getType())); + } + } + + protected void parseKeyPair() throws IOException { + BufferedReader r = new BufferedReader(resource.getReader()); + // Parse the text into headers and payloads + try { + String headerName = null; + String line; + while((line = r.readLine()) != null) { + int idx = line.indexOf(": "); + if(idx > 0) { + headerName = line.substring(0, idx); + headers.put(headerName, line.substring(idx + 2)); + } + else { + String s = payload.get(headerName); + if(s == null) { + s = line; + } + else { + // Append to previous line + s += line; + } + // Save payload + payload.put(headerName, s); + } + } + } + finally { + r.close(); + } + // Retrieve keys from payload + publicKey = Base64.decode(payload.get("Public-Lines")); + if(this.isEncrypted()) { + final char[] passphrase; + if(pwdf != null) { + passphrase = pwdf.reqPassword(resource); + } + else { + passphrase = "".toCharArray(); + } + try { + privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), new String(passphrase)); + this.verify(new String(passphrase)); + } + finally { + PasswordUtils.blankOut(passphrase); + } + } + else { + privateKey = Base64.decode(payload.get("Private-Lines")); + } + } + + /** + * Converts a passphrase into a key, by following the convention that PuTTY uses. + *

+ *

+ * This is used to decrypt the private key when it's encrypted. + */ + private byte[] toKey(final String passphrase) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + + // The encryption key is derived from the passphrase by means of a succession of SHA-1 hashes. + + // Sequence number 0 + digest.update(new byte[]{0, 0, 0, 0}); + digest.update(passphrase.getBytes()); + byte[] key1 = digest.digest(); + + // Sequence number 1 + digest.update(new byte[]{0, 0, 0, 1}); + digest.update(passphrase.getBytes()); + byte[] key2 = digest.digest(); + + byte[] r = new byte[32]; + System.arraycopy(key1, 0, r, 0, 20); + System.arraycopy(key2, 0, r, 20, 12); + + return r; + } + catch(NoSuchAlgorithmException e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * Verify the MAC. + */ + private void verify(final String passphrase) throws IOException { + try { + // The key to the MAC is itself a SHA-1 hash of: + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update("putty-private-key-file-mac-key".getBytes()); + if(passphrase != null) { + digest.update(passphrase.getBytes()); + } + final byte[] key = digest.digest(); + + final Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec(key, 0, 20, mac.getAlgorithm())); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final DataOutputStream data = new DataOutputStream(out); + // name of algorithm + data.writeInt(this.getType().toString().length()); + data.writeBytes(this.getType().toString()); + + data.writeInt(headers.get("Encryption").length()); + data.writeBytes(headers.get("Encryption")); + + data.writeInt(headers.get("Comment").length()); + data.writeBytes(headers.get("Comment")); + + data.writeInt(publicKey.length); + data.write(publicKey); + + data.writeInt(privateKey.length); + data.write(privateKey); + + final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray())); + final String reference = headers.get("Private-MAC"); + if(!encoded.equals(reference)) { + throw new IOException("Invalid passphrase"); + } + } + catch(GeneralSecurityException e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * Decrypt private key + * + * @param passphrase To decrypt + */ + private byte[] decrypt(final byte[] key, final String passphrase) throws IOException { + try { + final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + final byte[] expanded = this.toKey(passphrase); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"), + new IvParameterSpec(new byte[16])); // initial vector=0 + return cipher.doFinal(key); + } + catch(GeneralSecurityException e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * Parses the putty key bit vector, which is an encoded sequence + * of {@link java.math.BigInteger}s. + */ + private final static class KeyReader { + private final DataInput di; + + public KeyReader(byte[] key) { + this.di = new DataInputStream(new ByteArrayInputStream(key)); + } + + /** + * Skips an integer without reading it. + */ + public void skip() throws IOException { + final int read = di.readInt(); + if(read != di.skipBytes(read)) { + throw new IOException(String.format("Failed to skip %d bytes", read)); + } + } + + private byte[] read() throws IOException { + int len = di.readInt(); + if(len <= 0 || len > 513) { + throw new IOException(String.format("Invalid length %d", len)); + } + byte[] r = new byte[len]; + di.readFully(r); + return r; + } + + /** + * Reads the next integer. + */ + public BigInteger readInt() throws IOException { + return new BigInteger(read()); + } + } +} \ No newline at end of file diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java new file mode 100644 index 00000000..11ce6152 --- /dev/null +++ b/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java @@ -0,0 +1,221 @@ +package net.schmizz.sshj.keyprovider; + +import org.junit.Test; + +import java.io.IOException; +import java.io.StringReader; + +import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile; +import net.schmizz.sshj.userauth.password.PasswordFinder; +import net.schmizz.sshj.userauth.password.Resource; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class PuTTYKeyFileTest { + + final static String ppk2048 = "PuTTY-User-Key-File-2: ssh-rsa\n" + + "Encryption: none\n" + + "Comment: \n" + + "Public-Lines: 6\n" + + "AAAAB3NzaC1yc2EAAAADAQABAAABAQC0ITaAE49ievGiREUNxjccle9zJEZkNdkE\n" + + "2Nnkl0zxlGVwShwRIjtarM0uKiUQAFD1OhkdSA/1FhZKRumIUWD+2Fkj23EEvox0\n" + + "bTZyPzvQJERchdpvZKJyfxZgnPL6ygY6UQj8oNBhxnuwm9lL11cSJWS+4qJigT7g\n" + + "59eEhDyBaZ/HoijtDGvfPmJlhdNcq0MlC6ALy7XXpOd4RS2oo8m/TRpLFvoSnAQ/\n" + + "sr1wv9u7sTZh5C5RAjPN8LWBu7GuodqZ8PhlyT3oT+qbjpA/2e18vQta1ELROBKk\n" + + "qsLnsN4fjH69eYSZ8h5R07tfvQxTKRwCWKziqMjP4dF9Lz+gVcy1\n" + + "Private-Lines: 14\n" + + "AAABAQCqLWasAc7JH5YB07XZmZafrxeWFINcUXNCnQzeZgMPiT98osd5eHnS5MbE\n" + + "ApUZVPMne0gW3eoVhlRwwCYJ37hfjE5LDhrsfIl9xWBW916u+lSLhPolm1HOEjs1\n" + + "85GrVgokNkLjSZsVhMt+wv68JCnivuk7XipEHg8ltGNskvIG4AjW97uBqewyvyeG\n" + + "wsPYyBtiifRxJQ1th5hlLPh+jOBsyz91uC+ZeEW2pil2ftI7XrbKVbA95SRh4W8R\n" + + "tNBjqUpI+M2mJQ7nwh2gxd/GdNxKyvTUyCAfJo+DzAG+XRGW1Px7ibFw38jiT+CP\n" + + "tKTjCZRFMPUvmoH3MR1hzjqjqpuBAAAAgQDj4/2h35V/aAEYvQfkwF4k6rWOY15+\n" + + "gEV+gbfjWlYGxkH6U0AvMQv3c6EAvJNQsKip3/fqOHgdd37CcGVW+NQTucHlxz8K\n" + + "e4cYs0Dy8g4gcNhy2M99MOy9TuMsC0/mrTQUP0Vewwo7FASWF23sbhZsBM//BC8W\n" + + "m3LM843RwCbvXQAAAIEAylkY1TU721y22mVA2C+o6ADs55ZtMGJqjI0DjOiWCgxt\n" + + "j6pXRmJQ05hFZy4pO4AOYMZ5IW7MdqmCu2+GVytA6PxA6C+OGYF0Eh1YJbIh/Qrv\n" + + "07NMrYQVwQY2+FAJpLAwWJAjlrRRlANgGBHkbppf8RuQFB/euToHCZ6R6goJdTkA\n" + + "AACAOn0n+B0Lums/2FFmBRak2niTRONt6GMWhtK4e42MnKN3VxMGshAB4SdDAQOY\n" + + "4Qk66RYmniuhaC3sLwxtXsEKoVnMp9EXVoTPEd+BQCVBOJzZDVtAaejO9bqrvRL8\n" + + "kmknsX54RBXxrOVvNTofHLiRojncZnRSrM3BR+Xjo0b0+mE=\n" + + "Private-MAC: c9c10df8a1e3546eedcc08608efd5338de5df723\n"; + + final static String ppk4096 = "PuTTY-User-Key-File-2: ssh-rsa\n" + + "Encryption: none\n" + + "Comment: \n" + + "Public-Lines: 12\n" + + "AAAAB3NzaC1yc2EAAAADAQABAAACAQDMlwE5YNobWP8R47Ms41hnQnATKfJblTxW\n" + + "k/6nf+5IOknCNFBMQUOnToCmvcVRPzepr3nRFGm/gvo5SjsKdE4b0b9eT7xOGAYM\n" + + "9y18qO3flt6hARasK8NoivbT8Cm1f0Zj02eLBaiFpFYZOuBZdpluKiYH0wHuSPeq\n" + + "K3Q3/arsnQj1C0X+h5f4Nm0IYIHRkNsnvZrJf+MlHtcwS+BPXpAK9tkICcP1MJ2x\n" + + "UvTKh+TgWQJQ8EUq0OUkTBUBdmG+J6O+sdB0V6r06IpcXZUNed02F+bzP/DVUE1b\n" + + "mJZTx0ynZhKyP6NeXlwuZ3fUZhiwwqMRvCQuq1p8/itG9Vz+eY652KIIrCoVsyH1\n" + + "gIIRert3ADX5UySMdPcgBoDWYlfyj/fS+dR2o1lIwQXLcl9uL8ZELteSq/sLmapA\n" + + "YJbIis3r9aJnNSVaQSKn8p20tCWNnazAcSK0RTN2h5r0/r7WfvXafFIMt6VZUxPn\n" + + "dpFCJtxhgCrszosy87eL92NCoZvdQMOddpV3op04CDccZy0LEAt7o9dXoNNaeYlx\n" + + "czaUTV/JcuAnk9G0u3xUpTh0AOQauuxn8Dv6yyVLvXNJANp89zAhUukEwdhqOvXD\n" + + "U5qLLgY0Kf3v+ySj6HUWNBoms6ijF1txT3RDmJdCiVfuZ1nic9tsyp0A77S1oEEQ\n" + + "QD7Rgmi4rQ==\n" + + "Private-Lines: 28\n" + + "AAACABi+/xfonhkGt7t7NjXsvcmnoJTA0x6+u1ChkADEmZbE7hz+ZOQEVOGMvkTs\n" + + "2UwNgHcW0X43oN7YQdniH6gRD02QHjyTGmy7vSeeUjMs37DWt9Dzp8FlfbpMbLSP\n" + + "7QuV/HagoHqRUaPwj7V3iKFplf9cO8Ngg3BGBSbhIKqRFTaPfADfvzSdRAVy19dW\n" + + "jP1DLy7sYSeUP25C/7ZIxzXycyvQVcoCHGCw47IKHa/NpiJ4wa32kfcu0ziDt1q4\n" + + "7fOpKcYsDdG0tOnwoqOvchLyNY6Qb4/moQO8Nc8pcq1pgt0QnJxQ1Dra4P1/6F+Z\n" + + "hc0DjePcROgcM9LAj42Cqh/hpiCfCiLJiDts+HhQppgA4fOMy5d/wrG0nuqKfhIv\n" + + "BsX9nJDj4eHU4eNBAoraUfNLDIq0GHYDcm+jlhqO1SHxymjIhDqS6Cz/FWf07L1Q\n" + + "5DQ/+xHysVHCcavQk4jA7JwbZrRWo5qyKrdLoWRPFUX5w5ocLnmj0Zx8VOl6a+8M\n" + + "Q+ehLSZXFoCbao3nES/oEkKH0RFNQsDMJb0uiKQv4b/+6bywtYIFc0eqvEqd1GSF\n" + + "x3exCdHNhLRycaCgGSh+IdPCRrMj0N7/9pGZmbjfcZ7uKlFwqETVmy1H67NTXUCW\n" + + "NukVfsqTRewpqjFFeaxW5GEYwEeA34MbIChfdw4/KRr5XDhFAAABAQD3c9w0rWAQ\n" + + "rjVF1WTeD89Mf+Fnf7NvRaHAaD1EJxfimgqCD5juCIa5WSplzEBpPSG/rpl3HYVz\n" + + "CZ98rdJSS/bmJieojefvjlz1nuuPlApg2ctCfEZYOFnNP6yt0w88GLp3aMTfIsTf\n" + + "Z893GZnMFzMMLItLcZBTSmQLiqpyU6lWE+Tr4QOcQeCF8XqHGrLWbwACuKocmCW/\n" + + "4nI+6gZ4SfucLXKwFgcuhSaXo0XM6HiSgZHb5wEyjS2Boad6vX8t4YdjZbCcnGVm\n" + + "9TEm0/ow41Cl44SJUU6pLlo4UnSmR7aLmTK4iEG3fIMdEmmy4VX3MJ8fqXuVJwLE\n" + + "RLzqEjCgIcQzAAABAQDTqB/A3CyJfeHYFO7Et6edAOklejxqRW4UuuOu55v7FOj5\n" + + "X/yW72rWbndcci+mDXQvDL6P9EG3vF1twPS0konHqVxqj6Jlp1AtUWND2FzVTypY\n" + + "0X7z4Mif5V0p5bS5Qx6/pBg37XXbisSANSDxFVdH0/OSTYXi4EKmh0LjU5Ls0zIw\n" + + "MB6TYetuR1hEcCxuVESnOMUgjXMsoIwGR/jeKynle45UwTqUv/oWRQvFeIi5wlwn\n" + + "82GtUzLxhAo/BbXc3ODWjIGfKSxBJdsn0ZEXtPAk4CTqxM3VF4s3aOFAhHBDSyOv\n" + + "nHvWXwVRwmhtyXKEkTfAO6K4ptcS57LTNT8ta6+fAAABAQC9dPiPexqC35vWtWQd\n" + + "Zvm8DVCVscd7IPDn952FUsf2svoQ9MWodpD1craKGadSRsFCTVeYyHzS3Sg8HwKC\n" + + "NNoanAxpY4IqEPfuaLZZuKQsj3PsVj5rXdSEbmwCR7EhI9oDUDNcSLufR5A5DMpz\n" + + "wY4EJmg8uC2nO/O9Rzr516pIfDGsNwsdSKGWLlhgRzJxWl7M+cJjJfRlf6XruhLI\n" + + "WDDIq/jMHb5cLNjXdWTt3jyRQkm3HI6r5C3vc4mdInBm3tNUE+KKBtChegpgDgqg\n" + + "hZ41/hnd1e+3on3tvrE7arM3t4IHt7grwS/i1vdukV8ilYkTYHMG/Ls+6pUr+Swy\n" + + "z15x\n" + + "Private-MAC: a11331fa8b59cfb2be1c8e9f67ead34ac848d514\n"; + + final static String ppk1024_passphrase = "PuTTY-User-Key-File-2: ssh-rsa\n" + + "Encryption: aes256-cbc\n" + + "Comment: rsa-key-20121215\n" + + "Public-Lines: 4\n" + + "AAAAB3NzaC1yc2EAAAABJQAAAIB7KdUyuvGb2ne9G9YDAjaYvX/Mq6Q6ppGjbEQo\n" + + "bac66VUazxVpZsnAWikcdYAU7odkyt3jg7Nn1NgQS1a5mpXk/j77Ss5C9W4rymrU\n" + + "p32cmbgB/KIV80DnOyZyOtDWDPM0M0RRXqQvAO6TsnmsNSnBa8puMLHqCtrhvvJD\n" + + "KU+XEw==\n" + + "Private-Lines: 8\n" + + "4YMkPgLQJ9hOI1L1HsdOUnYi57tDy5h9DoPTHD55fhEYsn53h4WaHpxuZH8dTpbC\n" + + "5TcV3vYTfhh+aFBY0p/FI8L1hKfABLRxhkqkkc7xMmOGlA6HejAc8oTA3VArgSeG\n" + + "tRBuQRmBAC1Edtek/U+s8HzI2whzTw8tZoUUnT6844oc4tyCpWJUy5T8l+O3/03s\n" + + "SceJ98DN2k+L358VY8AXgPxP6NJvHvIlwmIo+PtcMWsyZegfSHEnoXN2GN4N0ul6\n" + + "298RzA9R+I3GSKKxsxUvWfOVibLq0dDM3+CTwcbmo4qvyM2xrRRLhObB2rVW07gL\n" + + "7+FZpHxf44QoQQ8mVkDJNaT1faF+h/8tCp2j1Cj5yEPHMOHGTVMyaz7gqhoMw5RX\n" + + "sfSP4ZaCGinLbouPrZN9Ue3ytwdEpmqU2MelmcZdcH6kWbLCqpWBswsxPfuhFdNt\n" + + "oYhmT2+0DKBuBVCAM4qRdA==\n" + + "Private-MAC: 40ccc8b9a7291ec64e5be0c99badbc8a012bf220\n"; + + final static String ppkdsa_passphrase = "PuTTY-User-Key-File-2: ssh-dss\n" + + "Encryption: aes256-cbc\n" + + "Comment: dsa-key-20140507\n" + + "Public-Lines: 10\n" + + "AAAAB3NzaC1kc3MAAACBAN6eo/Yh8ih26sKRAHAta/UqKesrXRS83GN7YqAxQzsP\n" + + "2tJ00UzOqZCdBoHIXLXC07QRJ9SkXOMnILw/KuaZ3paJ6ym92FzKi3BRfpzujIdo\n" + + "qBAEGSGOWbz2oYPDDSi0bsL84P4O8WD7ZxKhgTb4JAxlVJiW20vPfZA8Ft6xKJyd\n" + + "AAAAFQD1pnKWpSyHzi6RcVPn16FwmGIgmwAAAIEAiFPw87HVijatNOBeuxoU5PHH\n" + + "80kMl0TtxoI7rhB8fKO9bu7wLcT79h6xYS4Np6nHv9ajWwwVSLh8NjKgMbCXCz2j\n" + + "qD4ajvnusS7yz7TbTumeaGqFXEEzqzG4Xe6KXkv7kd7Yg+Dnw29zucgeAvPfuJFW\n" + + "Gtr4CWPoHSBgpTeyemEAAACBAJYvGi5gIMJQQUhIErKbtZ64V2L0zZtYkzlms03R\n" + + "cTBFN9++xV8zUvTPAAM8imsoxZ/5JNtNjJCAD+Ghrzyav24gxYG9v/YXtd2WsYa5\n" + + "0E/5wxcPor82SAqU2fd3IEQ5y9KHamXBuX/5KFDOTMC6cnGsutFkeo5rXQ0fI55C\n" + + "VSTq\n" + + "Private-Lines: 1\n" + + "nLEIBzB8WEaMMgDz5qwlqq7eBxLPIIi9uHyjMf7NOsQ=\n" + + "Private-MAC: b200c6d801fc8dd8f84a14afc3b94d9f9bb2df90\n"; + + @Test + public void test2048() throws Exception { + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new StringReader(ppk2048)); + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + } + + @Test + public void test4096() throws Exception { + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new StringReader(ppk4096)); + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + } + + @Test + public void testCorrectPassphraseRsa() throws Exception { + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new StringReader(ppk1024_passphrase), new PasswordFinder() { + @Override + public char[] reqPassword(Resource resource) { + // correct passphrase + return "123456".toCharArray(); + } + + @Override + public boolean shouldRetry(Resource resource) { + return false; + } + }); + // Install JCE Unlimited Strength Jurisdiction Policy Files if we get java.security.InvalidKeyException: Illegal key size + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + } + + @Test(expected = IOException.class) + public void testWrongPassphraseRsa() throws Exception { + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new StringReader(ppk1024_passphrase), new PasswordFinder() { + @Override + public char[] reqPassword(Resource resource) { + // wrong passphrase + return "egfsdgdfgsdfsdfasfs523534dgdsgdfa".toCharArray(); + } + + @Override + public boolean shouldRetry(Resource resource) { + return false; + } + }); + assertNotNull(key.getPublic()); + assertNull(key.getPrivate()); + } + + @Test + public void testCorrectPassphraseDsa() throws Exception { + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new StringReader(ppkdsa_passphrase), new PasswordFinder() { + @Override + public char[] reqPassword(Resource resource) { + // correct passphrase + return "secret".toCharArray(); + } + + @Override + public boolean shouldRetry(Resource resource) { + return false; + } + }); + // Install JCE Unlimited Strength Jurisdiction Policy Files if we get java.security.InvalidKeyException: Illegal key size + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + } + + @Test(expected = IOException.class) + public void testWrongPassphraseDsa() throws Exception { + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new StringReader(ppkdsa_passphrase), new PasswordFinder() { + @Override + public char[] reqPassword(Resource resource) { + // wrong passphrase + return "egfsdgdfgsdfsdfasfs523534dgdsgdfa".toCharArray(); + } + + @Override + public boolean shouldRetry(Resource resource) { + return false; + } + }); + assertNotNull(key.getPublic()); + assertNull(key.getPrivate()); + } +}