Skip to content

Commit 2e1ef9d

Browse files
Support v3 PuTTY keys (#716)
* Support v3 PuTTY keys * add test for putty v3 key * Format PuTTYKeyFile to fix Codacy warnings Signed-off-by: Jeroen van Erp <[email protected]> Co-authored-by: Jeroen van Erp <[email protected]>
1 parent 6f98737 commit 2e1ef9d

File tree

2 files changed

+43
-24
lines changed

2 files changed

+43
-24
lines changed

src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java

+24-24
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
/**
4747
* <h2>Sample PuTTY file format</h2>
48+
*
4849
* <pre>
4950
* PuTTY-User-Key-File-2: ssh-rsa
5051
* Encryption: none
@@ -70,8 +71,7 @@
7071
*/
7172
public class PuTTYKeyFile extends BaseFileKeyProvider {
7273

73-
public static class Factory
74-
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
74+
public static class Factory implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
7575

7676
@Override
7777
public FileKeyProvider create() {
@@ -88,33 +88,35 @@ public String getName() {
8888
private byte[] publicKey;
8989

9090
/**
91-
* Key type. Either "ssh-rsa" for RSA key, or "ssh-dss" for DSA key.
91+
* Key type
9292
*/
9393
@Override
9494
public KeyType getType() throws IOException {
95-
return KeyType.fromString(headers.get("PuTTY-User-Key-File-2"));
95+
for (String h : headers.keySet()) {
96+
if (h.startsWith("PuTTY-User-Key-File-")) {
97+
return KeyType.fromString(headers.get(h));
98+
}
99+
}
100+
return KeyType.UNKNOWN;
96101
}
97102

98103
public boolean isEncrypted() {
99104
// Currently the only supported encryption types are "aes256-cbc" and "none".
100105
return "aes256-cbc".equals(headers.get("Encryption"));
101106
}
102107

103-
private Map<String, String> payload
104-
= new HashMap<String, String>();
108+
private Map<String, String> payload = new HashMap<String, String>();
105109

106110
/**
107111
* For each line that looks like "Xyz: vvv", it will be stored in this map.
108112
*/
109-
private final Map<String, String> headers
110-
= new HashMap<String, String>();
111-
113+
private final Map<String, String> headers = new HashMap<String, String>();
112114

113115
protected KeyPair readKeyPair() throws IOException {
114116
this.parseKeyPair();
115117
final Buffer.PlainBuffer publicKeyReader = new Buffer.PlainBuffer(publicKey);
116118
final Buffer.PlainBuffer privateKeyReader = new Buffer.PlainBuffer(privateKey);
117-
publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name.
119+
publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name.
118120
if (KeyType.RSA.equals(this.getType())) {
119121
// public key exponent
120122
BigInteger e = publicKeyReader.readMPInt();
@@ -131,10 +133,8 @@ protected KeyPair readKeyPair() throws IOException {
131133
throw new IOException(s.getMessage(), s);
132134
}
133135
try {
134-
return new KeyPair(
135-
factory.generatePublic(new RSAPublicKeySpec(n, e)),
136-
factory.generatePrivate(new RSAPrivateKeySpec(n, d))
137-
);
136+
return new KeyPair(factory.generatePublic(new RSAPublicKeySpec(n, e)),
137+
factory.generatePrivate(new RSAPrivateKeySpec(n, d)));
138138
} catch (InvalidKeySpecException i) {
139139
throw new IOException(i.getMessage(), i);
140140
}
@@ -155,10 +155,8 @@ protected KeyPair readKeyPair() throws IOException {
155155
throw new IOException(s.getMessage(), s);
156156
}
157157
try {
158-
return new KeyPair(
159-
factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)),
160-
factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g))
161-
);
158+
return new KeyPair(factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)),
159+
factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g)));
162160
} catch (InvalidKeySpecException e) {
163161
throw new IOException(e.getMessage(), e);
164162
}
@@ -187,8 +185,8 @@ protected KeyPair readKeyPair() throws IOException {
187185
if (ecdsaCurve != null) {
188186
BigInteger s = new BigInteger(1, privateKeyReader.readBytes());
189187
X9ECParameters ecParams = NISTNamedCurves.getByName(ecdsaCurve);
190-
ECNamedCurveSpec ecCurveSpec =
191-
new ECNamedCurveSpec(ecdsaCurve, ecParams.getCurve(), ecParams.getG(), ecParams.getN());
188+
ECNamedCurveSpec ecCurveSpec = new ECNamedCurveSpec(ecdsaCurve, ecParams.getCurve(), ecParams.getG(),
189+
ecParams.getN());
192190
ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec);
193191
try {
194192
PrivateKey privateKey = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks);
@@ -247,7 +245,8 @@ protected void parseKeyPair() throws IOException {
247245
}
248246

249247
/**
250-
* Converts a passphrase into a key, by following the convention that PuTTY uses.
248+
* Converts a passphrase into a key, by following the convention that PuTTY
249+
* uses.
251250
* <p/>
252251
* <p/>
253252
* This is used to decrypt the private key when it's encrypted.
@@ -256,15 +255,16 @@ private byte[] toKey(final String passphrase) throws IOException {
256255
try {
257256
MessageDigest digest = MessageDigest.getInstance("SHA-1");
258257

259-
// The encryption key is derived from the passphrase by means of a succession of SHA-1 hashes.
258+
// The encryption key is derived from the passphrase by means of a succession of
259+
// SHA-1 hashes.
260260

261261
// Sequence number 0
262-
digest.update(new byte[]{0, 0, 0, 0});
262+
digest.update(new byte[] { 0, 0, 0, 0 });
263263
digest.update(passphrase.getBytes());
264264
byte[] key1 = digest.digest();
265265

266266
// Sequence number 1
267-
digest.update(new byte[]{0, 0, 0, 1});
267+
digest.update(new byte[] { 0, 0, 0, 1 });
268268
digest.update(passphrase.getBytes());
269269
byte[] key2 = digest.digest();
270270

src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java

+19
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,17 @@ public class PuTTYKeyFileTest {
226226
"nLEIBzB8WEaMMgDz5qwlqq7eBxLPIIi9uHyjMf7NOsQ=\n" +
227227
"Private-MAC: b200c6d801fc8dd8f84a14afc3b94d9f9bb2df90\n";
228228

229+
final static String v3_ecdsa = "PuTTY-User-Key-File-3: ecdsa-sha2-nistp256\n" +
230+
"Encryption: none\n" +
231+
"Comment: ecdsa-key-20210819\n" +
232+
"Public-Lines: 3\n" +
233+
"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBP5mbdlgVmkw\n" +
234+
"LzDkznoY8TXKnok/mlMkpk8FELFNSECnXNdtZ4B8+Bpqnvchhk/jY/0tUU98lFxt\n" +
235+
"JR0o0l8B5y0=\n" +
236+
"Private-Lines: 1\n" +
237+
"AAAAIEblmwyKaGuvc6dLgNeHsc1BuZeQORTSxBF5SBLNyjYc\n" +
238+
"Private-MAC: e1aed15a209f48fdaa5228640f1109a7740340764a96f97ec6023da7f92d07ea";
239+
229240
@Test
230241
public void test2048() throws Exception {
231242
PuTTYKeyFile key = new PuTTYKeyFile();
@@ -341,6 +352,14 @@ public void testEcDsa521() throws Exception {
341352
assertEquals(key.getPublic(), referenceKey.getPublic());
342353
}
343354

355+
@Test
356+
public void testV3Key() throws Exception {
357+
PuTTYKeyFile key = new PuTTYKeyFile();
358+
key.init(new StringReader(v3_ecdsa));
359+
assertNotNull(key.getPrivate());
360+
assertNotNull(key.getPublic());
361+
}
362+
344363
@Test
345364
public void testCorrectPassphraseRsa() throws Exception {
346365
PuTTYKeyFile key = new PuTTYKeyFile();

0 commit comments

Comments
 (0)