Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for encrypted private keys. #207

Merged
merged 13 commits into from
Aug 12, 2024
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@ abstract class Credential
{ }
class PrivateKeyCredential : Credential
{
PrivateKeyCredential(string path);
PrivateKeyCredential(string path, string? password = null);
PrivateKeyCredential(string path, Func<string?> passwordPrompt);
}
class PasswordCredential : Credential
{
Expand Down Expand Up @@ -426,7 +427,9 @@ Supported private key formats:
- RSA, ECDSA in `OPENSSH PRIVATE KEY` (`openssh-key-v1`)

Supported private key encryption cyphers:
- none
- OpenSSH Keys `OPENSSH PRIVATE KEY` (`openssh-key-v1`)
- aes[128|192|256]-[cbc|ctr]
- aes[128|256]-gcm@openssh.com

Supported client key algorithms:
- ecdsa-sha2-nistp521
Expand Down
49 changes: 49 additions & 0 deletions src/Tmds.Ssh/AesCtr.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// This file is part of Tmds.Ssh which is released under MIT.
// See file LICENSE for full license details.

using System;
using System.Security.Cryptography;

namespace Tmds.Ssh;

static class AesCtr
{
public static void DecryptCtr(ReadOnlySpan<byte> key, Span<byte> counter, ReadOnlySpan<byte> ciphertext, Span<byte> plaintext)
{
if (plaintext.Length < ciphertext.Length)
{
throw new ArgumentException("Plaintext buffer is too small.");
}

using Aes aes = Aes.Create();
aes.Key = key.ToArray();

int blockSize = counter.Length;
int offset = 0;
Span<byte> temp = stackalloc byte[blockSize];

while (offset < ciphertext.Length)
{
// .NET Does not have a CTR mode but we can use ECB with out own
// iv/counter manipulation between blocks.
aes.EncryptEcb(counter, temp, PaddingMode.None);

// Increment the counter that is treated as a big endian uint128
// value.
for (int i = blockSize - 1; i >= 0; i--)
{
if (++counter[i] != 0)
{
break;
}
}

for (int i = 0; i < Math.Min(blockSize, ciphertext.Length - offset); i++)
{
plaintext[i + offset] = (byte)(ciphertext[i + offset] ^ temp[i]);
}

offset += blockSize;
}
}
}
16 changes: 16 additions & 0 deletions src/Tmds.Ssh/AlgorithmNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,27 @@ static class AlgorithmNames // TODO: rename to KnownNames
public static Name EcdsaSha2Nistp521 => new Name(EcdsaSha2Nistp521Bytes);

// Encryption algorithms.
private static readonly byte[] Aes128CbcBytes = "aes128-cbc"u8.ToArray();
public static Name Aes128Cbc => new Name(Aes128CbcBytes);
private static readonly byte[] Aes192CbcBytes = "aes192-cbc"u8.ToArray();
public static Name Aes192Cbc => new Name(Aes192CbcBytes);
private static readonly byte[] Aes256CbcBytes = "aes256-cbc"u8.ToArray();
public static Name Aes256Cbc => new Name(Aes256CbcBytes);
private static readonly byte[] Aes128CtrBytes = "aes128-ctr"u8.ToArray();
public static Name Aes128Ctr => new Name(Aes128CtrBytes);
private static readonly byte[] Aes192CtrBytes = "aes192-ctr"u8.ToArray();
public static Name Aes192Ctr => new Name(Aes192CtrBytes);
private static readonly byte[] Aes256CtrBytes = "aes256-ctr"u8.ToArray();
public static Name Aes256Ctr => new Name(Aes256CtrBytes);
private static readonly byte[] Aes128GcmBytes = "aes128-gcm@openssh.com"u8.ToArray();
public static Name Aes128Gcm => new Name(Aes128GcmBytes);
private static readonly byte[] Aes256GcmBytes = "aes256-gcm@openssh.com"u8.ToArray();
public static Name Aes256Gcm => new Name(Aes256GcmBytes);

// KDF algorithms:
private static readonly byte[] BCryptBytes = "bcrypt"u8.ToArray();
public static Name BCrypt => new Name(BCryptBytes);

// Mac algorithms.
private static readonly byte[] HMacSha2_256Bytes = "hmac-sha2-256"u8.ToArray();
public static Name HMacSha2_256 => new Name(HMacSha2_256Bytes);
Expand Down
539 changes: 539 additions & 0 deletions src/Tmds.Ssh/BCrypt.cs

Large diffs are not rendered by default.

7 changes: 0 additions & 7 deletions src/Tmds.Ssh/EncryptionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Security.Cryptography;

namespace Tmds.Ssh;

Expand Down Expand Up @@ -85,10 +84,4 @@ public static EncryptionAlgorithm Find(Name name)
isAuthenticated: true,
tagLength: 16) },
};

