diff --git a/Ed25519 Tests/SignerTests.cs b/Ed25519 Tests/SignerTests.cs
index c83ec45..bc0611a 100644
--- a/Ed25519 Tests/SignerTests.cs
+++ b/Ed25519 Tests/SignerTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Security.Cryptography;
using System.Text;
using Ed25519;
@@ -30,7 +31,7 @@ namespace Ed25519_Tests
try
{
Signer.Sign(message, privateKey, publicKey);
- Assert.That(false);
+ Assert.Fail("Should not be reached!");
}
catch (ArgumentException e)
{
@@ -126,6 +127,127 @@ namespace Ed25519_Tests
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
[Test]
public void TestRFC8032Test01EmptyMessage()
diff --git a/Ed25519/Ed25519.csproj b/Ed25519/Ed25519.csproj
index ad29d3f..485a24f 100644
--- a/Ed25519/Ed25519.csproj
+++ b/Ed25519/Ed25519.csproj
@@ -24,4 +24,8 @@
+
+
+
+
diff --git a/Ed25519/Extensions.cs b/Ed25519/Extensions.cs
index e0ed216..20da4b0 100644
--- a/Ed25519/Extensions.cs
+++ b/Ed25519/Extensions.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
+using Encrypter;
namespace Ed25519
{
@@ -108,8 +109,16 @@ namespace Ed25519
return data[index / 8] >> (index % 8) & 1;
}
+ ///
+ /// Extracts the public key out of the given private key. The private key must be valid, i.e. must consist of 32 bytes.
+ ///
+ /// The private key.
+ /// The corresponding public key.
public static ReadOnlySpan ExtractPublicKey(this ReadOnlySpan privateKey)
{
+ if(privateKey.Length != 32)
+ return ReadOnlySpan.Empty;
+
var hash = privateKey.ComputeHash();
var a = Constants.TWO_POW_BIT_LENGTH_MINUS_TWO;
for (var i = 3; i < Constants.BIT_LENGTH - 2; i++)
@@ -125,9 +134,39 @@ namespace Ed25519
return bigA.EncodePoint();
}
+ ///
+ /// Extracts the public key out of the given private key. The private key must be valid, i.e. must consist of 32 bytes.
+ ///
+ /// The private key.
+ /// The corresponding public key.
public static ReadOnlySpan ExtractPublicKey(this Span privateKey)
{
return new ReadOnlySpan(privateKey.ToArray()).ExtractPublicKey();
}
+
+ ///
+ /// Writes a given key to a file.
+ ///
+ /// The chosen key
+ /// The desired file
+ public static void WriteKey(this ReadOnlySpan key, string filename)
+ {
+ File.WriteAllBytes(filename, key.ToArray());
+ }
+
+ ///
+ /// Decrypts an encrypted private key.
+ ///
+ /// The encrypted private key.
+ /// The matching password.
+ /// The decrypted private key.
+ public static ReadOnlySpan DecryptPrivateKey(this ReadOnlySpan privateKey, string password)
+ {
+ using var inputStream = new MemoryStream(privateKey.ToArray());
+ using var outputStream = new MemoryStream();
+ CryptoProcessor.Decrypt(inputStream, outputStream, password).Wait();
+
+ return outputStream.ToArray();
+ }
}
}
diff --git a/Ed25519/Signer.cs b/Ed25519/Signer.cs
index dcb57d0..2cf3757 100644
--- a/Ed25519/Signer.cs
+++ b/Ed25519/Signer.cs
@@ -2,16 +2,49 @@
using System.Collections.Generic;
using System.IO;
using System.Numerics;
+using System.Security.Cryptography;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Encrypter;
namespace Ed25519
{
public static class Signer
{
+ ///
+ /// Generates a random private key.
+ ///
+ /// An optional password to encrypt the key.
+ /// The private key.
+ public static ReadOnlySpan GeneratePrivateKey(string password = "")
+ {
+ var privateKey = new Span(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(outputStream.ToArray());
+ }
+
+ return privateKey;
+ }
+
+ ///
+ /// Loads a key (public or private key) from a file.
+ ///
+ /// The entire path to the corresponding file.
+ /// The desired key.
+ public static ReadOnlySpan LoadKey(string filename) => !File.Exists(filename) ? ReadOnlySpan.Empty : File.ReadAllBytes(filename);
+
public static ReadOnlySpan Sign(ReadOnlySpan message, ReadOnlySpan privateKey, ReadOnlySpan publicKey)
{
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)
throw new ArgumentException($"Public key length is wrong. Got {publicKey.Length} instead of {Constants.BIT_LENGTH / 8}.");