Skip to content

Commit b067e75

Browse files
authored
Support more ciphers for OpenSSH private key decryption. (#1487)
1 parent 6669309 commit b067e75

26 files changed

+375
-75
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,26 @@ The main types provided by this library are:
101101
* ECDSA 256/384/521 in OpenSSL PEM format ("BEGIN EC PRIVATE KEY")
102102
* ECDSA 256/384/521, ED25519 and RSA in OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
103103

104-
Private keys can be encrypted using one of the following cipher methods:
104+
Private keys in OpenSSL PEM and ssh.com format can be encrypted using one of the following cipher methods:
105105
* DES-EDE3-CBC
106106
* DES-EDE3-CFB
107107
* DES-CBC
108108
* AES-128-CBC
109109
* AES-192-CBC
110110
* AES-256-CBC
111111

112+
Private keys in OpenSSH key format can be encrypted using one of the following cipher methods:
113+
* 3des-cbc
114+
* aes128-cbc
115+
* aes192-cbc
116+
* aes256-cbc
117+
* aes128-ctr
118+
* aes192-ctr
119+
* aes256-ctr
120+
* aes128-gcm<span></span>@openssh.com
121+
* aes256-gcm<span></span>@openssh.com
122+
* chacha20-poly1305<span></span>@openssh.com
123+
112124
## Host Key Algorithms
113125

114126
**SSH.NET** supports the following host key algorithms:

src/Renci.SshNet/ConnectionInfo.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,9 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
386386
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
387387
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
388388
{ "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
389-
{ "[email protected]", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
390-
{ "[email protected]", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
391-
{ "[email protected]", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true) },
389+
{ "[email protected]", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv, aadLength: 4), isAead: true) },
390+
{ "[email protected]", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv, aadLength: 4), isAead: true) },
391+
{ "[email protected]", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key, aadLength: 4), isAead: true) },
392392
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
393393
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
394394
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },

src/Renci.SshNet/PrivateKeyFile.cs

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ namespace Renci.SshNet
3939
/// </list>
4040
/// </para>
4141
/// <para>
42-
/// The following encryption algorithms are supported:
42+
/// The following encryption algorithms are supported for OpenSSL PEM and ssh.com format:
4343
/// <list type="bullet">
4444
/// <item>
4545
/// <description>DES-EDE3-CBC</description>
@@ -60,6 +60,39 @@ namespace Renci.SshNet
6060
/// <description>AES-256-CBC</description>
6161
/// </item>
6262
/// </list>
63+
/// The following encryption algorithms are supported for OpenSSH format:
64+
/// <list type="bullet">
65+
/// <item>
66+
/// <description>3des-cbc</description>
67+
/// </item>
68+
/// <item>
69+
/// <description>aes128-cbc</description>
70+
/// </item>
71+
/// <item>
72+
/// <description>aes192-cbc</description>
73+
/// </item>
74+
/// <item>
75+
/// <description>aes256-cbc</description>
76+
/// </item>
77+
/// <item>
78+
/// <description>aes128-ctr</description>
79+
/// </item>
80+
/// <item>
81+
/// <description>aes192-ctr</description>
82+
/// </item>
83+
/// <item>
84+
/// <description>aes256-ctr</description>
85+
/// </item>
86+
/// <item>
87+
/// <description>[email protected]</description>
88+
/// </item>
89+
/// <item>
90+
/// <description>[email protected]</description>
91+
/// </item>
92+
/// <item>
93+
/// <description>[email protected]</description>
94+
/// </item>
95+
/// </list>
6396
/// </para>
6497
/// </remarks>
6598
public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
@@ -450,7 +483,17 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
450483

451484
var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
452485

453-
return cipher.Decrypt(cipherData);
486+
try
487+
{
488+
return cipher.Decrypt(cipherData);
489+
}
490+
finally
491+
{
492+
if (cipher is IDisposable disposable)
493+
{
494+
disposable.Dispose();
495+
}
496+
}
454497
}
455498

