18
18
import com .hierynomus .sshj .common .KeyAlgorithm ;
19
19
import net .schmizz .sshj .common .*;
20
20
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 ;
24
21
25
22
import javax .crypto .Cipher ;
26
23
import javax .crypto .Mac ;
24
+ import javax .crypto .SecretKey ;
27
25
import javax .crypto .spec .IvParameterSpec ;
28
26
import javax .crypto .spec .SecretKeySpec ;
29
27
import java .io .*;
@@ -75,6 +73,8 @@ public String getName() {
75
73
}
76
74
}
77
75
76
+ private static final String KEY_DERIVATION_HEADER = "Key-Derivation" ;
77
+
78
78
private Integer keyFileVersion ;
79
79
private byte [] privateKey ;
80
80
private byte [] publicKey ;
@@ -101,12 +101,12 @@ public boolean isEncrypted() throws IOException {
101
101
throw new IOException (String .format ("Unsupported encryption: %s" , encryption ));
102
102
}
103
103
104
- private final Map <String , String > payload = new HashMap <String , String >();
104
+ private final Map <String , String > payload = new HashMap <>();
105
105
106
106
/**
107
107
* For each line that looks like "Xyz: vvv", it will be stored in this map.
108
108
*/
109
- private final Map <String , String > headers = new HashMap <String , String >();
109
+ private final Map <String , String > headers = new HashMap <>();
110
110
111
111
protected KeyPair readKeyPair () throws IOException {
112
112
this .parseKeyPair ();
@@ -261,99 +261,43 @@ protected void parseKeyPair() throws IOException {
261
261
}
262
262
263
263
/**
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
268
265
*/
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 );
310
268
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 ;
314
271
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 ]);
334
277
} 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 ;
355
298
}
356
- return output ;
299
+
300
+ cipher .init (Cipher .DECRYPT_MODE , secretKey , ivParameterSpec );
357
301
}
358
302
359
303
/**
@@ -380,7 +324,7 @@ private void verify(final Mac mac) throws IOException {
380
324
data .writeInt (privateKey .length );
381
325
data .write (privateKey );
382
326
383
- final String encoded = Hex . toHexString (mac .doFinal (out .toByteArray ()));
327
+ final String encoded = ByteArrayUtils . toHex (mac .doFinal (out .toByteArray ()));
384
328
final String reference = headers .get ("Private-MAC" );
385
329
if (!encoded .equals (reference )) {
386
330
throw new IOException ("Invalid passphrase" );
0 commit comments