full support for encrypted PuTTY v3 files (#730)

* full support for encrypted PuTTY v3 files (Argon2 library not included)

* simplified the PuTTYKeyDerivation interface and provided an abstract PuTTYArgon2 class for an easy Argon2 integration

* use Argon2 implementation from Bouncy Castle

* missing license header added

* license header again

* unit tests extended to cover all Argon2 variants and non-standard Argon2 parameters; verify the loaded keys
This commit is contained in:
Jan S
2021-10-12 09:47:11 +02:00
committed by GitHub
parent d6d6f0dd33
commit b2115dea6f
5 changed files with 364 additions and 161 deletions

View File

@@ -29,6 +29,8 @@ 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.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.util.encoders.Hex;
@@ -40,7 +42,10 @@ import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
@@ -84,20 +89,18 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
}
}
private Integer keyFileVersion;
private byte[] privateKey;
private byte[] publicKey;
private byte[] verifyHmac; // only used by v3 keys
/**
* Key type
*/
@Override
public KeyType getType() throws IOException {
for (String h : headers.keySet()) {
if (h.startsWith("PuTTY-User-Key-File-")) {
return KeyType.fromString(headers.get(h));
}
}
return KeyType.UNKNOWN;
String headerName = String.format("PuTTY-User-Key-File-%d", this.keyFileVersion);
return KeyType.fromString(headers.get(headerName));
}
public boolean isEncrypted() throws IOException {
@@ -207,6 +210,7 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
}
protected void parseKeyPair() throws IOException {
this.keyFileVersion = null;
BufferedReader r = new BufferedReader(resource.getReader());
// Parse the text into headers and payloads
try {
@@ -217,6 +221,9 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
if (idx > 0) {
headerName = line.substring(0, idx);
headers.put(headerName, line.substring(idx + 2));
if (headerName.startsWith("PuTTY-User-Key-File-")) {
this.keyFileVersion = Integer.parseInt(headerName.substring(20));
}
} else {
String s = payload.get(headerName);
if (s == null) {
@@ -232,6 +239,9 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
} finally {
r.close();
}
if (this.keyFileVersion == null) {
throw new IOException("Invalid key file format: missing \"PuTTY-User-Key-File-?\" entry");
}
// Retrieve keys from payload
publicKey = Base64.decode(payload.get("Public-Lines"));
if (this.isEncrypted()) {
@@ -242,8 +252,14 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
passphrase = "".toCharArray();
}
try {
privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), new String(passphrase));
this.verify(new String(passphrase));
privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), passphrase);
Mac mac;
if (this.keyFileVersion <= 2) {
mac = this.prepareVerifyMacV2(passphrase);
} else {
mac = this.prepareVerifyMacV3();
}
this.verify(mac);
} finally {
PasswordUtils.blankOut(passphrase);
}
@@ -254,61 +270,106 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
/**
* Converts a passphrase into a key, by following the convention that PuTTY
* uses.
* <p/>
* <p/>
* uses. Only PuTTY v1/v2 key files
* <p><p/>
* This is used to decrypt the private key when it's encrypted.
*/
private byte[] toKey(final String passphrase) throws IOException {
private void initCipher(final char[] passphrase, Cipher cipher) throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
// The field Key-Derivation has been introduced with Putty v3 key file format
// The only available formats are "Argon2i" "Argon2d" and "Argon2id"
String keyDerivation = headers.get("Key-Derivation");
if (keyDerivation != null) {
throw new IOException(String.format("Unsupported key derivation function: %s", keyDerivation));
// For v3 the algorithms are "Argon2i" "Argon2d" and "Argon2id"
String kdfAlgorithm = headers.get("Key-Derivation");
if (kdfAlgorithm != null) {
kdfAlgorithm = kdfAlgorithm.toLowerCase();
byte[] keyData = this.argon2(kdfAlgorithm, passphrase);
if (keyData == null) {
throw new IOException(String.format("Unsupported key derivation function: %s", kdfAlgorithm));
}
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] tag = new byte[32]; // Hmac key
System.arraycopy(keyData, 0, key, 0, 32);
System.arraycopy(keyData, 32, iv, 0, 16);
System.arraycopy(keyData, 48, tag, 0, 32);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
new IvParameterSpec(iv));
verifyHmac = tag;
return;
}
// Key file format v1 + v2
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
// The encryption key is derived from the passphrase by means of a succession of
// SHA-1 hashes.
byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase);
// Sequence number 0
digest.update(new byte[]{0, 0, 0, 0});
digest.update(passphrase.getBytes());
digest.update(encodedPassphrase);
byte[] key1 = digest.digest();
// Sequence number 1
digest.update(new byte[]{0, 0, 0, 1});
digest.update(passphrase.getBytes());
digest.update(encodedPassphrase);
byte[] key2 = digest.digest();
byte[] r = new byte[32];
System.arraycopy(key1, 0, r, 0, 20);
System.arraycopy(key2, 0, r, 20, 12);
Arrays.fill(encodedPassphrase, (byte) 0);
byte[] expanded = new byte[32];
System.arraycopy(key1, 0, expanded, 0, 20);
System.arraycopy(key2, 0, expanded, 20, 12);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"),
new IvParameterSpec(new byte[16])); // initial vector=0
return r;
} catch (NoSuchAlgorithmException e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* Verify the MAC.
* Uses BouncyCastle Argon2 implementation
*/
private void verify(final String passphrase) throws IOException {
try {
// The key to the MAC is itself a SHA-1 hash of (v1/v2 key only):
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update("putty-private-key-file-mac-key".getBytes());
if (passphrase != null) {
digest.update(passphrase.getBytes());
private byte[] argon2(String algorithm, final char[] passphrase) throws IOException {
int type;
if ("argon2i".equals(algorithm)) {
type = Argon2Parameters.ARGON2_i;
} else if ("argon2d".equals(algorithm)) {
type = Argon2Parameters.ARGON2_d;
} else if ("argon2id".equals(algorithm)) {
type = Argon2Parameters.ARGON2_id;
} else {
return null;
}
final byte[] key = digest.digest();
byte[] salt = Hex.decode(headers.get("Argon2-Salt"));
int iterations = Integer.parseInt(headers.get("Argon2-Passes"));
int memory = Integer.parseInt(headers.get("Argon2-Memory"));
int parallelism = Integer.parseInt(headers.get("Argon2-Parallelism"));
final Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(key, 0, 20, mac.getAlgorithm()));
Argon2Parameters a2p = new Argon2Parameters.Builder(type)
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
.withIterations(iterations)
.withMemoryAsKB(memory)
.withParallelism(parallelism)
.withSalt(salt).build();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(a2p);
byte[] output = new byte[80];
int bytes = generator.generateBytes(passphrase, output);
if (bytes != output.length) {
throw new IOException("Failed to generate key via Argon2");
}
return output;
}
/**
* Verify the MAC (only required for v1/v2 keys. v3 keys are automatically
* verified as part of the decryption process.
*/
private void verify(final Mac mac) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream(256);
final DataOutputStream data = new DataOutputStream(out);
// name of algorithm
String keyType = this.getType().toString();
@@ -332,7 +393,34 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
if (!encoded.equals(reference)) {
throw new IOException("Invalid passphrase");
}
} catch (GeneralSecurityException e) {
}
private Mac prepareVerifyMacV2(final char[] passphrase) throws IOException {
// The key to the MAC is itself a SHA-1 hash of (v1/v2 key only):
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update("putty-private-key-file-mac-key".getBytes());
if (passphrase != null) {
byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase);
digest.update(encodedPassphrase);
Arrays.fill(encodedPassphrase, (byte) 0);
}
final byte[] key = digest.digest();
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(key, 0, 20, mac.getAlgorithm()));
return mac;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IOException(e.getMessage(), e);
}
}
private Mac prepareVerifyMacV3() throws IOException {
// for v3 keys the hMac key is included in the Argon output
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(this.verifyHmac, 0, 32, mac.getAlgorithm()));
return mac;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IOException(e.getMessage(), e);
}
}
@@ -340,17 +428,21 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
/**
* Decrypt private key
*
* @param privateKey the SSH private key to be decrypted
* @param passphrase To decrypt
*/
private byte[] decrypt(final byte[] key, final String passphrase) throws IOException {
private byte[] decrypt(final byte[] privateKey, final char[] 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);
this.initCipher(passphrase, cipher);
return cipher.doFinal(privateKey);
} catch (GeneralSecurityException e) {
throw new IOException(e.getMessage(), e);
}
}
public int getKeyFileVersion() {
return keyFileVersion;
}
}