456499
/// <summary>
@@ -474,7 +517,7 @@ private static Key ParseOpenSshV1Key(byte[] keyFileData, string passPhrase)
474517
throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
475518
}
476519

477-
// cipher will be "aes256-cbc" if using a passphrase, "none" otherwise
520+
// cipher will be "aes256-cbc" or other cipher if using a passphrase, "none" otherwise
478521
var cipherName = keyReader.ReadString(Encoding.UTF8);
479522

480523
// key derivation function (kdf): bcrypt or nothing
@@ -503,7 +546,7 @@ private static Key ParseOpenSshV1Key(byte[] keyFileData, string passPhrase)
503546

504547
// possibly encrypted private key
505548
var privateKeyLength = (int)keyReader.ReadUInt32();
506-
var privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
549+
byte[] privateKeyBytes;
507550

508551
// decrypt private key if necessary
509552
if (cipherName != "none")
@@ -518,38 +561,76 @@ private static Key ParseOpenSshV1Key(byte[] keyFileData, string passPhrase)
518561
throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
519562
}
520563

521-
// inspired by the SSHj library (https://github.com/hierynomus/sshj)
522-
// apply the kdf to derive a key and iv from the passphrase
523-
var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
524-
var keyiv = new byte[48];
525-
new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
526-
var key = new byte[32];
527-
Array.Copy(keyiv, 0, key, 0, 32);
528-
var iv = new byte[16];
529-
Array.Copy(keyiv, 32, iv, 0, 16);
530-
531-
AesCipher cipher;
564+
var ivLength = 16;
565+
CipherInfo cipherInfo;
532566
switch (cipherName)
533567
{
568+
case "3des-cbc":
569+
ivLength = 8;
570+
cipherInfo = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null));
571+
break;
572+
case "aes128-cbc":
573+
cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
574+
break;
575+
case "aes192-cbc":
576+
cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
577+
break;
534578
case "aes256-cbc":
535-
cipher = new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false);
579+
cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
580+
break;
581+
case "aes128-ctr":
582+
cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
583+
break;
584+
case "aes192-ctr":
585+
cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
536586
break;
537587
case "aes256-ctr":
538-
cipher = new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false);
588+
cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
589+
break;
590+
591+
cipherInfo = new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
592+
break;
593+
594+
cipherInfo = new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
595+
break;
596+
597+
ivLength = 12;
598+
cipherInfo = new CipherInfo(256, (key, iv) => new ChaCha20Poly1305Cipher(key, aadLength: 0), isAead: true);
539599
break;
540600
default:
541601
throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
542602
}
543603

604+
var keyLength = cipherInfo.KeySize / 8;
605+
606+
// inspired by the SSHj library (https://github.com/hierynomus/sshj)
607+
// apply the kdf to derive a key and iv from the passphrase
608+
var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
609+
var keyiv = new byte[keyLength + ivLength];
610+
new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
611+
612+
var key = keyiv.Take(keyLength);
613+
var iv = keyiv.Take(keyLength, ivLength);
614+
615+
var cipher = cipherInfo.Cipher(key, iv);
616+
var cipherData = keyReader.ReadBytes(privateKeyLength + cipher.TagSize);
617+
544618
try
545619
{
546-
privateKeyBytes = cipher.Decrypt(privateKeyBytes);
620+
privateKeyBytes = cipher.Decrypt(cipherData, 0, privateKeyLength);
547621
}
548622
finally
549623
{
550-
cipher.Dispose();
624+
if (cipher is IDisposable disposable)
625+
{
626+
disposable.Dispose();
627+
}
551628
}
552629
}
630+
else
631+
{
632+
privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
633+
}
553634

554635
// validate private key length
555636
privateKeyLength = privateKeyBytes.Length;

