11
11
using Renci . SshNet . Security . Cryptography . Ciphers . Modes ;
12
12
using Renci . SshNet . Security . Cryptography . Ciphers . Paddings ;
13
13
using System . Diagnostics . CodeAnalysis ;
14
+ using Renci . SshNet . Security . Cryptography ;
14
15
15
16
namespace Renci . SshNet
16
17
{
@@ -25,13 +26,16 @@ namespace Renci.SshNet
25
26
/// The following private keys are supported:
26
27
/// <list type="bullet">
27
28
/// <item>
28
- /// <description>RSA in OpenSSH and ssh.com format</description>
29
+ /// <description>RSA in OpenSSL PEM and ssh.com format</description>
29
30
/// </item>
30
31
/// <item>
31
- /// <description>DSA in OpenSSH and ssh.com format</description>
32
+ /// <description>DSA in OpenSSL PEM and ssh.com format</description>
32
33
/// </item>
33
34
/// <item>
34
- /// <description>ECDSA 256/384/521 in OpenSSH format</description>
35
+ /// <description>ECDSA 256/384/521 in OpenSSL PEM format</description>
36
+ /// </item>
37
+ /// <item>
38
+ /// <description>ED25519 in OpenSSH key format</description>
35
39
/// </item>
36
40
/// </list>
37
41
/// </para>
@@ -214,6 +218,10 @@ private void Open(Stream privateKey, string passPhrase)
214
218
HostKey = new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ;
215
219
break ;
216
220
#endif
221
+ case "OPENSSH" :
222
+ _key = ParseOpenSshV1Key ( decryptedData , passPhrase ) ;
223
+ HostKey = new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ;
224
+ break ;
217
225
case "SSH2 ENCRYPTED" :
218
226
var reader = new SshDataReader ( decryptedData ) ;
219
227
var magicNumber = reader . ReadUInt32 ( ) ;
@@ -358,6 +366,144 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
358
366
return cipher . Decrypt ( cipherData ) ;
359
367
}
360
368
369
+ /// <summary>
370
+ /// Parses an OpenSSH V1 key file (i.e. ED25519 key) according to the the key spec:
371
+ /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
372
+ /// </summary>
373
+ /// <param name="keyFileData">the key file data (i.e. base64 encoded data between the header/footer)</param>
374
+ /// <param name="passPhrase">passphrase or null if there isn't one</param>
375
+ /// <returns></returns>
376
+ private ED25519Key ParseOpenSshV1Key ( byte [ ] keyFileData , string passPhrase )
377
+ {
378
+ var keyReader = new SshDataReader ( keyFileData ) ;
379
+
380
+ //check magic header
381
+ var authMagic = Encoding . UTF8 . GetBytes ( "openssh-key-v1\0 " ) ;
382
+ var keyHeaderBytes = keyReader . ReadBytes ( authMagic . Length ) ;
383
+ if ( ! authMagic . IsEqualTo ( keyHeaderBytes ) )
384
+ {
385
+ throw new SshException ( "This openssh key does not contain the 'openssh-key-v1' format magic header" ) ;
386
+ }
387
+
388
+ //cipher will be "aes256-cbc" if using a passphrase, "none" otherwise
389
+ var cipherName = keyReader . ReadString ( Encoding . UTF8 ) ;
390
+ //key derivation function (kdf): bcrypt or nothing
391
+ var kdfName = keyReader . ReadString ( Encoding . UTF8 ) ;
392
+ //kdf options length: 24 if passphrase, 0 if no passphrase
393
+ var kdfOptionsLen = ( int ) keyReader . ReadUInt32 ( ) ;
394
+ byte [ ] salt = null ;
395
+ int rounds = 0 ;
396
+ if ( kdfOptionsLen > 0 )
397
+ {
398
+ var saltLength = ( int ) keyReader . ReadUInt32 ( ) ;
399
+ salt = keyReader . ReadBytes ( saltLength ) ;
400
+ rounds = ( int ) keyReader . ReadUInt32 ( ) ;
401
+ }
402
+
403
+ //number of public keys, only supporting 1 for now
404
+ var numberOfPublicKeys = ( int ) keyReader . ReadUInt32 ( ) ;
405
+ if ( numberOfPublicKeys != 1 )
406
+ {
407
+ throw new SshException ( "At this time only one public key in the openssh key is supported." ) ;
408
+ }
409
+
410
+ //length of first public key section
411
+ keyReader . ReadUInt32 ( ) ;
412
+ var keyType = keyReader . ReadString ( Encoding . UTF8 ) ;
413
+ if ( keyType != "ssh-ed25519" )
414
+ {
415
+ throw new SshException ( "openssh key type: " + keyType + " is not supported" ) ;
416
+ }
417
+
418
+ //read public key
419
+ var publicKeyLength = ( int ) keyReader . ReadUInt32 ( ) ; //32
420
+ var publicKey = keyReader . ReadBytes ( publicKeyLength ) ;
421
+
422
+ //possibly encrypted private key
423
+ var privateKeyLength = ( int ) keyReader . ReadUInt32 ( ) ;
424
+ var privateKeyBytes = keyReader . ReadBytes ( privateKeyLength ) ;
425
+
426
+ //decrypt private key if necessary
427
+ if ( cipherName == "aes256-cbc" )
428
+ {
429
+ if ( string . IsNullOrEmpty ( passPhrase ) )
430
+ {
431
+ throw new SshPassPhraseNullOrEmptyException ( "Private key is encrypted but passphrase is empty." ) ;
432
+ }
433
+ if ( string . IsNullOrEmpty ( kdfName ) || kdfName != "bcrypt" )
434
+ {
435
+ throw new SshException ( "kdf " + kdfName + " is not supported for openssh key file" ) ;
436
+ }
437
+
438
+ //inspired by the SSHj library (https://github.com/hierynomus/sshj)
439
+ //apply the kdf to derive a key and iv from the passphrase
440
+ var passPhraseBytes = Encoding . UTF8 . GetBytes ( passPhrase ) ;
441
+ byte [ ] keyiv = new byte [ 48 ] ;
442
+ new BCrypt ( ) . Pbkdf ( passPhraseBytes , salt , rounds , keyiv ) ;
443
+ byte [ ] key = new byte [ 32 ] ;
444
+ Array . Copy ( keyiv , 0 , key , 0 , 32 ) ;
445
+ byte [ ] iv = new byte [ 16 ] ;
446
+ Array . Copy ( keyiv , 32 , iv , 0 , 16 ) ;
447
+
448
+ //now that we have the key/iv, use a cipher to decrypt the bytes
449
+ var cipher = new AesCipher ( key , new CbcCipherMode ( iv ) , new PKCS7Padding ( ) ) ;
450
+ privateKeyBytes = cipher . Decrypt ( privateKeyBytes ) ;
451
+ }
452
+ else if ( cipherName != "none" )
453
+ {
454
+ throw new SshException ( "cipher name " + cipherName + " for openssh key file is not supported" ) ;
455
+ }
456
+
457
+ //validate private key length
458
+ privateKeyLength = privateKeyBytes . Length ;
459
+ if ( privateKeyLength % 8 != 0 )
460
+ {
461
+ throw new SshException ( "The private key section must be a multiple of the block size (8)" ) ;
462
+ }
463
+
464
+ //now parse the data we called the private key, it actually contains the public key again
465
+ //so we need to parse through it to get the private key bytes, plus there's some
466
+ //validation we need to do.
467
+ var privateKeyReader = new SshDataReader ( privateKeyBytes ) ;
468
+
469
+ //check ints should match, they wouldn't match for example if the wrong passphrase was supplied
470
+ int checkInt1 = ( int ) privateKeyReader . ReadUInt32 ( ) ;
471
+ int checkInt2 = ( int ) privateKeyReader . ReadUInt32 ( ) ;
472
+ if ( checkInt1 != checkInt2 )
473
+ {
474
+ throw new SshException ( "The checkints differed, the openssh key was not correctly decoded." ) ;
475
+ }
476
+
477
+ //key type, we already know it is ssh-ed25519
478
+ privateKeyReader . ReadString ( Encoding . UTF8 ) ;
479
+
480
+ //public key length/bytes (again)
481
+ var publicKeyLength2 = ( int ) privateKeyReader . ReadUInt32 ( ) ;
482
+ privateKeyReader . ReadBytes ( publicKeyLength2 ) ;
483
+
484
+ //length of private and public key (64)
485
+ privateKeyReader . ReadUInt32 ( ) ;
486
+ var unencryptedPrivateKey = privateKeyReader . ReadBytes ( 32 ) ;
487
+ //public key (again)
488
+ privateKeyReader . ReadBytes ( 32 ) ;
489
+
490
+ //comment, we don't need this but we could log it, not sure if necessary
491
+ var comment = privateKeyReader . ReadString ( Encoding . UTF8 ) ;
492
+
493
+ //The list of privatekey/comment pairs is padded with the bytes 1, 2, 3, ...
494
+ //until the total length is a multiple of the cipher block size.
495
+ var padding = privateKeyReader . ReadBytes ( ) ;
496
+ for ( int i = 0 ; i < padding . Length ; i ++ )
497
+ {
498
+ if ( ( int ) padding [ i ] != i + 1 )
499
+ {
500
+ throw new SshException ( "Padding of openssh key format contained wrong byte at position: " + i ) ;
501
+ }
502
+ }
503
+
504
+ return new ED25519Key ( publicKey . Reverse ( ) , unencryptedPrivateKey ) ;
505
+ }
506
+
361
507
#region IDisposable Members
362
508
363
509
private bool _isDisposed ;
@@ -426,6 +572,11 @@ public SshDataReader(byte[] data)
426
572
return base . ReadBytes ( length ) ;
427
573
}
428
574
575
+ public new byte [ ] ReadBytes ( )
576
+ {
577
+ return base . ReadBytes ( ) ;
578
+ }
579
+
429
580
/// <summary>
430
581
/// Reads next mpint data type from internal buffer where length specified in bits.
431
582
/// </summary>
0 commit comments