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