View File

@@ -15,6 +15,8 @@
*/
package net.schmizz.sshj.userauth.password;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/** Static utility method and factories */
@@ -54,4 +56,14 @@ public class PasswordUtils {
};
}
/**
* Converts a password to a UTF-8 encoded byte array
*
* @param password
* @return
*/
public static byte[] toByteArray(char[] password) {
CharBuffer charBuffer = CharBuffer.wrap(password);
return StandardCharsets.UTF_8.encode(charBuffer).array();
}
}

View File

@@ -18,20 +18,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 net.schmizz.sshj.util.UnitTestPasswordFinder;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.security.PrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class PuTTYKeyFileTest {
@@ -240,7 +238,7 @@ public class PuTTYKeyFileTest {
"AAAAIEblmwyKaGuvc6dLgNeHsc1BuZeQORTSxBF5SBLNyjYc\n" +
"Private-MAC: e1aed15a209f48fdaa5228640f1109a7740340764a96f97ec6023da7f92d07ea";
final static String v3_rsa_encrypted = "PuTTY-User-Key-File-3: ssh-rsa\n" +
final static String v3_rsa_argon2id = "PuTTY-User-Key-File-3: ssh-rsa\n" +
"Encryption: aes256-cbc\n" +
"Comment: rsa-key-20210926\n" +
"Public-Lines: 6\n" +
@@ -272,6 +270,70 @@ public class PuTTYKeyFileTest {
"6tV3Z/GJ5aQJFeMYPOq69ktXRLAWr800822NwEStcxtQHTWbaTk7dxh8+0xwlCgI\n" +
"Private-MAC: 582dea09758afd93a8e248abce358287d384e5ee36d21515ffcc0d42d8c5d86a\n";
final static String v3_rsa_argon2d = "PuTTY-User-Key-File-3: ssh-rsa\n" +
"Encryption: aes256-cbc\n" +
"Comment: rsa-key-20210926\n" +
"Public-Lines: 6\n" +
"AAAAB3NzaC1yc2EAAAADAQABAAABAQCBjWQHMpKAQnU3vZZF/iHn4RA867Ox+U03\n" +
"/GOHivW0SgGIQbhKcSSWvTzYOE+GQdtX9T2KJxr76z/lB4nghkcWkpLoQW91gNBf\n" +
"PUagMvaBxKXC8cNqaMm99uw5KpRg8SpTJWxwYPlQtzmyxav0PRFeOMSsiRsnjNuX\n" +
"polMDSu6vmkkuKrPzvinPZbsXoZeMybcm1gn2Zq+7ik4us0icaGxRJRuF+nVqYag\n" +
"EmO9jmQoytyqoNWzvPYEh/dh85hESwtIKXiaMOjQg52dW5BuELPGV7ZxaKRK7Znw\n" +
"RGW6CtoGYulo0mJz5IZslDrRK/EK2bSGDbrlAcYaajROB6aBDyaJ\n" +
"Key-Derivation: Argon2d\n" +
"Argon2-Memory: 2048\n" +
"Argon2-Passes: 5\n" +
"Argon2-Parallelism: 3\n" +
"Argon2-Salt: 5fa1eb89e9eac0cc562c59bc648cacea\n" +
"Private-Lines: 14\n" +
"CCLbHvtNdkMNqOrSM+CNF874xTjDs01+UanZX7pHmIA94nAbb9ofeEHPcw7pCCWq\n" +
"mxj7GK8BnsEQXIS1yCnRT6yCi1d68FpdXN2QvhlbWpEuzrmw4q71XpCwYpZ3KERo\n" +
"+/o7X2Pi4qhfaS+fgBAl2VwAiHdN2KQFewj6MWJqP2/GaegKyvnZue/3e+v/Edag\n" +
"DCbODfNhfirISUlw5U3SxqmIdrFT+2DKbpVTCLQwTeXL+fmzdYYvjOGQq6Kx7E9L\n" +
"iWf1aLoBZCWfN5gpMxD1F1tX1nBXmFMG8aW+lOr3+BxLMUAtjRQVsmc6Lyqb1RdV\n" +
"eyZp1W0R2+HKmwm59WUQK46HMnXdkwUqArb28VpBE61gj+KMWna9TJP6aJTF2N6m\n" +
"0Wv8D9WCGOrOC+IqnkfkfdSLkupu6PyyhiS69IR9b6vAyDYFxhtlEx6qZpjSKLYr\n" +
"X11I223yPAmSoO1X24RNPpo1uU4k8NfZWH0ZICY3YZ0K3PnETNGd5C38OSptQFor\n" +
"9aY1oV/1VencX/CmGXaQHsV5UJ/SnV78+PPSv3pEeQmd2ljmSx3kTL1BX91n4/Mc\n" +
"jNxE3kMXJ+6DD6OGGU0VbVmYBCrFDD4Mfj8yyLKOjJgEZubCLaZoI7WhDk4qZcui\n" +
"hzPt1tshrjIN6VKubqg84BVOWmJ3MmDD76ci9d5ILeAm4zzsliuagSLa+Y6t03hs\n" +
"PmRnFSiCv1zrqLl20PcrPEsifGeC/o1839/9E0Gywy/JDjlbucxfU9qHOntnqQJM\n" +
"8cAjXyuzgkKC5yzk/Py3VnjWegENrfM5Zf/eXFYFzD0cIA0ou2ap+Dcln14ckGFZ\n" +
"kir9AVgxyOiQikD8za+QjZ2rLeuzODe9mKPPKitI4npanpGcWRl+RPCG4t9poacO\n" +
"Private-MAC: d08aebc419131c109bbf8c200848f47eafedab9286b372c3155e8dc27e6b84cd\n";
final static String v3_rsa_argon2i = "PuTTY-User-Key-File-3: ssh-rsa\n" +
"Encryption: aes256-cbc\n" +
"Comment: rsa-key-20210926\n" +
"Public-Lines: 6\n" +
"AAAAB3NzaC1yc2EAAAADAQABAAABAQCBjWQHMpKAQnU3vZZF/iHn4RA867Ox+U03\n" +
"/GOHivW0SgGIQbhKcSSWvTzYOE+GQdtX9T2KJxr76z/lB4nghkcWkpLoQW91gNBf\n" +
"PUagMvaBxKXC8cNqaMm99uw5KpRg8SpTJWxwYPlQtzmyxav0PRFeOMSsiRsnjNuX\n" +
"polMDSu6vmkkuKrPzvinPZbsXoZeMybcm1gn2Zq+7ik4us0icaGxRJRuF+nVqYag\n" +
"EmO9jmQoytyqoNWzvPYEh/dh85hESwtIKXiaMOjQg52dW5BuELPGV7ZxaKRK7Znw\n" +
"RGW6CtoGYulo0mJz5IZslDrRK/EK2bSGDbrlAcYaajROB6aBDyaJ\n" +
"Key-Derivation: Argon2i\n" +
"Argon2-Memory: 1024\n" +
"Argon2-Passes: 5\n" +
"Argon2-Parallelism: 2\n" +
"Argon2-Salt: 2845c351de77c5aa9604e407ca830136\n" +
"Private-Lines: 14\n" +
"Ws3CZMJ0xYa/W6s0YZqn4j8ihXK81lw88iuXrmzWu+L88+RVGTBGvOvmE0oqLsMw\n" +
"YPIi7/eOaik1jZ+dnnD/PeJbVOqch7z2fSK1cVXMyNggPvFBQVjtxrFRhvGtIC0R\n" +
"5py8Z7Cfi0W9N/xyjHIvNrGwuvUQpBKeK1C/zYweQJF/clBSovnV/sGGRbtEd+jk\n" +
"rY8svRKSvX0HY0P4xftwH+E40XZhUdG2JetleCNIw0ohShuCSiO1fauxI3Az/i2J\n" +
"Ef3pRfMLCE91QW4/3nY2ofK6yyufNhyFSjqIaDkQUNBi4EYG1W2/29mK9zLpfa+Z\n" +
"eiujzOZJfI8QPar7gTp2sdrq7ND2YUniatwqpq9+vefTkWvMEhwuNAGvfRWgJ2qq\n" +
"IbB/EWtvNj8vA3z3M2j0ksMRvJSGpU1n8MKVdWe5PSjvpMiCaqtOTtrP3iqS+bwJ\n" +
"WjhV+JVod5RE0fCXnBcCkE8XdSu20m04aRIVHJvnIaKH7vZXThDdG9AhpSrUcvWM\n" +
"OVD5q0L9W9wcVQzN7XtQhTEjm3zja+tOo0gYn0Z/497kkxdL/g/su5kpPQsbbsLF\n" +
"0ROS5U2GZX0Le+QVg6hGIfqskBoCQp+ErTXFzIu+0//MoaZSACtW48ljeIpDj0fG\n" +
"v2Fhc9tbpTJKvQh6wlm9gkMBSV+XcRWUMh5zBPecmR6/v9O4/MCsOse89MNs4LxL\n" +
"sLRUABdjziKnjomq/1FlozlGGfF+v+VLhjjc1xq5ms+BEqkXUsWoJl8NNST6NqkN\n" +
"2T4nFzZA6b+RwFJFqYHF+BvgkQ5j0hEbXo1qlqKIf3Vk+/rouPkLyUIiHxZxdX4m\n" +
"P/LtnH79FPDQFbFl6826Ui1TPISAf3pTwKFI43HgKRrya3F5GPeQphsZHlu155JO\n" +
"Private-MAC: 1be8357d497fd4d641ce50a142c5a91ef3b0279355d2996e0c1f13e376394301\n";
@Test
public void test2048() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile();
@@ -319,17 +381,8 @@ public class PuTTYKeyFileTest {
// -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;
}
});
key.init(new File("src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk"),
new UnitTestPasswordFinder("123456"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
@@ -388,57 +441,62 @@ public class PuTTYKeyFileTest {
}
@Test
public void testV3Key() throws Exception {
public void testV3KeyArgon2id() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new StringReader(v3_ecdsa));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
}
/**
* Reading an encrypted Putty v3 key requires an Argon2i/Argon2d/Argon2id
* implementation.
* Putty v3 keys additionally use a different algorithm for generating the "Private-MAC"
*/
@Test
public void testRSAv3EncryptedKey() throws Exception {
public void testRSAv3EncryptedKeyArgon2id() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new StringReader(v3_rsa_encrypted), new PasswordFinder() {
@Override
public char[] reqPassword(Resource<?> resource) {
return "changeit".toCharArray();
key.init(new StringReader(v3_rsa_argon2id), new UnitTestPasswordFinder("changeit"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_rsa_putty_priv.openssh2"));
RSAPrivateKey loadedPrivate = (RSAPrivateKey) key.getPrivate();
RSAPrivateKey referencePrivate = (RSAPrivateKey) referenceKey.getPrivate();
assertEquals(referencePrivate.getPrivateExponent(), loadedPrivate.getPrivateExponent());
assertEquals(referencePrivate.getModulus(), loadedPrivate.getModulus());
assertEquals(referencePrivate.getModulus(), ((RSAPublicKey) key.getPublic()).getModulus());
}
@Override
public boolean shouldRetry(Resource<?> resource) {
return false;
@Test
public void testRSAv3EncryptedKeyArgon2d() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new StringReader(v3_rsa_argon2d), new UnitTestPasswordFinder("changeit"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_rsa_putty_priv.openssh2"));
RSAPrivateKey loadedPrivate = (RSAPrivateKey) key.getPrivate();
RSAPrivateKey referencePrivate = (RSAPrivateKey) referenceKey.getPrivate();
assertEquals(referencePrivate.getPrivateExponent(), loadedPrivate.getPrivateExponent());
assertEquals(referencePrivate.getModulus(), loadedPrivate.getModulus());
assertEquals(referencePrivate.getModulus(), ((RSAPublicKey) key.getPublic()).getModulus());
}
});
try {
PrivateKey privateKey = key.getPrivate();
fail("IOException expected as encrypted Putty v3 keys are not yet supported");
} catch (IOException e) {
assertTrue(e.getMessage().startsWith("Unsupported key derivation function"));
}
// assertNotNull(key.getPrivate());
// assertNotNull(key.getPublic());
@Test
public void testRSAv3EncryptedKeyArgon2i() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new StringReader(v3_rsa_argon2i), new UnitTestPasswordFinder("changeit"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_rsa_putty_priv.openssh2"));
RSAPrivateKey loadedPrivate = (RSAPrivateKey) key.getPrivate();
RSAPrivateKey referencePrivate = (RSAPrivateKey) referenceKey.getPrivate();
assertEquals(referencePrivate.getPrivateExponent(), loadedPrivate.getPrivateExponent());
assertEquals(referencePrivate.getModulus(), loadedPrivate.getModulus());
assertEquals(referencePrivate.getModulus(), ((RSAPublicKey) key.getPublic()).getModulus());
}
@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;
}
});
key.init(new StringReader(ppk1024_passphrase), new UnitTestPasswordFinder("123456"));
// Install JCE Unlimited Strength Jurisdiction Policy Files if we get java.security.InvalidKeyException: Illegal key size
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
@@ -447,18 +505,8 @@ public class PuTTYKeyFileTest {
@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;
}
});
key.init(new StringReader(ppk1024_passphrase),
new UnitTestPasswordFinder("egfsdgdfgsdfsdfasfs523534dgdsgdfa"));
assertNotNull(key.getPublic());
assertNull(key.getPrivate());
}
@@ -466,18 +514,7 @@ public class PuTTYKeyFileTest {
@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;
}
});
key.init(new StringReader(ppkdsa_passphrase), new UnitTestPasswordFinder("secret"));
// Install JCE Unlimited Strength Jurisdiction Policy Files if we get java.security.InvalidKeyException: Illegal key size
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
@@ -486,18 +523,8 @@ public class PuTTYKeyFileTest {
@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;
}
});
key.init(new StringReader(ppkdsa_passphrase),
new UnitTestPasswordFinder("egfsdgdfgsdfsdfasfs523534dgdsgdfa"));
assertNotNull(key.getPublic());
assertNull(key.getPrivate());
}

