diff --git a/Substrate.NetApi.Test/Extensions/BytesExtensionTest.cs b/Substrate.NetApi.Test/Extensions/BytesExtensionTest.cs new file mode 100644 index 0000000..7485c14 --- /dev/null +++ b/Substrate.NetApi.Test/Extensions/BytesExtensionTest.cs @@ -0,0 +1,62 @@ +using NUnit.Framework; +using Substrate.NetApi.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Substrate.NetApi.Test.Extensions +{ + public class BytesExtensionTest + { + [Test] + public void BytesFixLength_WhenNoChangeNeeded_ReturnsOriginalArray() + { + byte[] originalBytes = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + int bitLength = originalBytes.Length * 8; + + byte[] result = originalBytes.BytesFixLength(bitLength); + + Assert.That(originalBytes, Is.EqualTo(result)); + } + + [Test] + public void BytesFixLength_WhenShorter_WithAtStart_True_PrependsZeros() + { + byte[] originalBytes = new byte[] { 0x01, 0x02 }; + int bitLength = 32; // 4 bytes + bool atStart = true; + + byte[] result = originalBytes.BytesFixLength(bitLength, atStart); + + byte[] expected = new byte[] { 0x01, 0x02, 0x00, 0x00 }; + Assert.That(expected, Is.EqualTo(result)); + } + + [Test] + public void BytesFixLength_WhenShorter_WithAtStart_False_AppendsZeros() + { + byte[] originalBytes = new byte[] { 0x01, 0x02 }; + int bitLength = 32; // 4 bytes + bool atStart = false; + + byte[] result = originalBytes.BytesFixLength(bitLength, atStart); + + byte[] expected = new byte[] { 0x00, 0x00, 0x01, 0x02 }; + Assert.That(expected, Is.EqualTo(result)); + } + + [Test] + public void BytesFixLength_WhenLonger_TruncatesArray() + { + byte[] originalBytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + int bitLength = 24; // 3 bytes + + byte[] result = originalBytes.BytesFixLength(bitLength); + + byte[] expected = new byte[] { 0x01, 0x02, 0x03 }; + Assert.That(expected, Is.EqualTo(result)); + } + } +} diff --git a/Substrate.NetApi.Test/Extensions/HexadecimalValidatorTests.cs b/Substrate.NetApi.Test/Extensions/HexadecimalValidatorTests.cs new file mode 100644 index 0000000..bbd6d01 --- /dev/null +++ b/Substrate.NetApi.Test/Extensions/HexadecimalValidatorTests.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using Substrate.NetApi.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Substrate.NetApi.Test.Extensions +{ + public class HexadecimalValidatorTests + { + [Test] + [TestCase("", false)] + [TestCase("1234567890ABCDEF", true)] + [TestCase("0x1234567890ABCDEF", true)] + [TestCase("0x1234567890ABCDEF", true)] + [TestCase("1A2F3D", true)] + [TestCase("abcdef", true)] + [TestCase("0Xabcdef", true)] + [TestCase("12G", false)] + [TestCase("1X2", false)] + [TestCase(" ", false)] + [TestCase(null, false)] + public void IsHexadecimal_ValidatingHexadecimalStrings(string input, bool expected) + { + bool result = input.IsHex(); + Assert.That(expected, Is.EqualTo(result)); + } + } +} diff --git a/Substrate.NetApi/Extensions/BytesExtension.cs b/Substrate.NetApi/Extensions/BytesExtension.cs new file mode 100644 index 0000000..f4e37a8 --- /dev/null +++ b/Substrate.NetApi/Extensions/BytesExtension.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Substrate.NetApi.Extensions +{ + public static class BytesExtension + { + private static readonly RandomNumberGenerator RandomGenerator = RandomNumberGenerator.Create(); + + /// + /// Load a byte array with random bytes + /// + /// + /// + public static byte[] Populate(this byte[] data) + { + RandomGenerator.GetBytes(data); + return data; + } + + public static byte[] BytesFixLength(this byte[] value, int bitLength = -1, bool atStart = false) + { + int byteLength = (bitLength == -1) ? value.Length : (int)Math.Ceiling(bitLength / 8.0); + + if (value.Length == byteLength) + { + return value; + } + + if (value.Length > byteLength) + { + return value.Take(byteLength).ToArray(); + } + + byte[] result = new byte[byteLength]; + int copyIndex = atStart ? 0 : byteLength - value.Length; + + Array.Copy(value, 0, result, copyIndex, value.Length); + + return result; + } + } +} diff --git a/Substrate.NetApi/Extensions/StringExtension.cs b/Substrate.NetApi/Extensions/StringExtension.cs new file mode 100644 index 0000000..4c8148d --- /dev/null +++ b/Substrate.NetApi/Extensions/StringExtension.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Substrate.NetApi.Extensions +{ + public static class StringExtension + { + public static byte[] ToBytes(this string value) + { + return Encoding.UTF8.GetBytes(value); + } + + public static bool IsHex(this string value) + { + if (string.IsNullOrEmpty(value)) + return false; + + if (value.ToLower().StartsWith("0x")) + value = value.Remove(0, 2); + + return value.Length % 2 == 0 && IsHexCharacters(value); + } + + private static bool IsHexCharacters(string input) + { + foreach (char c in input) + { + if (!IsHexDigit(c)) + { + return false; + } + } + return true; + } + + private static bool IsHexDigit(char c) + { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + } + } +} diff --git a/Substrate.NetApi/Mnemonic.cs b/Substrate.NetApi/Mnemonic.cs index 068d825..20553a7 100644 --- a/Substrate.NetApi/Mnemonic.cs +++ b/Substrate.NetApi/Mnemonic.cs @@ -7,6 +7,7 @@ using Substrate.NetApi.BIP39; using Substrate.NetApi.Model.Types; using Schnorrkel.Keys; +using Substrate.NetApi.Extensions; namespace Substrate.NetApi { @@ -304,405 +305,62 @@ private static Wordlist GetWordlist(BIP39Wordlist language) throw new Exception($"Unknown {language} in BIP39 implementation!"); } } - } - //namespace System.Security.Cryptography - //{ - // /// - // /// This is a copy of Rfc28989DeiveBytes from corefx, since .NET Standard 2.0 leaves out the version - // /// where you can specify the HMAC to use I had to copy this version in and use it instead. - // /// - // public class Rfc2898DeriveBytesExtended : DeriveBytes - // { - // private const int MinimumSaltSize = 8; - - // private readonly byte[] _password; - // private byte[] _salt; - // private uint _iterations; - // private HMAC _hmac; - // private int _blockSize; - - // private byte[] _buffer; - // private uint _block; - // private int _startIndex; - // private int _endIndex; - - // public HashAlgorithmName HashAlgorithm { get; } - - // public Rfc2898DeriveBytesExtended(byte[] password, byte[] salt, int iterations) - // : this(password, salt, iterations, HashAlgorithmName.SHA1) - // { - // } - - // public Rfc2898DeriveBytesExtended(byte[] password, byte[] salt, int iterations, HashAlgorithmName hashAlgorithm) - // { - // if (salt == null) - // throw new ArgumentNullException(nameof(salt)); - // if (salt.Length < MinimumSaltSize) - // throw new ArgumentException("Salt is not at least eight bytes.", nameof(salt)); - // if (iterations <= 0) - // throw new ArgumentOutOfRangeException(nameof(iterations), "Positive number required."); - // if (password == null) - // throw new NullReferenceException(); // This "should" be ArgumentNullException but for compat, we throw NullReferenceException. - - // _salt = salt.CloneByteArray(); - // _iterations = (uint)iterations; - // _password = password.CloneByteArray(); - // HashAlgorithm = hashAlgorithm; - // _hmac = OpenHmac(); - // // _blockSize is in bytes, HashSize is in bits. - // _blockSize = _hmac.HashSize >> 3; - - // Initialize(); - // } - - // public Rfc2898DeriveBytesExtended(string password, byte[] salt) - // : this(password, salt, 1000) - // { - // } - - // public Rfc2898DeriveBytesExtended(string password, byte[] salt, int iterations) - // : this(password, salt, iterations, HashAlgorithmName.SHA1) - // { - // } - - // public Rfc2898DeriveBytesExtended(string password, byte[] salt, int iterations, HashAlgorithmName hashAlgorithm) - // : this(Encoding.UTF8.GetBytes(password), salt, iterations, hashAlgorithm) - // { - // } - - // public Rfc2898DeriveBytesExtended(string password, int saltSize) - // : this(password, saltSize, 1000) - // { - // } - - // public Rfc2898DeriveBytesExtended(string password, int saltSize, int iterations) - // : this(password, saltSize, iterations, HashAlgorithmName.SHA1) - // { - // } - - // public Rfc2898DeriveBytesExtended(string password, int saltSize, int iterations, HashAlgorithmName hashAlgorithm) - // { - // if (saltSize < 0) - // throw new ArgumentOutOfRangeException(nameof(saltSize), "Non-negative number required."); - // if (saltSize < MinimumSaltSize) - // throw new ArgumentException("Salt is not at least eight bytes.", nameof(saltSize)); - // if (iterations <= 0) - // throw new ArgumentOutOfRangeException(nameof(iterations), "Positive number required."); - - // _salt = Helpers.GenerateRandom(saltSize); - // _iterations = (uint)iterations; - // _password = Encoding.UTF8.GetBytes(password); - // HashAlgorithm = hashAlgorithm; - // _hmac = OpenHmac(); - // // _blockSize is in bytes, HashSize is in bits. - // _blockSize = _hmac.HashSize >> 3; - - // Initialize(); - // } - - // public int IterationCount - // { - // get - // { - // return (int)_iterations; - // } - - // set - // { - // if (value <= 0) - // throw new ArgumentOutOfRangeException(nameof(value), "Positive number required."); - // _iterations = (uint)value; - // Initialize(); - // } - // } - - // public byte[] Salt - // { - // get - // { - // return _salt.CloneByteArray(); - // } - - // set - // { - // if (value == null) - // throw new ArgumentNullException(nameof(value)); - // if (value.Length < MinimumSaltSize) - // throw new ArgumentException("Salt is not at least eight bytes."); - // _salt = value.CloneByteArray(); - // Initialize(); - // } - // } - - // protected override void Dispose(bool disposing) - // { - // if (disposing) - // { - // if (_hmac != null) - // { - // _hmac.Dispose(); - // _hmac = null; - // } - - // if (_buffer != null) - // Array.Clear(_buffer, 0, _buffer.Length); - // if (_password != null) - // Array.Clear(_password, 0, _password.Length); - // if (_salt != null) - // Array.Clear(_salt, 0, _salt.Length); - // } - // base.Dispose(disposing); - // } - - // public override byte[] GetBytes(int cb) - // { - // Debug.Assert(_blockSize > 0); - - // if (cb <= 0) - // throw new ArgumentOutOfRangeException(nameof(cb), "Positive number required."); - // byte[] password = new byte[cb]; - - // int offset = 0; - // int size = _endIndex - _startIndex; - // if (size > 0) - // { - // if (cb >= size) - // { - // Buffer.BlockCopy(_buffer, _startIndex, password, 0, size); - // _startIndex = _endIndex = 0; - // offset += size; - // } - // else - // { - // Buffer.BlockCopy(_buffer, _startIndex, password, 0, cb); - // _startIndex += cb; - // return password; - // } - // } - - // Debug.Assert(_startIndex == 0 && _endIndex == 0, "Invalid start or end index in the internal buffer."); - - // while (offset < cb) - // { - // byte[] T_block = Func(); - // int remainder = cb - offset; - // if (remainder > _blockSize) - // { - // Buffer.BlockCopy(T_block, 0, password, offset, _blockSize); - // offset += _blockSize; - // } - // else - // { - // Buffer.BlockCopy(T_block, 0, password, offset, remainder); - // offset += remainder; - // Buffer.BlockCopy(T_block, remainder, _buffer, _startIndex, _blockSize - remainder); - // _endIndex += (_blockSize - remainder); - // return password; - // } - // } - // return password; - // } - - // public byte[] CryptDeriveKey(string algname, string alghashname, int keySize, byte[] rgbIV) - // { - // // If this were to be implemented here, CAPI would need to be used (not CNG) because of - // // unfortunate differences between the two. Using CNG would break compatibility. Since this - // // assembly currently doesn't use CAPI it would require non-trivial additions. - // // In addition, if implemented here, only Windows would be supported as it is intended as - // // a thin wrapper over the corresponding native API. - // // Note that this method is implemented in PasswordDeriveBytes (in the Csp assembly) using CAPI. - // throw new PlatformNotSupportedException(); - // } - - // public override void Reset() - // { - // Initialize(); - // } - - // private HMAC OpenHmac() - // { - // Debug.Assert(_password != null); - - // HashAlgorithmName hashAlgorithm = HashAlgorithm; - - // if (string.IsNullOrEmpty(hashAlgorithm.Name)) - // throw new CryptographicException("The hash algorithm name cannot be null or empty."); - - // if (hashAlgorithm == HashAlgorithmName.SHA1) - // return new HMACSHA1(_password); - // if (hashAlgorithm == HashAlgorithmName.SHA256) - // return new HMACSHA256(_password); - // if (hashAlgorithm == HashAlgorithmName.SHA384) - // return new HMACSHA384(_password); - // if (hashAlgorithm == HashAlgorithmName.SHA512) - // return new HMACSHA512(_password); - - // throw new CryptographicException($"{hashAlgorithm.Name} is not a known hash algorithm."); - // } - - // private void Initialize() - // { - // if (_buffer != null) - // Array.Clear(_buffer, 0, _buffer.Length); - // _buffer = new byte[_blockSize]; - // _block = 1; - // _startIndex = _endIndex = 0; - // } - - // // This function is defined as follows: - // // Func (S, i) = HMAC(S || i) ^ HMAC2(S || i) ^ ... ^ HMAC(iterations) (S || i) - // // where i is the block number. - // private byte[] Func() - // { - // byte[] temp = new byte[_salt.Length + sizeof(uint)]; - // Buffer.BlockCopy(_salt, 0, temp, 0, _salt.Length); - // Helpers.WriteInt(_block, temp, _salt.Length); - - // temp = _hmac.ComputeHash(temp); - - // byte[] ret = temp; - // for (int i = 2; i <= _iterations; i++) - // { - // temp = _hmac.ComputeHash(temp); - - // for (int j = 0; j < _blockSize; j++) - // { - // ret[j] ^= temp[j]; - // } - // } - - // // increment the block count. - // _block++; - // return ret; - // } - - // } - - // internal static class Helpers - // { - // public static byte[] CloneByteArray(this byte[] src) - // { - // if (src == null) - // { - // return null; - // } - - // return (byte[])(src.Clone()); - // } - - // public static KeySizes[] CloneKeySizesArray(this KeySizes[] src) - // { - // return (KeySizes[])(src.Clone()); - // } - - // public static bool UsesIv(this CipherMode cipherMode) - // { - // return cipherMode != CipherMode.ECB; - // } - - // public static byte[] GetCipherIv(this CipherMode cipherMode, byte[] iv) - // { - // if (cipherMode.UsesIv()) - // { - // if (iv == null) - // { - // throw new CryptographicException("The cipher mode specified requires that an initialization vector (IV) be used."); - // } - - // return iv; - // } - - // return null; - // } - - // public static bool IsLegalSize(this int size, KeySizes[] legalSizes) - // { - // for (int i = 0; i < legalSizes.Length; i++) - // { - // KeySizes currentSizes = legalSizes[i]; - - // // If a cipher has only one valid key size, MinSize == MaxSize and SkipSize will be 0 - // if (currentSizes.SkipSize == 0) - // { - // if (currentSizes.MinSize == size) - // return true; - // } - // else if (size >= currentSizes.MinSize && size <= currentSizes.MaxSize) - // { - // // If the number is in range, check to see if it's a legal increment above MinSize - // int delta = size - currentSizes.MinSize; - - // // While it would be unusual to see KeySizes { 10, 20, 5 } and { 11, 14, 1 }, it could happen. - // // So don't return false just because this one doesn't match. - // if (delta % currentSizes.SkipSize == 0) - // { - // return true; - // } - // } - // } - - // return false; - // } - - // public static byte[] GenerateRandom(int count) - // { - // byte[] buffer = new byte[count]; - // using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) - // { - // rng.GetBytes(buffer); - // } - - // return buffer; - // } - - // // encodes the integer i into a 4-byte array, in big endian. - // public static void WriteInt(uint i, byte[] arr, int offset) - // { - // unchecked - // { - // Debug.Assert(arr != null); - // Debug.Assert(arr.Length >= offset + sizeof(uint)); - - // arr[offset] = (byte)(i >> 24); - // arr[offset + 1] = (byte)(i >> 16); - // arr[offset + 2] = (byte)(i >> 8); - // arr[offset + 3] = (byte)i; - // } - // } - - // public static byte[] FixupKeyParity(this byte[] key) - // { - // byte[] oddParityKey = new byte[key.Length]; - // for (int index = 0; index < key.Length; index++) - // { - // // Get the bits we are interested in - // oddParityKey[index] = (byte)(key[index] & 0xfe); - - // // Get the parity of the sum of the previous bits - // byte tmp1 = (byte)((oddParityKey[index] & 0xF) ^ (oddParityKey[index] >> 4)); - // byte tmp2 = (byte)((tmp1 & 0x3) ^ (tmp1 >> 2)); - // byte sumBitsMod2 = (byte)((tmp2 & 0x1) ^ (tmp2 >> 1)); - - // // We need to set the last bit in oddParityKey[index] to the negation - // // of the last bit in sumBitsMod2 - // if (sumBitsMod2 == 0) - // oddParityKey[index] |= 1; - // } - - // return oddParityKey; - // } - - // internal static void ConvertIntToByteArray(uint value, byte[] dest) - // { - // Debug.Assert(dest != null); - // Debug.Assert(dest.Length == 4); - // dest[0] = (byte)((value & 0xFF000000) >> 24); - // dest[1] = (byte)((value & 0xFF0000) >> 16); - // dest[2] = (byte)((value & 0xFF00) >> 8); - // dest[3] = (byte)(value & 0xFF); - // } - // } - - //} + public enum MnemonicSize + { + Words12, + Words15, + Words18, + Words21, + Words24 + } + + /// + /// Generate new mnemonic based on nb words + /// + /// + /// + /// + public static string[] GenerateMnemonic(MnemonicSize mnemonicSize, BIP39Wordlist bIP39Wordlist = BIP39Wordlist.English) + { + switch (mnemonicSize) + { + case MnemonicSize.Words12: + return MnemonicFromEntropy(new byte[16].Populate(), bIP39Wordlist); + case MnemonicSize.Words15: + return MnemonicFromEntropy(new byte[20].Populate(), bIP39Wordlist); + case MnemonicSize.Words18: + return MnemonicFromEntropy(new byte[24].Populate(), bIP39Wordlist); + case MnemonicSize.Words21: + return MnemonicFromEntropy(new byte[28].Populate(), bIP39Wordlist); + case MnemonicSize.Words24: + return MnemonicFromEntropy(new byte[32].Populate(), bIP39Wordlist); + } + + throw new InvalidOperationException("Invalid mnemonic size"); + } + + //public static byte[] MnemonicToMiniSecret(string mnemonic, string password, BIP39Wordlist bIP39Wordlist = BIP39Wordlist.English) + //{ + // if (!ValidateMnemonic(mnemonic, bIP39Wordlist)) + // { + // throw new InvalidOperationException("Invalid bip39 mnemonic specified"); + // } + + // return GetSecretKeyFromMnemonic(mnemonic, password, bIP39Wordlist); + //} + + public static bool ValidateMnemonic(string mnemonic, BIP39Wordlist bIP39Wordlist = BIP39Wordlist.English) + { + try + { + _ = MnemonicToEntropy(mnemonic, bIP39Wordlist); + return true; + } + catch (Exception) + { + return false; + } + } + } } \ No newline at end of file