1818import com .hierynomus .sshj .common .KeyAlgorithm ;
1919import net .schmizz .sshj .common .*;
2020import 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
2522import javax .crypto .Cipher ;
2623import javax .crypto .Mac ;
24+ import javax .crypto .SecretKey ;
2725import javax .crypto .spec .IvParameterSpec ;
2826import javax .crypto .spec .SecretKeySpec ;
2927import 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" );
0 commit comments