Skip to content

Commit e390394

Browse files
Refactored PuTTY Secret Key Derivation (#1003)
- Added KeyDerivationFunction interface for PuTTY Key Files - Moved Argon2 Key Derivation to Version 3 implementation class to separate Bouncy Castle dependency references - Replaced Bouncy Castle Hex references with ByteArrayUtils Co-authored-by: Jeroen van Erp <[email protected]>
1 parent 995de2d commit e390394

File tree

6 files changed

+247
-96
lines changed

6 files changed

+247
-96
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package net.schmizz.sshj.userauth.keyprovider;
1717

1818
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
19+
import net.schmizz.sshj.common.ByteArrayUtils;
1920
import net.schmizz.sshj.userauth.password.PasswordFinder;
2021
import net.schmizz.sshj.userauth.password.PasswordUtils;
2122
import net.schmizz.sshj.userauth.password.Resource;
@@ -24,7 +25,6 @@
2425
import org.bouncycastle.openssl.PEMException;
2526
import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider;
2627
import org.bouncycastle.operator.OperatorCreationException;
27-
import org.bouncycastle.util.encoders.Hex;
2828

2929
import java.io.BufferedReader;
3030
import java.io.IOException;
@@ -124,7 +124,7 @@ private DataEncryptionKeyInfo getDataEncryptionKeyInfo(final List<String> header
124124
if (matcher.matches()) {
125125
final String algorithm = matcher.group(DEK_INFO_ALGORITHM_GROUP);
126126
final String initializationVectorGroup = matcher.group(DEK_INFO_IV_GROUP);
127-
final byte[] initializationVector = Hex.decode(initializationVectorGroup);
127+
final byte[] initializationVector = ByteArrayUtils.parseHex(initializationVectorGroup);
128128
dataEncryptionKeyInfo = new DataEncryptionKeyInfo(algorithm, initializationVector);
129129
}
130130
}

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

Lines changed: 38 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@
1818
import com.hierynomus.sshj.common.KeyAlgorithm;
1919
import net.schmizz.sshj.common.*;
2020
import net.schmizz.sshj.userauth.password.PasswordUtils;
21-
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
22-
import org.bouncycastle.crypto.params.Argon2Parameters;
23-
import org.bouncycastle.util.encoders.Hex;
2421

2522
import javax.crypto.Cipher;
2623
import javax.crypto.Mac;
24+
import javax.crypto.SecretKey;
2725
import javax.crypto.spec.IvParameterSpec;
2826
import javax.crypto.spec.SecretKeySpec;
2927
import java.io.*;
@@ -75,6 +73,8 @@ public String getName() {
7573
}
7674
}
7775

76+
private static final String KEY_DERIVATION_HEADER = "Key-Derivation";
77+
7878
private Integer keyFileVersion;
7979
private byte[] privateKey;
8080
private byte[] publicKey;
@@ -101,12 +101,12 @@ public boolean isEncrypted() throws IOException {
101101
throw new IOException(String.format("Unsupported encryption: %s", encryption));
102102
}
103103

104-
private final Map<String, String> payload = new HashMap<String, String>();
104+
private final Map<String, String> payload = new HashMap<>();
105105

106106
/**
107107
* For each line that looks like "Xyz: vvv", it will be stored in this map.
108108
*/
109-
private final Map<String, String> headers = new HashMap<String, String>();
109+
private final Map<String, String> headers = new HashMap<>();
110110

