Skip to content

Commit d6d6f0d

Browse files
authored
only process supported Putty v3 keys + minor optimizations (#729)
1 parent 93de1ec commit d6d6f0d

File tree

2 files changed

+92
-13
lines changed

2 files changed

+92
-13
lines changed

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

+26-11
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,16 @@ public KeyType getType() throws IOException {
100100
return KeyType.UNKNOWN;
101101
}
102102

103-
public boolean isEncrypted() {
104-
// Currently the only supported encryption types are "aes256-cbc" and "none".
105-
return "aes256-cbc".equals(headers.get("Encryption"));
103+
public boolean isEncrypted() throws IOException {
104+
// Currently, the only supported encryption types are "aes256-cbc" and "none".
105+
String encryption = headers.get("Encryption");
106+
if ("none".equals(encryption)) {
107+
return false;
108+
}
109+
if ("aes256-cbc".equals(encryption)) {
110+
return true;
111+
}
112+
throw new IOException(String.format("Unsupported encryption: %s", encryption));
106113
}
107114

108115
private Map<String, String> payload = new HashMap<String, String>();
@@ -116,8 +123,9 @@ protected KeyPair readKeyPair() throws IOException {
116123
this.parseKeyPair();
117124
final Buffer.PlainBuffer publicKeyReader = new Buffer.PlainBuffer(publicKey);
118125
final Buffer.PlainBuffer privateKeyReader = new Buffer.PlainBuffer(privateKey);
126+
final KeyType keyType = this.getType();
119127
publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name.
120-
if (KeyType.RSA.equals(this.getType())) {
128+
if (KeyType.RSA.equals(keyType)) {
121129
// public key exponent
122130
BigInteger e = publicKeyReader.readMPInt();
123131
// modulus
@@ -139,7 +147,7 @@ protected KeyPair readKeyPair() throws IOException {
139147
throw new IOException(i.getMessage(), i);
140148
}
141149
}
142-
if (KeyType.DSA.equals(this.getType())) {
150+
if (KeyType.DSA.equals(keyType)) {
143151
BigInteger p = publicKeyReader.readMPInt();
144152
BigInteger q = publicKeyReader.readMPInt();
145153
BigInteger g = publicKeyReader.readMPInt();
@@ -161,14 +169,14 @@ protected KeyPair readKeyPair() throws IOException {
161169
throw new IOException(e.getMessage(), e);
162170
}
163171
}
164-
if (KeyType.ED25519.equals(this.getType())) {
172+
if (KeyType.ED25519.equals(keyType)) {
165173
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
166174
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyReader.readBytes(), ed25519);
167175
EdDSAPrivateKeySpec privateSpec = new EdDSAPrivateKeySpec(privateKeyReader.readBytes(), ed25519);
168176
return new KeyPair(new EdDSAPublicKey(publicSpec), new EdDSAPrivateKey(privateSpec));
169177
}
170178
final String ecdsaCurve;
171-
switch (this.getType()) {
179+
switch (keyType) {
172180
case ECDSA256:
173181
ecdsaCurve = "P-256";
174182
break;
@@ -190,7 +198,7 @@ protected KeyPair readKeyPair() throws IOException {
190198
ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec);
191199
try {
192200
PrivateKey privateKey = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks);
193-
return new KeyPair(getType().readPubKeyFromBuffer(publicKeyReader), privateKey);
201+
return new KeyPair(keyType.readPubKeyFromBuffer(publicKeyReader), privateKey);
194202
} catch (GeneralSecurityException e) {
195203
throw new IOException(e.getMessage(), e);
196204
}
@@ -252,6 +260,12 @@ protected void parseKeyPair() throws IOException {
252260
* This is used to decrypt the private key when it's encrypted.
253261
*/
254262
private byte[] toKey(final String passphrase) throws IOException {
263+
// The field Key-Derivation has been introduced with Putty v3 key file format
264+
// The only available formats are "Argon2i" "Argon2d" and "Argon2id"
265+
String keyDerivation = headers.get("Key-Derivation");
266+
if (keyDerivation != null) {
267+
throw new IOException(String.format("Unsupported key derivation function: %s", keyDerivation));
268+
}
255269
try {
256270
MessageDigest digest = MessageDigest.getInstance("SHA-1");
257271

@@ -283,7 +297,7 @@ private byte[] toKey(final String passphrase) throws IOException {
283297
*/
284298
private void verify(final String passphrase) throws IOException {
285299
try {
286-
// The key to the MAC is itself a SHA-1 hash of:
300+
// The key to the MAC is itself a SHA-1 hash of (v1/v2 key only):
287301
MessageDigest digest = MessageDigest.getInstance("SHA-1");
288302
digest.update("putty-private-key-file-mac-key".getBytes());
289303
if (passphrase != null) {
@@ -297,8 +311,9 @@ private void verify(final String passphrase) throws IOException {
297311
final ByteArrayOutputStream out = new ByteArrayOutputStream();
298312
final DataOutputStream data = new DataOutputStream(out);
299313
// name of algorithm
300-
data.writeInt(this.getType().toString().length());
301-
data.writeBytes(this.getType().toString());
314+
String keyType = this.getType().toString();
315+
data.writeInt(keyType.length());
316+
data.writeBytes(keyType);
302317

303318
data.writeInt(headers.get("Encryption").length());
304319
data.writeBytes(headers.get("Encryption"));

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

+66-2
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
import java.io.File;
2626
import java.io.IOException;
2727
import java.io.StringReader;
28+
import java.security.PrivateKey;
2829

2930
import static org.junit.Assert.assertEquals;
3031
import static org.junit.Assert.assertNotNull;
3132
import static org.junit.Assert.assertNull;
33+
import static org.junit.Assert.assertTrue;
34+
import static org.junit.Assert.fail;
3235

3336
public class PuTTYKeyFileTest {
3437

@@ -236,7 +239,39 @@ public class PuTTYKeyFileTest {
236239
"Private-Lines: 1\n" +
237240
"AAAAIEblmwyKaGuvc6dLgNeHsc1BuZeQORTSxBF5SBLNyjYc\n" +
238241
"Private-MAC: e1aed15a209f48fdaa5228640f1109a7740340764a96f97ec6023da7f92d07ea";
239-
242+
243+
final static String v3_rsa_encrypted = "PuTTY-User-Key-File-3: ssh-rsa\n" +
244+
"Encryption: aes256-cbc\n" +
245+
"Comment: rsa-key-20210926\n" +
246+
"Public-Lines: 6\n" +
247+
"AAAAB3NzaC1yc2EAAAADAQABAAABAQCBjWQHMpKAQnU3vZZF/iHn4RA867Ox+U03\n" +
248+
"/GOHivW0SgGIQbhKcSSWvTzYOE+GQdtX9T2KJxr76z/lB4nghkcWkpLoQW91gNBf\n" +
249+
"PUagMvaBxKXC8cNqaMm99uw5KpRg8SpTJWxwYPlQtzmyxav0PRFeOMSsiRsnjNuX\n" +
250+
"polMDSu6vmkkuKrPzvinPZbsXoZeMybcm1gn2Zq+7ik4us0icaGxRJRuF+nVqYag\n" +
251+
"EmO9jmQoytyqoNWzvPYEh/dh85hESwtIKXiaMOjQg52dW5BuELPGV7ZxaKRK7Znw\n" +
252+
"RGW6CtoGYulo0mJz5IZslDrRK/EK2bSGDbrlAcYaajROB6aBDyaJ\n" +
253+
"Key-Derivation: Argon2id\n" +
254+
"Argon2-Memory: 8192\n" +
255+
"Argon2-Passes: 21\n" +
256+
"Argon2-Parallelism: 1\n" +
257+
"Argon2-Salt: baf1530601433715467614d044c0e4a5\n" +
258+
"Private-Lines: 14\n" +
259+
"QAJl3mq/QJc8/of4xWbgBuE09GdgIuVhRYGAV5yC5C0dpuiJ+yF/6h7mk36s5E3Q\n" +
260+
"k32l+ZoWHG/kBc8s6N9rTQnIgC/eieNlN5FK3OSSoI9PBvoAtNEVWsR2T4U6ZkAG\n" +
261+
"FbyF3vRWq2h9Ux8flZusySqafQ2AhXP79pr13wvMziv1QbPkPFHWaR1Uvq9w0GJq\n" +
262+
"rfR+M6t8/6aPKhnsCTy8MiAoIcjeZmHiG/vOMIBBoWI7KtpD5IrbO4pIgzRK8m9Z\n" +
263+
"JqQvgWCPnddwCeiDFOZwf/Bm6g+duQYId4upB1IxSs34j21a7ZkMSExDZyV0d13S\n" +
264+
"G59U9pReZ7mHyIjORqeY7ssr/L9aJPPa7YCu4J5a/Bn/ARf/X5XmMnueFZ6H806M\n" +
265+
"ZUtHzeG2sZGoHULpwEaY1zRQs1JD5UAeaFzgDpzD4oeaD8v+FS3RdNlgj2gtWNcl\n" +
266+
"h8nvWD60XbylR0BdbB553xGuC8HC0482xQCCJUc8SMHZ/k2+FKTaf2m2p4dLyKkk\n" +
267+
"Qrw43QcmkgypUPRHKvnVs+6qUYMDHkwtPR1ZGFqHQzlHozvO9NdY/ZXTln/qfPZA\n" +
268+
"5w5TKvy0/GvofhISJCMocnPbkqGR6fDcKbpUjAS/RDgsCKKS5hxf6nhsYUgrXA4G\n" +
269+
"hXIgqGnMefLemjRG7dD/3XE8NmF6Q8mjIideEOBeP4tRCaDC2n90rZ3yChP9bsel\n" +
270+
"yg/TeKxj7OLk+X3ocP3yw2lsp3zOPsptSNtGI7g9VaIPGtxGaqRaIuObdLbBxCeR\n" +
271+
"ZgKSIuWtz8W1kT0aWuZ0aXMPagGao0ZsffmroyVpGbzW3QaI9633Krmf7EyphZoy\n" +
272+
"6tV3Z/GJ5aQJFeMYPOq69ktXRLAWr800822NwEStcxtQHTWbaTk7dxh8+0xwlCgI\n" +
273+
"Private-MAC: 582dea09758afd93a8e248abce358287d384e5ee36d21515ffcc0d42d8c5d86a\n";
274+
240275
@Test
241276
public void test2048() throws Exception {
242277
PuTTYKeyFile key = new PuTTYKeyFile();
@@ -359,7 +394,36 @@ public void testV3Key() throws Exception {
359394
assertNotNull(key.getPrivate());
360395
assertNotNull(key.getPublic());
361396
}
362-
397+
398+
/**
399+
* Reading an encrypted Putty v3 key requires an Argon2i/Argon2d/Argon2id
400+
* implementation.
401+
* Putty v3 keys additionally use a different algorithm for generating the "Private-MAC"
402+
*/
403+
@Test
404+
public void testRSAv3EncryptedKey() throws Exception {
405+
PuTTYKeyFile key = new PuTTYKeyFile();
406+
key.init(new StringReader(v3_rsa_encrypted), new PasswordFinder() {
407+
@Override
408+
public char[] reqPassword(Resource<?> resource) {
409+
return "changeit".toCharArray();
410+
}
411+
412+
@Override
413+
public boolean shouldRetry(Resource<?> resource) {
414+
return false;
415+
}
416+
});
417+
try {
418+
PrivateKey privateKey = key.getPrivate();
419+
fail("IOException expected as encrypted Putty v3 keys are not yet supported");
420+
} catch (IOException e) {
421+
assertTrue(e.getMessage().startsWith("Unsupported key derivation function"));
422+
}
423+
// assertNotNull(key.getPrivate());
424+
// assertNotNull(key.getPublic());
425+
}
426+
363427
@Test
364428
public void testCorrectPassphraseRsa() throws Exception {
365429
PuTTYKeyFile key = new PuTTYKeyFile();

0 commit comments

Comments
 (0)