mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 07:10:53 +03:00
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:
@@ -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(new byte[]{0, 0, 0, 0});
|
||||
digest.update(encodedPassphrase);
|
||||
byte[] key1 = digest.digest();
|
||||
|
||||
// Sequence number 1
|
||||
digest.update(new byte[] { 0, 0, 0, 1 });
|
||||
digest.update(passphrase.getBytes());
|
||||
digest.update(new byte[]{0, 0, 0, 1});
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
30
src/test/resources/keytypes/test_rsa_putty_priv.openssh2
Normal file
30
src/test/resources/keytypes/test_rsa_putty_priv.openssh2
Normal 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-----
|
||||
Reference in New Issue
Block a user