Implemented key handling

This commit is contained in:
Thorsten Sommer 2020-01-06 20:01:21 +01:00
parent 0707aa9cdc
commit 744bcd217f
4 changed files with 200 additions and 2 deletions

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Ed25519; using Ed25519;
@ -30,7 +31,7 @@ namespace Ed25519_Tests
try try
{ {
Signer.Sign(message, privateKey, publicKey); Signer.Sign(message, privateKey, publicKey);
Assert.That(false); Assert.Fail("Should not be reached!");
} }
catch (ArgumentException e) catch (ArgumentException e)
{ {
@ -126,6 +127,127 @@ namespace Ed25519_Tests
Assert.That(validationResult, Is.True); Assert.That(validationResult, Is.True);
} }
[Test]
public void TestKeyGeneratorWithoutPassword()
{
var privateKey = Signer.GeneratePrivateKey();
var publicKey = privateKey.ExtractPublicKey();
Assert.That(privateKey.Length, Is.EqualTo(32));
Assert.That(publicKey.Length, Is.EqualTo(32));
Assert.That(privateKey.ToArray(), Is.Not.EqualTo(publicKey.ToArray()));
}
[Test]
public void TestKeyGeneratorWithPassword()
{
var password = "test password";
var privateKeyEncrypted = Signer.GeneratePrivateKey(password);
var privateKeyDecrypted = privateKeyEncrypted.DecryptPrivateKey(password);
var publicKey = privateKeyDecrypted.ExtractPublicKey();
Assert.That(privateKeyEncrypted.Length, Is.GreaterThan(32));
Assert.That(privateKeyDecrypted.Length, Is.EqualTo(32));
Assert.That(publicKey.Length, Is.EqualTo(32));
Assert.That(privateKeyEncrypted.ToArray(), Is.Not.EqualTo(privateKeyDecrypted.ToArray()));
Assert.That(privateKeyEncrypted.ToArray(), Is.Not.EqualTo(publicKey.ToArray()));
Assert.That(privateKeyDecrypted.ToArray(), Is.Not.EqualTo(publicKey.ToArray()));
}
[Test]
public void TestMultipleKeysWithoutPassword()
{
var privateKey1 = Signer.GeneratePrivateKey();
var privateKey2 = Signer.GeneratePrivateKey();
var privateKey3 = Signer.GeneratePrivateKey();
Assert.That(privateKey1.ToArray(), Is.Not.EqualTo(privateKey2.ToArray()));
Assert.That(privateKey1.ToArray(), Is.Not.EqualTo(privateKey3.ToArray()));
Assert.That(privateKey2.ToArray(), Is.Not.EqualTo(privateKey3.ToArray()));
}
[Test]
public void TestMultipleKeysWithPassword()
{
var password = "test password";
var privateKey1 = Signer.GeneratePrivateKey(password);
var privateKey2 = Signer.GeneratePrivateKey(password);
var privateKey3 = Signer.GeneratePrivateKey(password);
Assert.That(privateKey1.ToArray(), Is.Not.EqualTo(privateKey2.ToArray()));
Assert.That(privateKey1.ToArray(), Is.Not.EqualTo(privateKey3.ToArray()));
Assert.That(privateKey2.ToArray(), Is.Not.EqualTo(privateKey3.ToArray()));
}
[Test]
public void TestPublicKeyFromRandomData()
{
var privateKey = new byte[] { 0x00, 0xac, 0x48 }.AsSpan();
var publicKey = privateKey.ExtractPublicKey();
Assert.That(privateKey.Length, Is.EqualTo(3));
Assert.That(publicKey.Length, Is.EqualTo(0));
Assert.That(publicKey.IsEmpty, Is.True);
}
[Test]
public void TestWritingKeys()
{
var tempFilePrivate = Path.GetTempFileName();
var tempFilePublic = Path.GetTempFileName();
try
{
var privateKey = Signer.GeneratePrivateKey();
var publicKey = privateKey.ExtractPublicKey();
privateKey.WriteKey(tempFilePrivate);
publicKey.WriteKey(tempFilePublic);
var reloadPrivateKey = Signer.LoadKey(tempFilePrivate);
var reloadPublicKey = Signer.LoadKey(tempFilePublic);
Assert.That(reloadPublicKey.Length, Is.EqualTo(32));
Assert.That(reloadPrivateKey.Length, Is.EqualTo(32));
Assert.That(reloadPublicKey.ToArray(), Is.EqualTo(publicKey.ToArray()));
Assert.That(reloadPrivateKey.ToArray(), Is.EqualTo(privateKey.ToArray()));
}
finally
{
File.Delete(tempFilePublic);
File.Delete(tempFilePrivate);
}
}
[Test]
public void TestExtractPublicKeyFromEncryptedPrivateKey()
{
var privateKeyEncrypted = Signer.GeneratePrivateKey("secret password");
var privateKeyDecrypted = privateKeyEncrypted.DecryptPrivateKey("secret password");
var publicKeyFromDecrypted = privateKeyDecrypted.ExtractPublicKey();
var publicKeyFromEncrypted = privateKeyEncrypted.ExtractPublicKey();
Assert.That(publicKeyFromDecrypted.ToArray(), Is.Not.EqualTo(publicKeyFromEncrypted.ToArray()));
}
[Test]
public void TestSigningBehaviourPrivateKeyWithPassword()
{
var privateKeyEncrypted = Signer.GeneratePrivateKey("secret password");
var privateKeyDecrypted = privateKeyEncrypted.DecryptPrivateKey("secret password");
var publicKey = privateKeyDecrypted.ExtractPublicKey();
var message = Encoding.UTF8.GetBytes("This is a test message.");
var signaturePrivateKeyDecrypted = Signer.Sign(message, privateKeyDecrypted, publicKey);
var signaturePrivateKeyEncrypted = Signer.Sign(message, privateKeyEncrypted[..32], publicKey); // Note: The encrypted private key is 64 bytes long!
var validationDecrypted = Signer.Validate(signaturePrivateKeyDecrypted, message, publicKey);
var validationEncrypted = Signer.Validate(signaturePrivateKeyEncrypted, message, publicKey);
Assert.That(validationDecrypted, Is.True); // This is the intended behaviour. The public key was derived from the decrypted private key.
Assert.That(validationEncrypted, Is.False); // This should fail, because the public key does not match the *encrypted* private key!
Assert.That(signaturePrivateKeyEncrypted.ToArray(), Is.Not.EqualTo(signaturePrivateKeyDecrypted.ToArray()));
}
// See https://tools.ietf.org/html/rfc8032#section-7.1 // See https://tools.ietf.org/html/rfc8032#section-7.1
[Test] [Test]
public void TestRFC8032Test01EmptyMessage() public void TestRFC8032Test01EmptyMessage()

