Skip to content

Commit 04c3cd2

Browse files
committed
Update xml doc comments and README file; some small tweaks
1 parent 286b05e commit 04c3cd2

File tree

3 files changed

+59
-49
lines changed

3 files changed

+59
-49
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,21 @@ The main types provided by this library are:
101101
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
102102
* ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY")
103103
* OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
104+
* PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
104105
* DSA in
105106
* OpenSSL traditional PEM format ("BEGIN DSA PRIVATE KEY")
106107
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
107108
* ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY")
109+
* PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
108110
* ECDSA 256/384/521 in
109111
* OpenSSL traditional PEM format ("BEGIN EC PRIVATE KEY")
110112
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
111113
* OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
114+
* PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
112115
* ED25519 in
113116
* OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
114117
* OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
118+
* PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
115119

116120
Private keys in OpenSSL traditional PEM format can be encrypted using one of the following cipher methods:
117121
* DES-EDE3-CBC
@@ -123,7 +127,7 @@ Private keys in OpenSSL traditional PEM format can be encrypted using one of the
123127

124128
Private keys in OpenSSL PKCS#8 PEM format can be encrypted using any cipher method BouncyCastle supports.
125129

126-
Private keys in ssh.com format can be encrypted using one of the following cipher methods:
130+
Private keys in ssh.com format can be encrypted using the following cipher method:
127131
* 3des-cbc
128132

129133
Private keys in OpenSSH key format can be encrypted using one of the following cipher methods:
@@ -138,6 +142,9 @@ Private keys in OpenSSH key format can be encrypted using one of the following c
138142
* aes256-gcm<span></span>@openssh.com
139143
* chacha20-poly1305<span></span>@openssh.com
140144

145+
Private keys in PuTTY private key format can be encrypted using the following cipher method:
146+
* aes256-cbc
147+
141148
## Host Key Algorithms
142149

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

src/Renci.SshNet/PrivateKeyFile.PuTTY.cs

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ public Key Parse()
9393
cipherKey = keyData.Take(32);
9494
cipherIV = keyData.Take(32, 16);
9595

96-
hmac = new HMACSHA256(keyData.Take(48, 32));
96+
var macKey = keyData.Take(48, 32);
97+
hmac = new HMACSHA256(macKey);
9798

9899
break;
99100
case "2":
@@ -102,14 +103,8 @@ public Key Parse()
102103
cipherKey = keyData.Take(32);
103104
cipherIV = new byte[16];
104105

105-
using (var sha1 = SHA1.Create())
106-
{
107-
var part1 = Encoding.UTF8.GetBytes("putty-private-key-file-mac-key");
108-
var part2 = Encoding.UTF8.GetBytes(_passPhrase);
109-
_ = sha1.TransformBlock(part1, 0, part1.Length, outputBuffer: null, 0);
110-
_ = sha1.TransformFinalBlock(part2, 0, part2.Length);
111-
hmac = new HMACSHA1(sha1.Hash.Take(20));
112-
}
106+
macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key" + _passPhrase)).Take(20);
107+
hmac = new HMACSHA1(macKey);
113108

114109
break;
115110
default:
@@ -129,8 +124,8 @@ public Key Parse()
129124
hmac = new HMACSHA256(Array.Empty<byte>());
130125
break;
131126
case "2":
132-
var hash = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key"));
133-
hmac = new HMACSHA1(hash);
127+
var macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key"));
128+
hmac = new HMACSHA1(macKey);
134129
break;
135130
default:
136131
throw new SshException("PuTTY key file version " + _version + " is not supported");
@@ -139,33 +134,33 @@ public Key Parse()
139134
privateKey = _data;
140135
break;
141136
default:
142-
throw new SshException("encryption " + _encryptionType + " is not supported for PuTTY key file");
137+
throw new SshException("Encryption " + _encryptionType + " is not supported for PuTTY key file");
143138
}
144139

