1111using Renci . SshNet . Security . Cryptography . Ciphers . Modes ;
1212using Renci . SshNet . Security . Cryptography . Ciphers . Paddings ;
1313using System . Diagnostics . CodeAnalysis ;
14+ using System . Linq ;
15+ using Renci . SshNet . Security . Cryptography ;
1416
1517namespace Renci . SshNet
1618{
@@ -23,6 +25,7 @@ namespace Renci.SshNet
2325 /// <remarks>
2426 /// <para>
2527 /// 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.
2629 /// </para>
2730 /// <para>
2831 /// The following encryption algorithms are supported:
@@ -203,6 +206,10 @@ private void Open(Stream privateKey, string passPhrase)
203206 HostKey = new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ;
204207 break ;
205208#endif
209+ case "OPENSSH" :
210+ _key = ParseOpenSshV1Key ( decryptedData , passPhrase ) ;
211+ HostKey = new KeyHostAlgorithm ( _key . ToString ( ) , _key ) ;
212+ break ;
206213 case "SSH2 ENCRYPTED" :
207214 var reader = new SshDataReader ( decryptedData ) ;
208215 var magicNumber = reader . ReadUInt32 ( ) ;
@@ -347,7 +354,145 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
347354 return cipher . Decrypt ( cipherData ) ;
348355 }
349356
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
351496
352497 private bool _isDisposed ;
353498
@@ -415,6 +560,11 @@ public SshDataReader(byte[] data)
415560 return base . ReadBytes ( length ) ;
416561 }
417562
563+ public new byte [ ] ReadBytes ( )
564+ {
565+ return base . ReadBytes ( ) ;
566+ }
567+
418568 /// <summary>
419569 /// Reads next mpint data type from internal buffer where length specified in bits.
420570 /// </summary>
0 commit comments