View File

@ -24,4 +24,8 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Encrypter" Version="1.0.0" />
</ItemGroup>
</Project> </Project>

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Numerics; using System.Numerics;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Encrypter;
namespace Ed25519 namespace Ed25519
{ {
@ -108,8 +109,16 @@ namespace Ed25519
return data[index / 8] >> (index % 8) & 1; return data[index / 8] >> (index % 8) & 1;
} }
/// <summary>
/// Extracts the public key out of the given private key. The private key must be valid, i.e. must consist of 32 bytes.
/// </summary>
/// <param name="privateKey">The private key.</param>
/// <returns>The corresponding public key.</returns>
public static ReadOnlySpan<byte> ExtractPublicKey(this ReadOnlySpan<byte> privateKey) public static ReadOnlySpan<byte> ExtractPublicKey(this ReadOnlySpan<byte> privateKey)
{ {
if(privateKey.Length != 32)
return ReadOnlySpan<byte>.Empty;
var hash = privateKey.ComputeHash(); var hash = privateKey.ComputeHash();
var a = Constants.TWO_POW_BIT_LENGTH_MINUS_TWO; var a = Constants.TWO_POW_BIT_LENGTH_MINUS_TWO;
for (var i = 3; i < Constants.BIT_LENGTH - 2; i++) for (var i = 3; i < Constants.BIT_LENGTH - 2; i++)
@ -125,9 +134,39 @@ namespace Ed25519
return bigA.EncodePoint(); return bigA.EncodePoint();
} }
/// <summary>
/// Extracts the public key out of the given private key. The private key must be valid, i.e. must consist of 32 bytes.
/// </summary>
/// <param name="privateKey">The private key.</param>
/// <returns>The corresponding public key.</returns>
public static ReadOnlySpan<byte> ExtractPublicKey(this Span<byte> privateKey) public static ReadOnlySpan<byte> ExtractPublicKey(this Span<byte> privateKey)
{ {
return new ReadOnlySpan<byte>(privateKey.ToArray()).ExtractPublicKey(); return new ReadOnlySpan<byte>(privateKey.ToArray()).ExtractPublicKey();
} }
/// <summary>
/// Writes a given key to a file.
/// </summary>
/// <param name="key">The chosen key</param>
/// <param name="filename">The desired file</param>
public static void WriteKey(this ReadOnlySpan<byte> key, string filename)
{
File.WriteAllBytes(filename, key.ToArray());
}
/// <summary>
/// Decrypts an encrypted private key.
/// </summary>
/// <param name="privateKey">The encrypted private key.</param>
/// <param name="password">The matching password.</param>
/// <returns>The decrypted private key.</returns>
public static ReadOnlySpan<byte> DecryptPrivateKey(this ReadOnlySpan<byte> privateKey, string password)
{
using var inputStream = new MemoryStream(privateKey.ToArray());
using var outputStream = new MemoryStream();
CryptoProcessor.Decrypt(inputStream, outputStream, password).Wait();
return outputStream.ToArray();
}
} }
} }