View File

@@ -0,0 +1,42 @@
/*
* 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.util;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.Resource;
public class UnitTestPasswordFinder implements PasswordFinder {
private final char[] password;
public UnitTestPasswordFinder(String password) {
this.password = password.toCharArray();
}
public UnitTestPasswordFinder(char[] password) {
this.password = password;
}
@Override
public char[] reqPassword(Resource<?> resource) {
return password;
}
@Override
public boolean shouldRetry(Resource<?> resource) {
return false;
}
}

View File

@@ -0,0 +1,30 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz
c2gtcnNhAAAAAwEAAQAAAQEAgY1kBzKSgEJ1N72WRf4h5+EQPOuzsflNN/xjh4r1
tEoBiEG4SnEklr082DhPhkHbV/U9iica++s/5QeJ4IZHFpKS6EFvdYDQXz1GoDL2
gcSlwvHDamjJvfbsOSqUYPEqUyVscGD5ULc5ssWr9D0RXjjErIkbJ4zbl6aJTA0r
ur5pJLiqz874pz2W7F6GXjMm3JtYJ9mavu4pOLrNInGhsUSUbhfp1amGoBJjvY5k
KMrcqqDVs7z2BIf3YfOYREsLSCl4mjDo0IOdnVuQbhCzxle2cWikSu2Z8ERlugra
BmLpaNJic+SGbJQ60SvxCtm0hg265QHGGmo0TgemgQ8miQAAA9DskMes7JDHrAAA
AAdzc2gtcnNhAAABAQCBjWQHMpKAQnU3vZZF/iHn4RA867Ox+U03/GOHivW0SgGI
QbhKcSSWvTzYOE+GQdtX9T2KJxr76z/lB4nghkcWkpLoQW91gNBfPUagMvaBxKXC
8cNqaMm99uw5KpRg8SpTJWxwYPlQtzmyxav0PRFeOMSsiRsnjNuXpolMDSu6vmkk
uKrPzvinPZbsXoZeMybcm1gn2Zq+7ik4us0icaGxRJRuF+nVqYagEmO9jmQoytyq
oNWzvPYEh/dh85hESwtIKXiaMOjQg52dW5BuELPGV7ZxaKRK7ZnwRGW6CtoGYulo
0mJz5IZslDrRK/EK2bSGDbrlAcYaajROB6aBDyaJAAAAAwEAAQAAAQBJt9bvcYuD
iE2DBlJ4SX+pnpvKzqRV5XJXJTrNafkeOe5dRmhDk9YqIEx7DK/Tya2yg04dSttD
9j1Jady+8imJYqZNms59omrvhsKlbdpvRSK9pyx3ZGFHwzXv4ZbFAvX+khD+cW/s
yhX+8BREymsTnmHre6kD/FcIGC+QIv57J/snTHLIWRYMvHssgEBAvtjyTmXXurzr
9g92uAp/RwpIIiWB695lQbWWv9jwTFl9MinFu0ckRjp8djwvoK54vyWIvzxAOH7X
D8t3qmEAuekrf4uo6np3CqkHOU/5SAEs+o0KZ729/pS4fs8vWLi8qRXmRxbPrcvq
+uTTEjeQw7gBAAAAgQCtka9gYjdGNVz61o5wQWzHyMsszUZBlZwrXEbOnLYuz9A0
dwgb9yRuZFbDpGja5eLMjECua+ZvXu4XLk16SOr/y70Q4WRpv9I/Pk/QTDWepcs9
jmjp4IikYwBNBPZQPhnbLJ/9wrALOy0dFOuG76j8/jhoxDCGnxKbxNursWaxUQAA
AIEA/zvWbAkmleL4Xc70PtlsUGYkIitT8zb1FYj2L/neW63iSdn+9dPjJ8jCyIhK
R11jwlyKoyS765cZtZJCfSf5RkoF7p7I7m3dSXZ2+3ccS9Kv1VvE+mBIxLpySXPh
C9rmILzDHXU4VfhQ08Cp4Fd87wZC0Z1PlOeWWS/k9Vqcn0EAAACBAIHw9Z3xfqG3
tEuWima0iLbEZhNRjvxqbyCBLG2MoA8EwQN1lMNzpYKP4VxTdkuS8xF2xhDzr8xT
Ap0Lb8Q/gr2bZB2a8AsxeuYSlon1svVUDy+IGeD9JLx79lyevwAjcZ8lx6fOyC3E
/vMXJ14gHWfzamKvRGLy6UZ4A+t6431JAAAAEHJzYS1rZXktMjAyMTA5MjYBAgME
BQYHCAkK
-----END OPENSSH PRIVATE KEY-----