diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java index a1f58eba6..818ae7b64 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java @@ -16,9 +16,20 @@ package net.schmizz.sshj.userauth.keyprovider; import com.hierynomus.sshj.common.KeyAlgorithm; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.Buffer; import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.userauth.password.PasswordUtils; +import org.bouncycastle.asn1.nist.NISTNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; import org.bouncycastle.util.encoders.Hex; import javax.crypto.Cipher; @@ -101,17 +112,17 @@ public boolean isEncrypted() { protected KeyPair readKeyPair() throws IOException { this.parseKeyPair(); + final Buffer.PlainBuffer publicKeyReader = new Buffer.PlainBuffer(publicKey); + final Buffer.PlainBuffer privateKeyReader = new Buffer.PlainBuffer(privateKey); + publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name. if (KeyType.RSA.equals(this.getType())) { - final KeyReader publicKeyReader = new KeyReader(publicKey); - publicKeyReader.skip(); // skip this // public key exponent - BigInteger e = publicKeyReader.readInt(); + BigInteger e = publicKeyReader.readMPInt(); // modulus - BigInteger n = publicKeyReader.readInt(); + BigInteger n = publicKeyReader.readMPInt(); - final KeyReader privateKeyReader = new KeyReader(privateKey); // private key exponent - BigInteger d = privateKeyReader.readInt(); + BigInteger d = privateKeyReader.readMPInt(); final KeyFactory factory; try { @@ -129,16 +140,13 @@ protected KeyPair readKeyPair() throws IOException { } } 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); + BigInteger p = publicKeyReader.readMPInt(); + BigInteger q = publicKeyReader.readMPInt(); + BigInteger g = publicKeyReader.readMPInt(); + BigInteger y = publicKeyReader.readMPInt(); + // Private exponent from the private key - BigInteger x = privateKeyReader.readInt(); + BigInteger x = privateKeyReader.readMPInt(); final KeyFactory factory; try { @@ -154,9 +162,42 @@ protected KeyPair readKeyPair() throws IOException { } catch (InvalidKeySpecException e) { throw new IOException(e.getMessage(), e); } - } else { - throw new IOException(String.format("Unknown key type %s", this.getType())); } + if (KeyType.ED25519.equals(this.getType())) { + EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519"); + EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyReader.readBytes(), ed25519); + EdDSAPrivateKeySpec privateSpec = new EdDSAPrivateKeySpec(privateKeyReader.readBytes(), ed25519); + return new KeyPair(new EdDSAPublicKey(publicSpec), new EdDSAPrivateKey(privateSpec)); + } + final String ecdsaCurve; + switch (this.getType()) { + case ECDSA256: + ecdsaCurve = "P-256"; + break; + case ECDSA384: + ecdsaCurve = "P-384"; + break; + case ECDSA521: + ecdsaCurve = "P-521"; + break; + default: + ecdsaCurve = null; + break; + } + if (ecdsaCurve != null) { + BigInteger s = new BigInteger(1, privateKeyReader.readBytes()); + X9ECParameters ecParams = NISTNamedCurves.getByName(ecdsaCurve); + ECNamedCurveSpec ecCurveSpec = + new ECNamedCurveSpec(ecdsaCurve, ecParams.getCurve(), ecParams.getG(), ecParams.getN()); + ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec); + try { + PrivateKey privateKey = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks); + return new KeyPair(getType().readPubKeyFromBuffer(publicKeyReader), privateKey); + } catch (GeneralSecurityException e) { + throw new IOException(e.getMessage(), e); + } + } + throw new IOException(String.format("Unknown key type %s", this.getType())); } protected void parseKeyPair() throws IOException { @@ -297,40 +338,4 @@ private byte[] decrypt(final byte[] key, final String passphrase) throws IOExcep 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(); - byte[] r = new byte[len]; - di.readFully(r); - return r; - } - - /** - * Reads the next integer. - */ - public BigInteger readInt() throws IOException { - return new BigInteger(read()); - } - } } diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java index 598a93df9..b22b98407 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java @@ -15,14 +15,18 @@ */ package net.schmizz.sshj.keyprovider; +import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile; +import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile; import net.schmizz.sshj.userauth.password.PasswordFinder; import net.schmizz.sshj.userauth.password.Resource; import org.junit.Test; +import java.io.File; import java.io.IOException; import java.io.StringReader; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -246,6 +250,97 @@ public void test8192() throws Exception { assertNotNull(key.getPublic()); } + @Test + public void testEd25519() throws Exception { + // Generated with + // puttygen src/test/resources/keytypes/test_ed25519 -O private \ + // -o src/test/resources/keytypes/test_ed25519_puttygen.ppk + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new File("src/test/resources/keytypes/test_ed25519_puttygen.ppk")); + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + + OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile(); + referenceKey.init(new File("src/test/resources/keytypes/test_ed25519")); + assertEquals(key.getPrivate(), referenceKey.getPrivate()); + assertEquals(key.getPublic(), referenceKey.getPublic()); + } + + @Test + public void testEd25519Encrypted() throws Exception { + // Generated with + // puttygen src/test/resources/keytypes/test_ed25519 -O private \ + // -o src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk \ + // --new-passphrase <(echo 123456) + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new File("src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk"), new PasswordFinder() { + @Override + public char[] reqPassword(Resource resource) { + return "123456".toCharArray(); + } + + @Override + public boolean shouldRetry(Resource resource) { + return false; + } + }); + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + + OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile(); + referenceKey.init(new File("src/test/resources/keytypes/test_ed25519")); + assertEquals(key.getPrivate(), referenceKey.getPrivate()); + assertEquals(key.getPublic(), referenceKey.getPublic()); + } + + @Test + public void testEcDsa256() throws Exception { + // Generated with + // puttygen src/test/resources/keytypes/test_ecdsa_nistp256 -O private \ + // -o src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk")); + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + + PKCS8KeyFile referenceKey = new PKCS8KeyFile(); + referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp256")); + assertEquals(key.getPrivate(), referenceKey.getPrivate()); + assertEquals(key.getPublic(), referenceKey.getPublic()); + } + + @Test + public void testEcDsa384() throws Exception { + // Generated with + // puttygen src/test/resources/keytypes/test_ecdsa_nistp384_2 -O private \ + // -o src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk")); + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + + OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile(); + referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp384_2")); + assertEquals(key.getPrivate(), referenceKey.getPrivate()); + assertEquals(key.getPublic(), referenceKey.getPublic()); + } + + @Test + public void testEcDsa521() throws Exception { + // Generated with + // puttygen src/test/resources/keytypes/test_ecdsa_nistp521_2 -O private \ + // -o src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk + PuTTYKeyFile key = new PuTTYKeyFile(); + key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk")); + assertNotNull(key.getPrivate()); + assertNotNull(key.getPublic()); + + OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile(); + referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp521_2")); + assertEquals(key.getPrivate(), referenceKey.getPrivate()); + assertEquals(key.getPublic(), referenceKey.getPublic()); + } + @Test public void testCorrectPassphraseRsa() throws Exception { PuTTYKeyFile key = new PuTTYKeyFile(); diff --git a/src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk b/src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk new file mode 100644 index 000000000..c093165d9 --- /dev/null +++ b/src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk @@ -0,0 +1,10 @@ +PuTTY-User-Key-File-2: ecdsa-sha2-nistp256 +Encryption: none +Comment: imported-openssh-key +Public-Lines: 3 +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOEQcvowiV3i +gdRO7rKPrZrao1hCQrnC4tgsxqSJdQCbABI+vHrdbJRfWZNuSk48aAtARJzJVmkn +/r63EPJgkh8= +Private-Lines: 1 +AAAAIQCVDJbEpV6gmZgo5TeJFe4cz/qfabtH8CfK+JtapXufEg== +Private-MAC: 48f3a17cf5f65f4f225e7a21f007d8270d7c8c8f diff --git a/src/test/resources/keytypes/test_ecdsa_nistp384_2 b/src/test/resources/keytypes/test_ecdsa_nistp384_2 new file mode 100644 index 000000000..065f5a178 --- /dev/null +++ b/src/test/resources/keytypes/test_ecdsa_nistp384_2 @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTItEGNGyMGn9tCIM4oC3fpU7jVxDQP +RRkB/Qv8lfM4mmSuYLPcakV6av0ATlM6mKD/TObWQNOJAYzp3MsUn1EMgVLe/sd9TY/hP6 +8Vn+zumMqjmtdX70Ty5ftEoH9zBlgAAADYhfSye4X0snsAAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEEyLRBjRsjBp/bQiDOKAt36VO41cQ0D0UZAf0L/JXzOJ +pkrmCz3GpFemr9AE5TOpig/0zm1kDTiQGM6dzLFJ9RDIFS3v7HfU2P4T+vFZ/s7pjKo5rX +V+9E8uX7RKB/cwZYAAAAMGvH38HMnj6cELCBVQnAQYHlA/Vz1+RVZHj08cey/P3PALx7MR +pV135UZNZAtWQm+wAAAAlyb290QHNzaGoBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/src/test/resources/keytypes/test_ecdsa_nistp384_2.pub b/src/test/resources/keytypes/test_ecdsa_nistp384_2.pub new file mode 100644 index 000000000..cff421777 --- /dev/null +++ b/src/test/resources/keytypes/test_ecdsa_nistp384_2.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMi0QY0bIwaf20IgzigLd+lTuNXENA9FGQH9C/yV8ziaZK5gs9xqRXpq/QBOUzqYoP9M5tZA04kBjOncyxSfUQyBUt7+x31Nj+E/rxWf7O6YyqOa11fvRPLl+0Sgf3MGWA== root@sshj diff --git a/src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk b/src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk new file mode 100644 index 000000000..1467ce980 --- /dev/null +++ b/src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk @@ -0,0 +1,11 @@ +PuTTY-User-Key-File-2: ecdsa-sha2-nistp384 +Encryption: none +Comment: root@sshj +Public-Lines: 3 +AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMi0QY0bIwaf +20IgzigLd+lTuNXENA9FGQH9C/yV8ziaZK5gs9xqRXpq/QBOUzqYoP9M5tZA04kB +jOncyxSfUQyBUt7+x31Nj+E/rxWf7O6YyqOa11fvRPLl+0Sgf3MGWA== +Private-Lines: 2 +AAAAMGvH38HMnj6cELCBVQnAQYHlA/Vz1+RVZHj08cey/P3PALx7MRpV135UZNZA +tWQm+w== +Private-MAC: aa4d48441934e15491af0a30f75a02f4e324e652 diff --git a/src/test/resources/keytypes/test_ecdsa_nistp521_2 b/src/test/resources/keytypes/test_ecdsa_nistp521_2 new file mode 100644 index 000000000..70a0e95f8 --- /dev/null +++ b/src/test/resources/keytypes/test_ecdsa_nistp521_2 @@ -0,0 +1,12 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS +1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQA3ilD2XkhjkSuEj8KcIXWjhjKSOfQ +QEZBFZyoPT4QV8oRiGT1NRVcN86Paymq8M8WgANFVEAZp7eDqTnsKJ6LEpoAM93DJa1ERO +RWwSeDTDy5GIxMDYgg+CKZVhAMJmS/iavsSXyKUf1ibYo9b5S8y8rpzvmiRg/dQGkfloJR +BLu7czAAAAEI8uaocPLmqHAAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ +AAAIUEAN4pQ9l5IY5ErhI/CnCF1o4Yykjn0EBGQRWcqD0+EFfKEYhk9TUVXDfOj2spqvDP +FoADRVRAGae3g6k57CieixKaADPdwyWtRETkVsEng0w8uRiMTA2IIPgimVYQDCZkv4mr7E +l8ilH9Ym2KPW+UvMvK6c75okYP3UBpH5aCUQS7u3MwAAAAQSlrwjeSrVTc6OyiA3OTfac4 ++3nKcf/PRSjIhOLsGUIs2pVCxGYP8/ZfbVfkv7nHMn5Cc0fDZEs2cSWi2QhVKBSfAAAACX +Jvb3RAc3NoagEC +-----END OPENSSH PRIVATE KEY----- diff --git a/src/test/resources/keytypes/test_ecdsa_nistp521_2.pub b/src/test/resources/keytypes/test_ecdsa_nistp521_2.pub new file mode 100644 index 000000000..fb47e2765 --- /dev/null +++ b/src/test/resources/keytypes/test_ecdsa_nistp521_2.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADeKUPZeSGORK4SPwpwhdaOGMpI59BARkEVnKg9PhBXyhGIZPU1FVw3zo9rKarwzxaAA0VUQBmnt4OpOewonosSmgAz3cMlrURE5FbBJ4NMPLkYjEwNiCD4IplWEAwmZL+Jq+xJfIpR/WJtij1vlLzLyunO+aJGD91AaR+WglEEu7tzMA== root@sshj diff --git a/src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk b/src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk new file mode 100644 index 000000000..94e9bb50a --- /dev/null +++ b/src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk @@ -0,0 +1,12 @@ +PuTTY-User-Key-File-2: ecdsa-sha2-nistp521 +Encryption: none +Comment: root@sshj +Public-Lines: 4 +AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADeKUPZeSGO +RK4SPwpwhdaOGMpI59BARkEVnKg9PhBXyhGIZPU1FVw3zo9rKarwzxaAA0VUQBmn +t4OpOewonosSmgAz3cMlrURE5FbBJ4NMPLkYjEwNiCD4IplWEAwmZL+Jq+xJfIpR +/WJtij1vlLzLyunO+aJGD91AaR+WglEEu7tzMA== +Private-Lines: 2 +AAAAQSlrwjeSrVTc6OyiA3OTfac4+3nKcf/PRSjIhOLsGUIs2pVCxGYP8/ZfbVfk +v7nHMn5Cc0fDZEs2cSWi2QhVKBSf +Private-MAC: 052d1a2fe2c5837aec9dbe0bf10f2ccc376eda43 diff --git a/src/test/resources/keytypes/test_ed25519_puttygen.ppk b/src/test/resources/keytypes/test_ed25519_puttygen.ppk new file mode 100644 index 000000000..126aaa528 --- /dev/null +++ b/src/test/resources/keytypes/test_ed25519_puttygen.ppk @@ -0,0 +1,9 @@ +PuTTY-User-Key-File-2: ssh-ed25519 +Encryption: none +Comment: root@sshj +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPo +WICJ +Private-Lines: 1 +AAAAIKaxyRDJxad8ZArpe1ClowY4NsCQxA50k0rpclKKkHt0 +Private-MAC: 388f807649f181243015cad9650633ec28b25208 diff --git a/src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk b/src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk new file mode 100644 index 000000000..c3b381b2d --- /dev/null +++ b/src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk @@ -0,0 +1,9 @@ +PuTTY-User-Key-File-2: ssh-ed25519 +Encryption: aes256-cbc +Comment: root@sshj +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPo +WICJ +Private-Lines: 1 +XFJyRzRt5NjuCVhDEyb50sI+gRn8FB65hh0U8uhGvP3VBl4haChinQasOTBYa4pj +Private-MAC: 80f50e1a7075567980742644460edffeb67ca829