src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
1212
/// </summary>
1313
internal sealed partial class AesGcmCipher : SymmetricCipher, IDisposable
1414
{
15-
private const int PacketLengthFieldLength = 4;
1615
private const int TagSizeInBytes = 16;
1716
private readonly byte[] _iv;
17+
private readonly int _aadLength;
1818
#if NET6_0_OR_GREATER
1919
private readonly Impl _impl;
2020
#else
@@ -55,11 +55,13 @@ public override int TagSize
5555
/// </summary>
5656
/// <param name="key">The key.</param>
5757
/// <param name="iv">The IV.</param>
58-
public AesGcmCipher(byte[] key, byte[] iv)
58+
/// <param name="aadLength">The length of additional associated data.</param>
59+
public AesGcmCipher(byte[] key, byte[] iv, int aadLength)
5960
: base(key)
6061
{
6162
// SSH AES-GCM requires a 12-octet Initial IV
6263
_iv = iv.Take(12);
64+
_aadLength = aadLength;
6365
#if NET6_0_OR_GREATER
6466
if (System.Security.Cryptography.AesGcm.IsSupported)
6567
{
@@ -78,32 +80,30 @@ public AesGcmCipher(byte[] key, byte[] iv)
7880
/// <param name="input">
7981
/// The input data with below format:
8082
/// <code>
81-
/// [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
82-
/// [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
83+
/// [----(offset)][----AAD----][----Plain Text----(length)]
8384
/// </code>
8485
/// </param>
8586
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
8687
/// <param name="length">The number of bytes to encrypt from <paramref name="input"/>.</param>
8788
/// <returns>
8889
/// The encrypted data with below format:
8990
/// <code>
90-
/// [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
91-
/// [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
91+
/// [----AAD----][----Cipher Text----][----TAG----]
9292
/// </code>
9393
/// </returns>
9494
public override byte[] Encrypt(byte[] input, int offset, int length)
9595
{
9696
var output = new byte[length + TagSize];
97-
Buffer.BlockCopy(input, offset, output, 0, PacketLengthFieldLength);
97+
Buffer.BlockCopy(input, offset, output, 0, _aadLength);
9898

9999
_impl.Encrypt(
100100
input,
101-
plainTextOffset: offset + PacketLengthFieldLength,
102-
plainTextLength: length - PacketLengthFieldLength,
101+
plainTextOffset: offset + _aadLength,
102+
plainTextLength: length - _aadLength,
103103
associatedDataOffset: offset,
104-
associatedDataLength: PacketLengthFieldLength,
104+
associatedDataLength: _aadLength,
105105
output,
106-
cipherTextOffset: PacketLengthFieldLength);
106+
cipherTextOffset: _aadLength);
107107

108108
IncrementCounter();
109109

@@ -116,31 +116,29 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
116116
/// <param name="input">
117117
/// The input data with below format:
118118
/// <code>
119-
/// [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
120-
/// [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
119+
/// [----][----AAD----(offset)][----Cipher Text----(length)][----TAG----]
121120
/// </code>
122121
/// </param>
123122
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting and authenticating.</param>
124123
/// <param name="length">The number of bytes to decrypt and authenticate from <paramref name="input"/>.</param>
125124
/// <returns>
126125
/// The decrypted data with below format:
127126
/// <code>
128-
/// [padding length field sz][payload][random paddings]
129-
/// [--------------------Plain Text-------------------]
127+
/// [----Plain Text----]
130128
/// </code>
131129
/// </returns>
132130
public override byte[] Decrypt(byte[] input, int offset, int length)
133131
{
134-
Debug.Assert(offset == 8, "The offset must be 8");
132+
Debug.Assert(offset >= _aadLength, "The offset must be greater than or equals to aad length");
135133

136134
var output = new byte[length];
137135

138136
_impl.Decrypt(
139137
input,
140138
cipherTextOffset: offset,
141139
cipherTextLength: length,
142-
associatedDataOffset: offset - PacketLengthFieldLength,
143-
associatedDataLength: PacketLengthFieldLength,
140+
associatedDataOffset: offset - _aadLength,
141+
associatedDataLength: _aadLength,
144142
output,
145143
plainTextOffset: 0);
146144

0 commit comments

Comments
 (0)