From d1305e606c87204b52d0285c2682482e41ef577a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 7 Jan 2020 12:16:59 +0100 Subject: [PATCH] Added async methods --- Ed25519/EdPoint.cs | 22 ++++++ Ed25519/Extensions.cs | 114 +++++++++++++++++++++++++----- Ed25519/Signer.cs | 132 ++++++++++++++++++++++++++++++++++- Ed25519/Tests/SignerTests.cs | 33 ++++++++- 4 files changed, 281 insertions(+), 20 deletions(-) diff --git a/Ed25519/EdPoint.cs b/Ed25519/EdPoint.cs index 44a3a7f..55c850d 100644 --- a/Ed25519/EdPoint.cs +++ b/Ed25519/EdPoint.cs @@ -33,6 +33,28 @@ namespace Ed25519 return point; } + public static EdPoint DecodePoint(byte[] pointBytes) + { + var y = new BigInteger(pointBytes) & Constants.U_N; + var x = y.RecoverX(); + + if ((x.IsEven ? 0 : 1) != pointBytes.GetBit(Constants.BIT_LENGTH - 1)) + { + x = Constants.Q - x; + } + + var point = new EdPoint + { + X = x, + Y = y, + }; + + if (!point.IsOnCurve()) + throw new ArgumentException("Decoding point is not on curve"); + + return point; + } + public ReadOnlySpan EncodePoint() { var nout = this.Y.EncodeInt(); diff --git a/Ed25519/Extensions.cs b/Ed25519/Extensions.cs index b90d4d4..5ccb6e0 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 System.Threading.Tasks; using Encrypter; namespace Ed25519 @@ -16,14 +17,36 @@ namespace Ed25519 return sha512.ComputeHash(data.ToArray()); } + internal static async Task ComputeHashAsync(this byte[] data) + { + return await Task.Run(() => + { + using var sha512 = SHA512.Create(); + return sha512.ComputeHash(data); + }); + } + internal static ReadOnlySpan ComputeHash(this Stream inputStream) { - inputStream.Seek(0, SeekOrigin.Begin); + if(inputStream.CanSeek) + inputStream.Seek(0, SeekOrigin.Begin); using var sha512 = SHA512.Create(); return sha512.ComputeHash(inputStream); } + internal static async Task ComputeHashAsync(this Stream inputStream) + { + return await Task.Run(() => + { + if(inputStream.CanSeek) + inputStream.Seek(0, SeekOrigin.Begin); + + using var sha512 = SHA512.Create(); + return sha512.ComputeHash(inputStream); + }); + } + internal static BigInteger Mod(this BigInteger number, BigInteger modulo) { var result = number % modulo; @@ -31,10 +54,7 @@ namespace Ed25519 return result; } - internal static BigInteger Inv(this BigInteger number) - { - return number.ExpMod(Constants.QM2, Constants.Q); - } + internal static BigInteger Inv(this BigInteger number) => number.ExpMod(Constants.QM2, Constants.Q); internal static BigInteger RecoverX(this BigInteger y) { @@ -96,10 +116,9 @@ namespace Ed25519 return nout; } - internal static BigInteger DecodeInt(this ReadOnlySpan data) - { - return new BigInteger(data) & Constants.U_N; - } + internal static BigInteger DecodeInt(this ReadOnlySpan data) => new BigInteger(data) & Constants.U_N; + + internal static BigInteger DecodeInt(this byte[] data) => new BigInteger(data) & Constants.U_N; internal static BigInteger HashInt(this MemoryStream data) { @@ -120,16 +139,34 @@ namespace Ed25519 return hashSum; } - internal static int GetBit(this ReadOnlySpan data, int index) + internal static async Task HashIntAsync(this MemoryStream data) { - return data[index / 8] >> (index % 8) & 1; + data.Seek(0, SeekOrigin.Begin); + + var hash = await data.ComputeHashAsync(); + var hashSum = BigInteger.Zero; + + for (var i = 0; i < 2 * Constants.BIT_LENGTH; i++) + { + var bit = hash.GetBit(i); + if (bit != 0) + { + hashSum += Constants.TWO_POW_CACHE[i]; + } + } + + return hashSum; } + internal static int GetBit(this ReadOnlySpan data, int index) => data[index / 8] >> (index % 8) & 1; + + internal static int GetBit(this byte[] data, int index) => 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. + /// The corresponding public key. Returns an empty ReadOnlySpan, when the private key's length is incorrect. public static ReadOnlySpan ExtractPublicKey(this ReadOnlySpan privateKey) { if(privateKey.Length != 32) @@ -155,9 +192,31 @@ namespace Ed25519 /// /// The private key. /// The corresponding public key. - public static ReadOnlySpan ExtractPublicKey(this Span privateKey) + public static ReadOnlySpan ExtractPublicKey(this Span privateKey) => new ReadOnlySpan(privateKey.ToArray()).ExtractPublicKey(); + + /// + /// 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. Returns null, when the private key's length is incorrect. + public static async Task ExtractPublicKeyAsync(this byte[] privateKey) { - return new ReadOnlySpan(privateKey.ToArray()).ExtractPublicKey(); + if (privateKey.Length != 32) + return null; + + var hash = await privateKey.ComputeHashAsync(); + var a = Constants.TWO_POW_BIT_LENGTH_MINUS_TWO; + for (var i = 3; i < Constants.BIT_LENGTH - 2; i++) + { + var bit = hash.GetBit(i); + if (bit != 0) + { + a += Constants.TWO_POW_CACHE[i]; + } + } + + var bigA = Constants.B.ScalarMul(a); + return bigA.EncodePoint().ToArray(); } /// @@ -165,10 +224,14 @@ namespace Ed25519 /// /// The chosen key /// The desired file - public static void WriteKey(this ReadOnlySpan key, string filename) - { - File.WriteAllBytes(filename, key.ToArray()); - } + public static void WriteKey(this ReadOnlySpan key, string filename) => File.WriteAllBytes(filename, key.ToArray()); + + /// + /// Writes a given key to a file. + /// + /// The chosen key + /// The desired file + public static async Task WriteKeyAsync(this byte[] key, string filename) => await File.WriteAllBytesAsync(filename, key); /// /// Decrypts an encrypted private key. @@ -184,5 +247,20 @@ namespace Ed25519 return outputStream.ToArray(); } + + /// + /// Decrypts an encrypted private key. + /// + /// The encrypted private key. + /// The matching password. + /// The decrypted private key. + public static async Task DecryptPrivateKeyAsync(this byte[] privateKey, string password) + { + await using var inputStream = new MemoryStream(privateKey); + await using var outputStream = new MemoryStream(); + await CryptoProcessor.Decrypt(inputStream, outputStream, password); + + return outputStream.ToArray(); + } } } diff --git a/Ed25519/Signer.cs b/Ed25519/Signer.cs index ac0360c..4cebada 100644 --- a/Ed25519/Signer.cs +++ b/Ed25519/Signer.cs @@ -34,13 +34,42 @@ namespace Ed25519 return privateKey; } + /// + /// Generates a random private key. + /// + /// An optional password to encrypt the key. + /// The private key. + public static async Task GeneratePrivateKeyAsync(string password = "") + { + var privateKey = new byte[32]; + await Task.Run(() => RandomNumberGenerator.Create().GetBytes(privateKey)); + + if (!string.IsNullOrWhiteSpace(password)) + { + await using var inputStream = new MemoryStream(privateKey, false); + await using var outputStream = new MemoryStream(); + + await CryptoProcessor.Encrypt(inputStream, outputStream, password); + privateKey = outputStream.ToArray(); + } + + return privateKey; + } + /// /// Loads a key (public or private key) from a file. /// /// The entire path to the corresponding file. - /// The desired key. + /// The desired key. Returns an empty ReadOnlySpan, when the file does not exist. public static ReadOnlySpan LoadKey(string filename) => !File.Exists(filename) ? ReadOnlySpan.Empty : File.ReadAllBytes(filename); + /// + /// Loads a key (public or private key) from a file. + /// + /// The entire path to the corresponding file. + /// The desired key. Returns null, when the file does not exist. + public static async Task LoadKeyAsync(string filename) => !File.Exists(filename) ? null : await File.ReadAllBytesAsync(filename); + /// /// Signs a message with the given private and public keys. /// @@ -101,6 +130,66 @@ namespace Ed25519 } } + /// + /// Signs a message with the given private and public keys. + /// + /// The message to sign. + /// The desired private key. + /// The corresponding public key. + /// The derived signature. It's length is 64 bytes. + public static async Task SignAsync(byte[] message, byte[] privateKey, byte[] publicKey) + { + if (privateKey.Length != 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}."); + + var privateKeyHash = await privateKey.ComputeHashAsync(); + var privateKeyBits = Constants.TWO_POW_BIT_LENGTH_MINUS_TWO; + for (var i = 3; i < Constants.BIT_LENGTH - 2; i++) + { + var bit = privateKeyHash.GetBit(i); + if (bit != 0) + { + privateKeyBits += Constants.TWO_POW_CACHE[i]; + } + } + + BigInteger r; + await using (var rSub = new MemoryStream((Constants.BIT_LENGTH / 8) + message.Length)) + { + await rSub.WriteAsync(privateKeyHash[(Constants.BIT_LENGTH / 8)..]); + await rSub.WriteAsync(message); + await rSub.FlushAsync(); + + r = await rSub.HashIntAsync(); + } + + var bigR = Constants.B.ScalarMul(r); + + BigInteger s; + var encodedBigR = bigR.EncodePoint().ToArray(); + await using (var sTemp = new MemoryStream(encodedBigR.Length + publicKey.Length + message.Length)) + { + await sTemp.WriteAsync(encodedBigR); + await sTemp.WriteAsync(publicKey); + await sTemp.WriteAsync(message); + await sTemp.FlushAsync(); + + s = (r + await sTemp.HashIntAsync() * privateKeyBits).Mod(Constants.L); + } + + await using (var nOut = new MemoryStream(64)) + { + await nOut.WriteAsync(encodedBigR); + await nOut.WriteAsync(s.EncodeInt().ToArray()); + await nOut.FlushAsync(); + + return nOut.ToArray(); + } + } + /// /// Validates a given signature by means of the given public key. /// @@ -141,5 +230,46 @@ namespace Ed25519 return ra.X.Equals(rb.X) && ra.Y.Equals(rb.Y); } + + /// + /// Validates a given signature by means of the given public key. + /// + /// The signature to validate. + /// The corresponding message. + /// The used public key. + /// Returns true when the combination of signature + message is valid. + public static async Task ValidateAsync(byte[] signature, byte[] message, byte[] publicKey) + { + if (signature.Length != Constants.BIT_LENGTH / 4) + throw new ArgumentException($"Signature length is wrong. Got {signature.Length} instead of {Constants.BIT_LENGTH / 4}."); + + if (publicKey.Length != Constants.BIT_LENGTH / 8) + throw new ArgumentException($"Public key length is wrong. Got {publicKey.Length} instead of {Constants.BIT_LENGTH / 8}."); + + var signatureSliceLeft = signature[..(Constants.BIT_LENGTH / 8)]; + var pointSignatureLeft = EdPoint.DecodePoint(signatureSliceLeft); + var pointPublicKey = EdPoint.DecodePoint(publicKey); + + var signatureSliceRight = signature[(Constants.BIT_LENGTH / 8)..]; + var signatureRight = signatureSliceRight.DecodeInt(); + var encodedSignatureLeftPoint = pointSignatureLeft.EncodePoint().ToArray(); + + BigInteger h; + await using (var sTemp = new MemoryStream(encodedSignatureLeftPoint.Length + publicKey.Length + message.Length)) + { + await sTemp.WriteAsync(encodedSignatureLeftPoint); + await sTemp.WriteAsync(publicKey); + await sTemp.WriteAsync(message); + await sTemp.FlushAsync(); + + h = await sTemp.HashIntAsync(); + } + + var ra = Constants.B.ScalarMul(signatureRight); + var ah = pointPublicKey.ScalarMul(h); + var rb = pointSignatureLeft.Edwards(ah); + + return ra.X.Equals(rb.X) && ra.Y.Equals(rb.Y); + } } } diff --git a/Ed25519/Tests/SignerTests.cs b/Ed25519/Tests/SignerTests.cs index 6a876a1..2f1b657 100644 --- a/Ed25519/Tests/SignerTests.cs +++ b/Ed25519/Tests/SignerTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Security.Cryptography; using System.Text; +using System.Threading.Tasks; using NUnit.Framework; namespace Ed25519 @@ -191,7 +192,7 @@ namespace Ed25519 } [Test] - public void TestWritingKeys() + public void TestWritingAndLoadingKeys() { var tempFilePrivate = Path.GetTempFileName(); var tempFilePublic = Path.GetTempFileName(); @@ -231,6 +232,22 @@ namespace Ed25519 Assert.That(publicKeyFromDecrypted.ToArray(), Is.Not.EqualTo(publicKeyFromEncrypted.ToArray())); } + [Test] + public void TestExtractPublicKeyFromTooShortPrivateKey() + { + var privateKey= Signer.GeneratePrivateKey()[..6]; // Six first bytes from private key! + var publicKey = privateKey.ExtractPublicKey(); + Assert.That(publicKey == ReadOnlySpan.Empty, Is.True); + } + + [Test] + public async Task TestExtractPublicKeyFromTooShortPrivateKeyAsync() + { + var privateKey = (await Signer.GeneratePrivateKeyAsync())[..6]; // Six first bytes from private key! + var publicKey = await privateKey.ExtractPublicKeyAsync(); + Assert.That(publicKey, Is.Null); + } + [Test] public void TestSigningBehaviourPrivateKeyWithPassword() { @@ -308,6 +325,20 @@ namespace Ed25519 Assert.That(true); } + [Test] + public void TestLoadingNotExistingKey() + { + var key = Signer.LoadKey(Guid.NewGuid().ToString()); + Assert.That(key == ReadOnlySpan.Empty, Is.True); + } + + [Test] + public async Task TestLoadingNotExistingKeyAsync() + { + var key = await Signer.LoadKeyAsync(Guid.NewGuid().ToString()); + Assert.That(key, Is.Null); + } + // See https://tools.ietf.org/html/rfc8032#section-7.1 [Test] public void TestRFC8032Test01EmptyMessage()