145-
using (var macData = new SshDataStream(256))
140+
byte[] macData;
141+
using (var macStream = new SshDataStream(256))
146142
{
147-
macData.Write(_algorithmName, Encoding.UTF8);
148-
macData.Write(_encryptionType, Encoding.UTF8);
149-
macData.Write(_comment, Encoding.UTF8);
150-
macData.WriteBinary(_publicKey);
151-
macData.WriteBinary(privateKey);
152-
try
153-
{
154-
var encoded = hmac.ComputeHash(macData.ToArray());
143+
macStream.Write(_algorithmName, Encoding.UTF8);
144+
macStream.Write(_encryptionType, Encoding.UTF8);
145+
macStream.Write(_comment, Encoding.UTF8);
146+
macStream.WriteBinary(_publicKey);
147+
macStream.WriteBinary(privateKey);
148+
macData = macStream.ToArray();
149+
}
150+
151+
byte[] macValue;
152+
using (hmac)
153+
{
154+
macValue = hmac.ComputeHash(macData);
155+
}
155156
#if NET
156-
var reference = Convert.FromHexString(_mac);
157+
var reference = Convert.FromHexString(_mac);
157158
#else
158-
var reference = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_mac);
159+
var reference = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_mac);
159160
#endif
160-
if (!encoded.SequenceEqual(reference))
161-
{
162-
throw new SshException("MAC verification failed for PuTTY key file");
163-
}
164-
}
165-
finally
166-
{
167-
hmac.Dispose();
168-
}
161+
if (!macValue.SequenceEqual(reference))
162+
{
163+
throw new SshException("MAC verification failed for PuTTY key file");
169164
}
170165

171166
var publicKeyReader = new SshDataReader(_publicKey);
@@ -183,7 +178,7 @@ public Key Parse()
183178
break;
184179
case "ecdsa-sha2-nistp256":
185180
case "ecdsa-sha2-nistp384":
186-
case "ecdsa-sha2-nistp512":
181+
case "ecdsa-sha2-nistp521":
187182
var curve = publicKeyReader.ReadString(Encoding.ASCII);
188183
var pub = publicKeyReader.ReadBignum2();
189184
var prv = privateKeyReader.ReadBignum2();
@@ -207,7 +202,7 @@ public Key Parse()
207202
parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
208203
break;
209204
default:
210-
throw new SshException("key type " + keyType + " is not supported for PuTTY key file");
205+
throw new SshException("Key type " + keyType + " is not supported for PuTTY key file");
211206
}
212207

213208
parsedKey.Comment = _comment;
@@ -229,7 +224,7 @@ private static byte[] Argon2(string type, int iterations, int memory, int parall
229224
param = Argon2Parameters.Argon2id;
230225
break;
231226
default:
232-
throw new SshException("kdf " + type + " is not supported for PuTTY key file");
227+
throw new SshException("KDF " + type + " is not supported for PuTTY key file");
233228
}
234229

235230
var a2p = new Argon2Parameters.Builder(param)
@@ -254,18 +249,18 @@ private static byte[] Argon2(string type, int iterations, int memory, int parall
254249
return output;
255250
}
256251