111111
protected KeyPair readKeyPair() throws IOException {
112112
this.parseKeyPair();
@@ -261,99 +261,43 @@ protected void parseKeyPair() throws IOException {
261261
}
262262

263263
/**
264-
* Converts a passphrase into a key, by following the convention that PuTTY
265-
* uses. Only PuTTY v1/v2 key files
266-
* <p><p/>
267-
* This is used to decrypt the private key when it's encrypted.
264+
* Initialize Java Cipher for decryption using Secret Key derived from passphrase according to PuTTY Key Version
268265
*/
269-
private void initCipher(final char[] passphrase, Cipher cipher) throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
270-
// The field Key-Derivation has been introduced with Putty v3 key file format
271-
// For v3 the algorithms are "Argon2i" "Argon2d" and "Argon2id"
272-
String kdfAlgorithm = headers.get("Key-Derivation");
273-
if (kdfAlgorithm != null) {
274-
kdfAlgorithm = kdfAlgorithm.toLowerCase();
275-
byte[] keyData = this.argon2(kdfAlgorithm, passphrase);
276-
if (keyData == null) {
277-
throw new IOException(String.format("Unsupported key derivation function: %s", kdfAlgorithm));
278-
}
279-
byte[] key = new byte[32];
280-
byte[] iv = new byte[16];
281-
byte[] tag = new byte[32]; // Hmac key
282-
System.arraycopy(keyData, 0, key, 0, 32);
283-
System.arraycopy(keyData, 32, iv, 0, 16);
284-
System.arraycopy(keyData, 48, tag, 0, 32);
285-
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
286-
new IvParameterSpec(iv));
287-
verifyHmac = tag;
288-
return;
289-
}
290-
291-
// Key file format v1 + v2
292-
try {
293-
MessageDigest digest = MessageDigest.getInstance("SHA-1");
294-
295-
// The encryption key is derived from the passphrase by means of a succession of
296-
// SHA-1 hashes.
297-
byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase);
298-
299-
// Sequence number 0
300-
digest.update(new byte[]{0, 0, 0, 0});
301-
digest.update(encodedPassphrase);
302-
byte[] key1 = digest.digest();
303-
304-
// Sequence number 1
305-
digest.update(new byte[]{0, 0, 0, 1});
306-
digest.update(encodedPassphrase);
307-
byte[] key2 = digest.digest();
308-
309-
Arrays.fill(encodedPassphrase, (byte) 0);
266+
private void initCipher(final char[] passphrase, final Cipher cipher) throws InvalidAlgorithmParameterException, InvalidKeyException {
267+
final String keyDerivationHeader = headers.get(KEY_DERIVATION_HEADER);
310268

311-
byte[] expanded = new byte[32];
312-
System.arraycopy(key1, 0, expanded, 0, 20);
313-
System.arraycopy(key2, 0, expanded, 20, 12);
269+
final SecretKey secretKey;
270+
final IvParameterSpec ivParameterSpec;
314271

315-
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"),
316-
new IvParameterSpec(new byte[16])); // initial vector=0
317-
318-
} catch (NoSuchAlgorithmException e) {
319-
throw new IOException(e.getMessage(), e);
320-
}
321-
}
322-
323-
/**
324-
* Uses BouncyCastle Argon2 implementation
325-
*/
326-
private byte[] argon2(String algorithm, final char[] passphrase) throws IOException {
327-
int type;
328-
if ("argon2i".equals(algorithm)) {
329-
type = Argon2Parameters.ARGON2_i;
330-
} else if ("argon2d".equals(algorithm)) {
331-
type = Argon2Parameters.ARGON2_d;
332-
} else if ("argon2id".equals(algorithm)) {
333-
type = Argon2Parameters.ARGON2_id;
272+
if (keyDerivationHeader == null) {
273+
// Key Version 1 and 2 with historical key derivation
274+
final PuTTYSecretKeyDerivationFunction keyDerivationFunction = new V1PuTTYSecretKeyDerivationFunction();
275+
secretKey = keyDerivationFunction.deriveSecretKey(passphrase);
276+
ivParameterSpec = new IvParameterSpec(new byte[16]);
334277
} else {
335-
return null;
336-
}
337-
byte[] salt = Hex.decode(headers.get("Argon2-Salt"));
338-
int iterations = Integer.parseInt(headers.get("Argon2-Passes"));
339-
int memory = Integer.parseInt(headers.get("Argon2-Memory"));
340-
int parallelism = Integer.parseInt(headers.get("Argon2-Parallelism"));
341-
342-
Argon2Parameters a2p = new Argon2Parameters.Builder(type)
343-
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
344-
.withIterations(iterations)
345-
.withMemoryAsKB(memory)
346-
.withParallelism(parallelism)
347-
.withSalt(salt).build();
348-
349-
Argon2BytesGenerator generator = new Argon2BytesGenerator();
350-
generator.init(a2p);
351-
byte[] output = new byte[80];
352-
int bytes = generator.generateBytes(passphrase, output);
353-
if (bytes != output.length) {
354-
throw new IOException("Failed to generate key via Argon2");
278+
// Key Version 3 with Argon2 key derivation
279+
final PuTTYSecretKeyDerivationFunction keyDerivationFunction = new V3PuTTYSecretKeyDerivationFunction(headers);
280+
final SecretKey derivedSecretKey = keyDerivationFunction.deriveSecretKey(passphrase);
281+
final byte[] derivedSecretKeyEncoded = derivedSecretKey.getEncoded();
282+
283+
// Set Secret Key from first 32 bytes
284+
final byte[] secretKeyEncoded = new byte[32];
285+
System.arraycopy(derivedSecretKeyEncoded, 0, secretKeyEncoded, 0, secretKeyEncoded.length);
286+
secretKey = new SecretKeySpec(secretKeyEncoded, derivedSecretKey.getAlgorithm());
287+
288+
// Set IV from next 16 bytes
289+
final byte[] iv = new byte[16];
290+
System.arraycopy(derivedSecretKeyEncoded, secretKeyEncoded.length, iv, 0, iv.length);
291+
ivParameterSpec = new IvParameterSpec(iv);
292+
293+
// Set HMAC Tag from next 32 bytes
294+
final byte[] tag = new byte[32];
295+
final int tagSourcePosition = secretKeyEncoded.length + iv.length;
296+
System.arraycopy(derivedSecretKeyEncoded, tagSourcePosition, tag, 0, tag.length);
297+
verifyHmac = tag;
355298
}
356-
return output;
299+
300+
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
357301
}
358302

359303
/**
@@ -380,7 +324,7 @@ private void verify(final Mac mac) throws IOException {
380324
data.writeInt(privateKey.length);
381325
data.write(privateKey);
382326

383-
final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray()));
327+
final String encoded = ByteArrayUtils.toHex(mac.doFinal(out.toByteArray()));
384328
final String reference = headers.get("Private-MAC");
385329
if (!encoded.equals(reference)) {
386330
throw new IOException("Invalid passphrase");
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.schmizz.sshj.userauth.keyprovider;
17+
18+
import javax.crypto.SecretKey;
19+
20+
/**
21+
* Abstraction for deriving the Secret Key for decrypting PuTTY Key Files
22+
*/
23+
interface PuTTYSecretKeyDerivationFunction {
24+
/**
25+
* Derive Secret Key from provided passphrase characters
26+
*
27+
* @param passphrase Passphrase characters required
28+
* @return Derived Secret Key
29+
*/
30+
SecretKey deriveSecretKey(char[] passphrase);
31+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.schmizz.sshj.userauth.keyprovider;
17+
18+
import net.schmizz.sshj.common.SecurityUtils;
19+
import net.schmizz.sshj.userauth.password.PasswordUtils;
20+
21+
import javax.crypto.SecretKey;
22+
import javax.crypto.spec.SecretKeySpec;
23+
import java.security.MessageDigest;
24+
import java.security.NoSuchAlgorithmException;
25+
import java.security.NoSuchProviderException;
26+
import java.util.Arrays;
27+
import java.util.Objects;
28+
29+
/**
30+
* PuTTY Key Derivation Function supporting Version 1 and 2 Key files with historical SHA-1 key derivation
31+
*/
32+
class V1PuTTYSecretKeyDerivationFunction implements PuTTYSecretKeyDerivationFunction {
33+
private static final String SECRET_KEY_ALGORITHM = "AES";
34+
35+
private static final String DIGEST_ALGORITHM = "SHA-1";
36+
37+
/**
38+
* Derive Secret Key from provided passphrase characters
39+
*
40+
* @param passphrase Passphrase characters required
41+
* @return Derived Secret Key
42+
*/
43+
public SecretKey deriveSecretKey(char[] passphrase) {
44+
Objects.requireNonNull(passphrase, "Passphrase required");
45+
46+
final MessageDigest digest = getMessageDigest();
47+
final byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase);
48+
49+
// Sequence number 0
50+
digest.update(new byte[]{0, 0, 0, 0});
51+
digest.update(encodedPassphrase);
52+
final byte[] key1 = digest.digest();
53+
54+
// Sequence number 1
55+
digest.update(new byte[]{0, 0, 0, 1});
56+
digest.update(encodedPassphrase);
57+
final byte[] key2 = digest.digest();
58+
59+
Arrays.fill(encodedPassphrase, (byte) 0);
60+
61+
final byte[] secretKeyEncoded = new byte[32];
62+
System.arraycopy(key1, 0, secretKeyEncoded, 0, 20);
63+
System.arraycopy(key2, 0, secretKeyEncoded, 20, 12);
64+
65+
return new SecretKeySpec(secretKeyEncoded, SECRET_KEY_ALGORITHM);
66+
}
67+
68+
private MessageDigest getMessageDigest() {
69+
try {
70+
return SecurityUtils.getMessageDigest(DIGEST_ALGORITHM);
71+
} catch (final NoSuchAlgorithmException | NoSuchProviderException e) {
72+
final String message = String.format("Message Digest Algorithm [%s] not supported", DIGEST_ALGORITHM);
73+
throw new IllegalStateException(message, e);
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)