View File

@ -2,16 +2,49 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Numerics; using System.Numerics;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Encrypter;
namespace Ed25519 namespace Ed25519
{ {
public static class Signer public static class Signer
{ {
/// <summary>
/// Generates a random private key.
/// </summary>
/// <param name="password">An optional password to encrypt the key.</param>
/// <returns>The private key.</returns>
public static ReadOnlySpan<byte> GeneratePrivateKey(string password = "")
{
var privateKey = new Span<byte>(new byte[32]);
RandomNumberGenerator.Create().GetBytes(privateKey);
if (!string.IsNullOrWhiteSpace(password))
{
using var inputStream = new MemoryStream(privateKey.ToArray(), false);
using var outputStream = new MemoryStream();
CryptoProcessor.Encrypt(inputStream, outputStream, password).Wait();
privateKey = new Span<byte>(outputStream.ToArray());
}
return privateKey;
}
/// <summary>
/// Loads a key (public or private key) from a file.
/// </summary>
/// <param name="filename">The entire path to the corresponding file.</param>
/// <returns>The desired key.</returns>
public static ReadOnlySpan<byte> LoadKey(string filename) => !File.Exists(filename) ? ReadOnlySpan<byte>.Empty : File.ReadAllBytes(filename);
public static ReadOnlySpan<byte> Sign(ReadOnlySpan<byte> message, ReadOnlySpan<byte> privateKey, ReadOnlySpan<byte> publicKey) public static ReadOnlySpan<byte> Sign(ReadOnlySpan<byte> message, ReadOnlySpan<byte> privateKey, ReadOnlySpan<byte> publicKey)
{ {
if(privateKey.Length != Constants.BIT_LENGTH / 8) if(privateKey.Length != Constants.BIT_LENGTH / 8)
throw new ArgumentException($"Private key length is wrong. Got {publicKey.Length} instead of {Constants.BIT_LENGTH / 8}."); throw new ArgumentException($"Private key length is wrong. Got {privateKey.Length} instead of {Constants.BIT_LENGTH / 8}.");
if (publicKey.Length != Constants.BIT_LENGTH / 8) if (publicKey.Length != Constants.BIT_LENGTH / 8)
throw new ArgumentException($"Public key length is wrong. Got {publicKey.Length} instead of {Constants.BIT_LENGTH / 8}."); throw new ArgumentException($"Public key length is wrong. Got {publicKey.Length} instead of {Constants.BIT_LENGTH / 8}.");