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}.");