257-
private static byte[] V2KDF(string passphrase)
252+
private static byte[] V2KDF(string passPhrase)
258253
{
259254
var cipherKey = new List<byte>();
260255

261-
var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
256+
var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
262257
for (var sequenceNumber = 0; sequenceNumber < 2; sequenceNumber++)
263258
{
264259
using (var sha1 = SHA1.Create())
265260
{
266261
var sequence = new byte[] { 0, 0, 0, (byte)sequenceNumber };
267262
_ = sha1.TransformBlock(sequence, 0, 4, outputBuffer: null, 0);
268-
_ = sha1.TransformFinalBlock(passwordBytes, 0, passwordBytes.Length);
263+
_ = sha1.TransformFinalBlock(passPhraseBytes, 0, passPhraseBytes.Length);
269264
Debug.Assert(sha1.Hash != null, "Hash is null");
270265
cipherKey.AddRange(sha1.Hash);
271266
}

src/Renci.SshNet/PrivateKeyFile.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ namespace Renci.SshNet
2525
/// The following private keys are supported:
2626
/// <list type="bullet">
2727
/// <item>
28-
/// <description>RSA in OpenSSL PEM, ssh.com and OpenSSH key format</description>
28+
/// <description>RSA in OpenSSL PEM, ssh.com, OpenSSH and PuTTY key format</description>
2929
/// </item>
3030
/// <item>
31-
/// <description>DSA in OpenSSL PEM and ssh.com format</description>
31+
/// <description>DSA in OpenSSL PEM, ssh.com and PuTTY key format</description>
3232
/// </item>
3333
/// <item>
34-
/// <description>ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format</description>
34+
/// <description>ECDSA 256/384/521 in OpenSSL PEM, OpenSSH and PuTTY key format</description>
3535
/// </item>
3636
/// <item>
37-
/// <description>ED25519 in OpenSSL PEM and OpenSSH key format</description>
37+
/// <description>ED25519 in OpenSSL PEM, OpenSSH and PuTTY key format</description>
3838
/// </item>
3939
/// </list>
4040
/// </para>
@@ -73,7 +73,7 @@ namespace Renci.SshNet
7373
/// </list>
7474
/// </para>
7575
/// <para>
76-
/// The following encryption algorithms are supported for OpenSSH format:
76+
/// The following encryption algorithms are supported for OpenSSH key format:
7777
/// <list type="bullet">
7878
/// <item>
7979
/// <description>3des-cbc</description>
@@ -107,29 +107,37 @@ namespace Renci.SshNet
107107
/// </item>
108108
/// </list>
109109
/// </para>
110+
/// <para>
111+
/// The following encryption algorithms are supported for PuTTY key format:
112+
/// <list type="bullet">
113+
/// <item>
114+
/// <description>aes256-cbc</description>
115+
/// </item>
116+
/// </list>
117+
/// </para>
110118
/// </remarks>
111119
public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
112120
{
113121
private const string PrivateKeyPattern = @"^-+ *BEGIN (?<keyName>\w+( \w+)*) *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[a-fA-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k<keyName> *-+";
114-
private const string PrivateKeyPuTTYPattern = @"^(?<keyName>PuTTY-User-Key-File)-(?<version>\d+): (?<algorithmName>[\w-]+)\r?\nEncryption: (?<encryptionType>[\w-]+)\nComment: (?<comment>.*)\r?\nPublic-Lines: \d+\r?\n(?<publicKey>(([a-zA-Z0-9/+=]{1,64})\r?\n)+)(Key-Derivation: (?<argon2Type>\w+)\r?\nArgon2-Memory: (?<argon2Memory>\d+)\r?\nArgon2-Passes: (?<argon2Passes>\d+)\r?\nArgon2-Parallelism: (?<argon2Parallelism>\d+)\r?\nArgon2-Salt: (?<argon2Salt>[\w\d]+)\r?\n)?Private-Lines: \d+\r?\n(?<data>(([a-zA-Z0-9/+=]{1,64})\r?\n)+)+Private-MAC: (?<mac>[\w\d]+)";
122+
private const string PuTTYPrivateKeyPattern = @"^(?<keyName>PuTTY-User-Key-File)-(?<version>\d+): (?<algorithmName>[\w-]+)\r?\nEncryption: (?<encryptionType>[\w-]+)\r?\nComment: (?<comment>.*)\r?\nPublic-Lines: \d+\r?\n(?<publicKey>(([a-zA-Z0-9/+=]{1,64})\r?\n)+)(Key-Derivation: (?<argon2Type>\w+)\r?\nArgon2-Memory: (?<argon2Memory>\d+)\r?\nArgon2-Passes: (?<argon2Passes>\d+)\r?\nArgon2-Parallelism: (?<argon2Parallelism>\d+)\r?\nArgon2-Salt: (?<argon2Salt>[\w\d]+)\r?\n)?Private-Lines: \d+\r?\n(?<data>(([a-zA-Z0-9/+=]{1,64})\r?\n)+)+Private-MAC: (?<mac>[\w\d]+)";
115123
private const string CertificatePattern = @"(?<type>[-\w]+@openssh\.com)\s(?<data>[a-zA-Z0-9\/+=]*)(\s+(?<comment>.*))?";
116124

117125
#if NET7_0_OR_GREATER
118126
private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex();
119-
private static readonly Regex PrivateKeyPuTTYRegex = GetPrivateKeyPuTTYRegex();
127+
private static readonly Regex PuTTYPrivateKeyRegex = GetPrivateKeyPuTTYRegex();
120128
private static readonly Regex CertificateRegex = GetCertificateRegex();
121129

122130
[GeneratedRegex(PrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
123131
private static partial Regex GetPrivateKeyRegex();
124132

125-
[GeneratedRegex(PrivateKeyPuTTYPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
133+
[GeneratedRegex(PuTTYPrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
126134
private static partial Regex GetPrivateKeyPuTTYRegex();
127135

128136
[GeneratedRegex(CertificatePattern, RegexOptions.ExplicitCapture)]
129137
private static partial Regex GetCertificateRegex();
130138
#else
131139
private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
132-
private static readonly Regex PrivateKeyPuTTYRegex = new Regex(PrivateKeyPuTTYPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
140+
private static readonly Regex PuTTYPrivateKeyRegex = new Regex(PuTTYPrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
133141
private static readonly Regex CertificateRegex = new Regex(CertificatePattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
134142
#endif
135143

@@ -295,7 +303,7 @@ private void Open(Stream privateKey, string? passPhrase)
295303
var text = sr.ReadToEnd();
296304
if (text.StartsWith("PuTTY-User-Key-File", StringComparison.Ordinal))
297305
{
298-
privateKeyMatch = PrivateKeyPuTTYRegex.Match(text);
306+
privateKeyMatch = PuTTYPrivateKeyRegex.Match(text);
299307
}
300308
else
301309
{

0 commit comments

Comments
 (0)