private static IPacketEncoder CreatePacketEncoder(IDisposableCryptoTransform encodeTransform, IHMac hmac)
=> new TransformAndHMacPacketEncoder(encodeTransform, hmac);

private static IPacketDecoder CreatePacketDecoder(SequencePool sequencePool, IDisposableCryptoTransform encodeTransform, IHMac hmac)
=> new TransformAndHMacPacketDecoder(sequencePool, encodeTransform, hmac);
}
102 changes: 102 additions & 0 deletions src/Tmds.Ssh/OpenSshKeyCipher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// This file is part of Tmds.Ssh which is released under MIT.
// See file LICENSE for full license details.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;

namespace Tmds.Ssh;

sealed class OpenSshKeyCipher
{
private delegate byte[] DecryptDelegate(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag);

private readonly DecryptDelegate _decryptData;

private OpenSshKeyCipher(
int keyLength,
int ivLength,
DecryptDelegate decryptData,
int tagLength = 0)
{
KeyLength = keyLength;
IVLength = ivLength;
TagLength = tagLength;
_decryptData = decryptData;
}

public int KeyLength { get; }
public int IVLength { get; }
public int TagLength { get; }

public byte[] Decrypt(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag)
{
if (KeyLength != key.Length)
{
throw new ArgumentException(nameof(key));
}
if (IVLength != iv.Length)
{
throw new ArgumentException(nameof(iv));
}
if (tag.Length != TagLength)
{
throw new ArgumentException(nameof(tag));
}

return _decryptData(key, iv, data, tag);
}

public static bool TryGetCipher(Name name, [NotNullWhen(true)] out OpenSshKeyCipher? ciphers)
=> _ciphers.TryGetValue(name, out ciphers);

private static Dictionary<Name, OpenSshKeyCipher> _ciphers = new()
{
{ AlgorithmNames.Aes128Cbc, CreateAesCbcCipher(16) },
{ AlgorithmNames.Aes192Cbc, CreateAesCbcCipher(24) },
{ AlgorithmNames.Aes256Cbc, CreateAesCbcCipher(32) },
{ AlgorithmNames.Aes128Ctr, CreateAesCtrCipher(16) },
{ AlgorithmNames.Aes192Ctr, CreateAesCtrCipher(24) },
{ AlgorithmNames.Aes256Ctr, CreateAesCtrCipher(32) },
{ AlgorithmNames.Aes128Gcm, CreateAesGcmCipher(16) },
{ AlgorithmNames.Aes256Gcm, CreateAesGcmCipher(32) },
};

private static OpenSshKeyCipher CreateAesCbcCipher(int keyLength)
=> new OpenSshKeyCipher(keyLength: keyLength, ivLength: 16,
(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> _)
=> DecryptAesCbc(key, iv, data));

private static OpenSshKeyCipher CreateAesCtrCipher(int keyLength)
=> new OpenSshKeyCipher(keyLength: keyLength, ivLength: 16,
(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> _)
=> DecryptAesCtr(key, iv, data));

private static OpenSshKeyCipher CreateAesGcmCipher(int keyLength)
=> new OpenSshKeyCipher(keyLength: keyLength, ivLength: 12,
DecryptAesGcm,
tagLength: 16);

private static byte[] DecryptAesCbc(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data)
{
using Aes aes = Aes.Create();
aes.Key = key.ToArray();
return aes.DecryptCbc(data, iv, PaddingMode.None);
}

private static byte[] DecryptAesCtr(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data)
{
byte[] plaintext = new byte[data.Length];
AesCtr.DecryptCtr(key, iv, data, plaintext);
return plaintext;
}

private static byte[] DecryptAesGcm(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag)
{
using AesGcm aesGcm = new AesGcm(key, tag.Length);
byte[] plaintext = new byte[data.Length];
aesGcm.Decrypt(iv, data, tag, plaintext, null);
return plaintext;
}
}
8 changes: 7 additions & 1 deletion src/Tmds.Ssh/PrivateKeyCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ public sealed class PrivateKeyCredential : Credential
{
internal string FilePath { get; }

public PrivateKeyCredential(string path)
internal Func<string?> PasswordPrompt { get; }

public PrivateKeyCredential(string path, string? password = null) : this(path, () => password)
{ }

public PrivateKeyCredential(string path, Func<string?> passwordPrompt)
{
FilePath = path ?? throw new ArgumentNullException(nameof(path));
PasswordPrompt = passwordPrompt;
}
}
Loading
Loading