1111using Renci . SshNet . Security . Cryptography . Ciphers . Modes ;
1212using Renci . SshNet . Security . Cryptography . Ciphers . Paddings ;
1313using System . Diagnostics . CodeAnalysis ;
14+ using Renci . SshNet . Security . Cryptography ;
1415
1516namespace Renci . SshNet
1617{
@@ -25,13 +26,16 @@ namespace Renci.SshNet
2526 /// The following private keys are supported:
2627 /// <list type="bullet">
2728 /// <item>
28- /// <description>RSA in OpenSSH and ssh.com format</description>
29+ /// <description>RSA in OpenSSL PEM and ssh.com format</description>
2930 /// </item>
3031 /// <item>
31- /// <description>DSA in OpenSSH and ssh.com format</description>
32+ /// <description>DSA in OpenSSL PEM and ssh.com format</description>
3233 /// </item>
3334 /// <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>
3539 /// </item>
3640 /// </list>
3741 /// </para>
@@ -214,6 +218,10 @@ private void Open(Stream privateKey, string passPhrase)
214218 HostKey = new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ;
215219 break ;
216220#endif
221+ case "OPENSSH" :
222+ _key = ParseOpenSshV1Key ( decryptedData , passPhrase ) ;
223+ HostKey = new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ;
224+ break ;
217225 case "SSH2 ENCRYPTED" :
218226 var reader = new SshDataReader ( decryptedData ) ;
219227 var magicNumber = reader . ReadUInt32 ( ) ;
@@ -358,7 +366,145 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
358366 return cipher . Decrypt ( cipherData ) ;
359367 }
360368
361- #region IDisposable Members
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+
507+ #region IDisposable Members
362508
363509 private bool _isDisposed ;
364510
@@ -426,6 +572,11 @@ public SshDataReader(byte[] data)
426572 return base . ReadBytes ( length ) ;
427573 }
428574
575+ public new byte [ ] ReadBytes ( )
576+ {
577+ return base . ReadBytes ( ) ;
578+ }
579+
429580 /// <summary>
430581 /// Reads next mpint data type from internal buffer where length specified in bits.
431582 /// </summary>
0 commit comments