diff --git a/Substrate.NET.Wallet.Test/EncodingTest.cs b/Substrate.NET.Wallet.Test/EncodingTest.cs new file mode 100644 index 0000000..057cd49 --- /dev/null +++ b/Substrate.NET.Wallet.Test/EncodingTest.cs @@ -0,0 +1,58 @@ +using NUnit.Framework; +using Substrate.NetApi; +using System; +using System.IO; + +namespace Substrate.NET.Wallet.Test +{ + public class EncodingTest + { + private Random _random; + + [SetUp] + public void Setup() + { + SystemInteraction.ReadData = f => File.ReadAllText(Path.Combine(Environment.CurrentDirectory, f)); + SystemInteraction.DataExists = f => File.Exists(Path.Combine(Environment.CurrentDirectory, f)); + SystemInteraction.ReadPersistent = f => File.ReadAllText(Path.Combine(Environment.CurrentDirectory, f)); + SystemInteraction.PersistentExists = f => File.Exists(Path.Combine(Environment.CurrentDirectory, f)); + SystemInteraction.Persist = (f, c) => File.WriteAllText(Path.Combine(Environment.CurrentDirectory, f), c); + + _random = new Random(); + } + + [Test] + public void EncryptionTest() + { + var origData = new byte[_random.Next(10, 500)]; + _random.NextBytes(origData); + + var salt = new byte[32]; + _random.NextBytes(salt); + + var encryptedData = Wallet.Encrypt(origData, "aA1234dd", salt); + var reprData = Wallet.Decrypt(encryptedData, "aA1234dd", salt); + + Assert.AreEqual(origData, reprData); + } + + [Test] + public void EncryptionSaltTest() + { + var address = "5CcaF7yE6YU67TyPHjSwd9DKiVBTAS2AktdxNG3DeLYs63gF"; + + var seed = new byte[16]; + _random.NextBytes(seed); + + var hash = new byte[16]; + _random.NextBytes(hash); + + var salt = Wallet.GetSalt(Utils.GetPublicKeyFrom(address), hash); + + var encodedData = Wallet.Encrypt(seed, "aA1234dd", salt); + var reprData = Wallet.Decrypt(encodedData, "aA1234dd", salt); + + Assert.AreEqual(seed, reprData); + } + } +} \ No newline at end of file diff --git a/Substrate.NET.Wallet.Test/Substrate.NET.Wallet.Test.csproj b/Substrate.NET.Wallet.Test/Substrate.NET.Wallet.Test.csproj index 853ee64..5afe054 100644 --- a/Substrate.NET.Wallet.Test/Substrate.NET.Wallet.Test.csproj +++ b/Substrate.NET.Wallet.Test/Substrate.NET.Wallet.Test.csproj @@ -16,6 +16,12 @@ + + + Always + + + Always diff --git a/Substrate.NET.Wallet.Test/WalletTest.cs b/Substrate.NET.Wallet.Test/WalletTest.cs index 7d5176d..788406a 100644 --- a/Substrate.NET.Wallet.Test/WalletTest.cs +++ b/Substrate.NET.Wallet.Test/WalletTest.cs @@ -1,12 +1,11 @@ using NUnit.Framework; -using Substrate.NET.Wallet; using Substrate.NetApi; using Substrate.NetApi.Model.Types; using System; using System.IO; using System.Text; -namespace SubstrateNetWalletTest +namespace Substrate.NET.Wallet.Test { public class WalletTest { @@ -49,14 +48,6 @@ public void LoadWalletFromFileTest() Wallet.Load(walletName1, out Wallet wallet1); Assert.True(wallet1.IsStored); Assert.False(wallet1.IsUnlocked); - Assert.AreEqual("Ed25519", - wallet1.FileStore.KeyType.ToString()); - Assert.AreEqual("5FfzQe73TTQhmSQCgvYocrr6vh1jJXEKB8xUB6tExfpKVCEZ", - Utils.GetAddressFrom(wallet1.FileStore.PublicKey)); - Assert.AreEqual("0x17E39AC65C894EC263396E9B8720D78A7A5FE0CB6C5C05DC32E756DF3D5D2D9622DBFDB41CE0C9067B810BB03E1DCE9C89CFC061FBB063B616FF91F3AA31498158632A35601C91DFEE5DA869D44FA8A4", - Utils.Bytes2HexString(wallet1.FileStore.EncryptedSeed)); - Assert.AreEqual("0x34F0627DB7C9BF1B580A597122622E95", - Utils.Bytes2HexString(wallet1.FileStore.Salt)); wallet1.Unlock("aA1234dd"); Assert.True(wallet1.IsUnlocked); @@ -64,18 +55,8 @@ public void LoadWalletFromFileTest() Wallet.Load(walletName2, out Wallet wallet2); Assert.True(wallet2.IsStored); Assert.False(wallet2.IsUnlocked); - Assert.AreEqual("Sr25519", - wallet2.FileStore.KeyType.ToString()); - Assert.AreEqual("5Fe24e21Ff5vRtuWa4ZNPv1EGQz1zBq1VtT8ojqfmzo9k11P", - Utils.GetAddressFrom(wallet2.FileStore.PublicKey)); - Assert.AreEqual("0xDA24A6B58BE083B58E3F011929B8A454B5FE9F1B91961DCC766D3E9F6AFE7AF96AAC1372DBA4537856F95C7E47A365C10590ACC092DB5AA95D6ECF5E06167B799AC6247178B7C51AC9B8F64C16602659", - Utils.Bytes2HexString(wallet2.FileStore.EncryptedSeed)); - Assert.AreEqual("0xD048477FCAD42D83402CDE3B2AF369D4", - Utils.Bytes2HexString(wallet2.FileStore.Salt)); wallet2.Unlock("aA1234dd"); Assert.True(wallet2.IsUnlocked); - Assert.AreEqual("0x6BED04FEE1504A49825339A68F601F7739FA7CEBF3B5E6A4A2476979F53CF40A112F6ED717AE8E8F5134C784A07DE6F3B2F7DA51D8117C566547A5038D4B3C27", - Utils.Bytes2HexString(wallet2.Account.PrivateKey)); } [Test] @@ -89,14 +70,6 @@ public void LoadWalletFromFileStore() var walletName2 = "dev_wallet3"; Wallet.Load(walletName2, wallet1.FileStore, out Wallet wallet2); - Assert.AreEqual("Ed25519", - wallet2.FileStore.KeyType.ToString()); - Assert.AreEqual("5FfzQe73TTQhmSQCgvYocrr6vh1jJXEKB8xUB6tExfpKVCEZ", - Utils.GetAddressFrom(wallet2.FileStore.PublicKey)); - Assert.AreEqual("0x17E39AC65C894EC263396E9B8720D78A7A5FE0CB6C5C05DC32E756DF3D5D2D9622DBFDB41CE0C9067B810BB03E1DCE9C89CFC061FBB063B616FF91F3AA31498158632A35601C91DFEE5DA869D44FA8A4", - Utils.Bytes2HexString(wallet2.FileStore.EncryptedSeed)); - Assert.AreEqual("0x34F0627DB7C9BF1B580A597122622E95", - Utils.Bytes2HexString(wallet2.FileStore.Salt)); wallet2.Unlock("aA1234dd"); Assert.True(wallet2.IsUnlocked); } diff --git a/Substrate.NET.Wallet.Test/dev_wallet1.json b/Substrate.NET.Wallet.Test/dev_wallet1.json index 9c3acf1..c21ed8b 100644 --- a/Substrate.NET.Wallet.Test/dev_wallet1.json +++ b/Substrate.NET.Wallet.Test/dev_wallet1.json @@ -1 +1 @@ -{"KeyType":0,"PublicKey":"n54BhZZyCmeLir4glnLK+jIGHT9uBADZ1unbIo7hWk4=","EncryptedSeed":"F+OaxlyJTsJjOW6bhyDXinpf4MtsXAXcMudW3z1dLZYi2/20HODJBnuBC7A+Hc6cic/AYfuwY7YW/5HzqjFJgVhjKjVgHJHf7l2oadRPqKQ=","Salt":"NPBifbfJvxtYCllxImIulQ=="} \ No newline at end of file +{"Encoded":"7Iy0sjfUMqXtZAXOIDJfkKxqZ3Pf0uo3frA6CgvrDVcvQ196eeFtOin2fknAwIzG/NdInwYLxGTGNTKN3kSsQhHexPWFcEaD","Encoding":{"Content":["pkcs8","ed25519"],"Type":["scrypt","xsalsa20-poly1305"],"Version":"3"},"Address":"5HUSXsYAg9h9KaWHmb6r5FXypyR8V7hsdRxsRAcy5M968PD4","Meta":{"GenesisHash":"0x3160D66BC1D86500DBAC8AA57FB845CE","IsHardware":false,"Name":"SUBSTRATE","Tags":[],"WhenCreated":1696592070618}} \ No newline at end of file diff --git a/Substrate.NET.Wallet.Test/dev_wallet2.json b/Substrate.NET.Wallet.Test/dev_wallet2.json index e8dd052..34a148b 100644 --- a/Substrate.NET.Wallet.Test/dev_wallet2.json +++ b/Substrate.NET.Wallet.Test/dev_wallet2.json @@ -1 +1 @@ -{"KeyType":1,"PublicKey":"nh0WPDIVJFrGLIO2ecRI2LYNKEyneZbpEvGsb3LQCF4=","EncryptedSeed":"2iSmtYvgg7WOPwEZKbikVLX+nxuRlh3Mdm0+n2r+evlqrBNy26RTeFb5XH5Ho2XBBZCswJLbWqldbs9eBhZ7eZrGJHF4t8Uaybj2TBZgJlk=","Salt":"0EhHf8rULYNALN47KvNp1A=="} \ No newline at end of file +{"Encoded":"qVxFVCuaqvo/8ThK0uTpdxaqOaSuttmi333Q4xRMmEx6mdyo9WOmrrjf2/Tk+ZFB6vfD/ZK45BwdX2ALBHCHIP8y2p0ulP0Q","Encoding":{"Content":["pkcs8","sr25519"],"Type":["scrypt","xsalsa20-poly1305"],"Version":"3"},"Address":"5ELuscqHb44nDVyz5kHBLXQdBdp3hxJfGB8A6WQmpQtgf96z","Meta":{"GenesisHash":"0xEEF01B3AB74708E1F8ADC640B303C4FA","IsHardware":false,"Name":"SUBSTRATE","Tags":[],"WhenCreated":1696592071243}} \ No newline at end of file diff --git a/Substrate.NET.Wallet/FileStore.cs b/Substrate.NET.Wallet/FileStore.cs deleted file mode 100644 index 712a255..0000000 --- a/Substrate.NET.Wallet/FileStore.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Substrate.NetApi.Model.Types; - -namespace Substrate.NET.Wallet -{ - /// - /// Wallet File description. - /// - public class FileStore - { - /// - /// Initializes a new instance of the class. - /// - /// Type of the key. - /// The public key. - /// The encrypted seed. - /// The salt. - public FileStore(KeyType keyType, byte[] publicKey, byte[] encryptedSeed, byte[] salt) - { - KeyType = keyType; - PublicKey = publicKey; - EncryptedSeed = encryptedSeed; - Salt = salt; - } - - /// - /// Gets the type of the key. - /// - /// - /// The type of the key. - /// - public KeyType KeyType { get; } - - /// - /// Gets the public key. - /// - /// - /// The public key. - /// - public byte[] PublicKey { get; } - - /// - /// Gets the encrypted seed. - /// - /// - /// The encrypted seed. - /// - public byte[] EncryptedSeed { get; } - - /// - /// Gets the salt. - /// - /// - /// The salt. - /// - public byte[] Salt { get; } - } -} \ No newline at end of file diff --git a/Substrate.NET.Wallet/Model/EncodedData.cs b/Substrate.NET.Wallet/Model/EncodedData.cs new file mode 100644 index 0000000..57cc78f --- /dev/null +++ b/Substrate.NET.Wallet/Model/EncodedData.cs @@ -0,0 +1,10 @@ +namespace Substrate.NET.Wallet.Model +{ + public class EncodedData + { + public string Encoded { get; set; } + public EncodingInfo Encoding { get; set; } + public string Address { get; set; } + public Metadata Meta { get; set; } + } +} \ No newline at end of file diff --git a/Substrate.NET.Wallet/Model/EncodingInfo.cs b/Substrate.NET.Wallet/Model/EncodingInfo.cs new file mode 100644 index 0000000..ea3aa24 --- /dev/null +++ b/Substrate.NET.Wallet/Model/EncodingInfo.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Substrate.NET.Wallet.Model +{ + public class EncodingInfo + { + public List Content { get; set; } = new List(); + public List Type { get; set; } = new List(); + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/Substrate.NET.Wallet/Model/Metadata.cs b/Substrate.NET.Wallet/Model/Metadata.cs new file mode 100644 index 0000000..b4f4549 --- /dev/null +++ b/Substrate.NET.Wallet/Model/Metadata.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Substrate.NET.Wallet.Model +{ + public class Metadata + { + public string GenesisHash { get; set; } + public bool IsHardware { get; set; } + public string Name { get; set; } + public List Tags { get; set; } = new List(); + public long WhenCreated { get; set; } // This seems to be a Unix timestamp in milliseconds + } +} \ No newline at end of file diff --git a/Substrate.NET.Wallet/Substrate.NET.Wallet.csproj b/Substrate.NET.Wallet/Substrate.NET.Wallet.csproj index 7ccfeb8..ebc6a57 100644 --- a/Substrate.NET.Wallet/Substrate.NET.Wallet.csproj +++ b/Substrate.NET.Wallet/Substrate.NET.Wallet.csproj @@ -3,7 +3,7 @@ Substrate.NET.Wallet netstandard2.0;netstandard2.1;net6.0 - 1.0.5 + 1.0.6 BloGa Tech AG Cedric Decoster true @@ -22,6 +22,7 @@ + diff --git a/Substrate.NET.Wallet/Wallet.cs b/Substrate.NET.Wallet/Wallet.cs index 9632803..1bae165 100644 --- a/Substrate.NET.Wallet/Wallet.cs +++ b/Substrate.NET.Wallet/Wallet.cs @@ -2,14 +2,20 @@ using Schnorrkel; using Schnorrkel.Keys; using Serilog; +using Sodium; +using Substrate.NET.Wallet.Model; using Substrate.NetApi; using Substrate.NetApi.Model.Types; using Substrate.NetApi.Sign; using System; +using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; +[assembly: InternalsVisibleTo("Substrate.NET.Wallet.Test")] + namespace Substrate.NET.Wallet { /// @@ -29,7 +35,7 @@ public class Wallet public string FileName { get; private set; } - public FileStore FileStore { get; private set; } + public EncodedData FileStore { get; private set; } /// /// @@ -37,7 +43,7 @@ public class Wallet /// /// /// - private Wallet(Account account, string walletName, FileStore fileStore) + private Wallet(Account account, string walletName, EncodedData fileStore) { Account = account; FileName = walletName; @@ -77,36 +83,41 @@ public bool Unlock(string password, bool noCheck = false) Logger.Information("Unlock wallet."); - try + if (!Enum.TryParse(FileStore.Encoding.Content[1], true, out KeyType keyType)) { - var pswBytes = Encoding.UTF8.GetBytes(password); - - pswBytes = SHA256.Create().ComputeHash(pswBytes); + Logger.Warning("Couldn't parse key type defintion."); + return false; + } - var seed = ManagedAes.DecryptStringFromBytes_Aes(FileStore.EncryptedSeed, pswBytes, FileStore.Salt); + try + { + var publicKeyBytes = Utils.GetPublicKeyFrom(FileStore.Address); + var salt = GetSalt(Utils.GetPublicKeyFrom(FileStore.Address), Utils.HexToByteArray(FileStore.Meta.GenesisHash)); + var seed = Decrypt(FileStore.Encoded, password, salt); byte[] publicKey = null; byte[] privateKey = null; - switch (FileStore.KeyType) + + switch (keyType) { case KeyType.Ed25519: - Ed25519.KeyPairFromSeed(out publicKey, out privateKey, Utils.HexToByteArray(seed)); + Ed25519.KeyPairFromSeed(out publicKey, out privateKey, seed); break; case KeyType.Sr25519: - var miniSecret = new MiniSecret(Utils.HexToByteArray(seed), ExpandMode.Ed25519); + var miniSecret = new MiniSecret(seed, ExpandMode.Ed25519); var getPair = miniSecret.GetPair(); privateKey = getPair.Secret.ToBytes(); publicKey = getPair.Public.Key; break; } - if (!noCheck && !publicKey.SequenceEqual(FileStore.PublicKey)) + if (!noCheck && !publicKey.SequenceEqual(publicKeyBytes)) { throw new NotSupportedException("Public key check failed!"); } - Account = Account.Build(FileStore.KeyType, privateKey, publicKey); + Account = Account.Build(keyType, privateKey, publicKey); } catch (Exception e) { @@ -117,6 +128,14 @@ public bool Unlock(string password, bool noCheck = false) return true; } + internal static byte[] GetSalt(byte[] hash1, byte[] hash2) + { + byte[] concHash = new byte[hash1.Length + hash2.Length]; + Array.Copy(hash1, 0, concHash, 0, hash1.Length); + Array.Copy(hash2, 0, concHash, hash1.Length, hash2.Length); + return SHA256.Create().ComputeHash(concHash); + } + /// /// /// @@ -133,9 +152,15 @@ public bool Lock(string password, bool noCheck = false) Logger.Information("Lock wallet."); + if (!Enum.TryParse(FileStore.Encoding.Content[1], true, out KeyType keyType)) + { + Logger.Warning("Couldn't parse key type defintion."); + return false; + } + try { - Account = Account.Build(FileStore.KeyType, null, Account.Bytes); + Account = Account.Build(keyType, null, Account.Bytes); } catch (Exception e) { @@ -183,14 +208,22 @@ public static bool Load(string walletName, out Wallet wallet) } var walletFileName = ConcatWalletFileType(walletName); - if (!Caching.TryReadFile(walletFileName, out FileStore fileStore)) + if (!Caching.TryReadFile(walletFileName, out EncodedData fileStore)) { Logger.Warning("Failed to load wallet file '{walletFileName}'!", walletFileName); return false; } + if (!Enum.TryParse(fileStore.Encoding.Content[1], true, out KeyType keyType)) + { + Logger.Warning("Couldn't parse key type defintion."); + return false; + } + + var publicKeyBytes = Utils.HexToByteArray(fileStore.Address); + var newAccount = new Account(); - newAccount.Create(fileStore.KeyType, fileStore.PublicKey); + newAccount.Create(keyType, publicKeyBytes); wallet = new Wallet(newAccount, walletName, fileStore); @@ -198,13 +231,14 @@ public static bool Load(string walletName, out Wallet wallet) } /// - /// Load the wallet from file store object. + /// Load the wallet from the file store object. /// /// /// /// + /// /// - public static bool Load(string walletName, FileStore fileStore, out Wallet wallet) + public static bool Load(string walletName, EncodedData fileStore, out Wallet wallet, bool tryPersist = true) { wallet = null; @@ -214,8 +248,29 @@ public static bool Load(string walletName, FileStore fileStore, out Wallet walle return false; } + if (!Enum.TryParse(fileStore.Encoding.Content[1], true, out KeyType keyType)) + { + Logger.Warning("Couldn't parse key type defintion."); + return false; + } + + var publicKeyBytes = Utils.HexToByteArray(fileStore.Address); + var newAccount = new Account(); - newAccount.Create(fileStore.KeyType, fileStore.PublicKey); + newAccount.Create(keyType, publicKeyBytes); + + if (tryPersist) + { + try + { + Caching.Persist(Wallet.ConcatWalletFileType(walletName), fileStore); + } + catch (Exception e) + { + Logger.Warning("Failed to persist wallet file '{walletFileName}'! {error}", walletName, e); + return false; + } + } wallet = new Wallet(newAccount, walletName, fileStore); @@ -223,12 +278,16 @@ public static bool Load(string walletName, FileStore fileStore, out Wallet walle } /// - /// Creates the asynchronous. + /// Creates the from random. /// - /// The password. - /// Name of the wallet. + /// + /// + /// + /// + /// /// - public static bool CreateFromRandom(string password, KeyType keyType, string walletName, out Wallet wallet) + /// + public static bool CreateFromRandom(string password, KeyType keyType, string walletName, out Wallet wallet, bool tryPersist = true) { wallet = null; @@ -241,7 +300,7 @@ public static bool CreateFromRandom(string password, KeyType keyType, string wal if (!IsValidPassword(password)) { Logger.Warning( - "Password isn't is invalid, please provide a proper password. Minmimu eight size and must have upper, lower and digits."); + "Password is invalid, please provide a proper password. Minmimu eight size and must have upper, lower and digits."); return false; } @@ -253,9 +312,7 @@ public static bool CreateFromRandom(string password, KeyType keyType, string wal var memoryBytes = randomBytes.AsMemory(); - var pswBytes = Encoding.UTF8.GetBytes(password); - - var salt = memoryBytes.Slice(0, 16).ToArray(); + var hash = memoryBytes.Slice(0, 16).ToArray(); var seed = memoryBytes.Slice(16, 32).ToArray(); @@ -276,28 +333,167 @@ public static bool CreateFromRandom(string password, KeyType keyType, string wal throw new NotImplementedException($"KeyType {keyType} isn't implemented!"); } - pswBytes = SHA256.Create().ComputeHash(pswBytes); - - var encryptedSeed = - ManagedAes.EncryptStringToBytes_Aes( - Utils.Bytes2HexString(seed, Utils.HexStringFormat.Pure), pswBytes, salt); + var fileStore = GetFileStore(account.Value, account.KeyType, hash, seed, password); - var fileStore = new FileStore(keyType, account.Bytes, encryptedSeed, salt); - Caching.Persist(Wallet.ConcatWalletFileType(walletName), fileStore); + if (tryPersist) + { + try + { + Caching.Persist(Wallet.ConcatWalletFileType(walletName), fileStore); + } + catch (Exception e) + { + Logger.Warning("Failed to persist wallet file '{walletFileName}'! {error}", walletName, e); + return false; + } + } wallet = new Wallet(account, walletName, fileStore); + return !tryPersist || wallet.Save(password); + } + + private static EncodedData GetFileStore(string address, KeyType keyType, byte[] hash, byte[] seed, string password) + { + var salt = GetSalt(Utils.GetPublicKeyFrom(address), hash); + + return new EncodedData + { + Encoded = Encrypt(seed, password, salt), + Encoding = new Model.EncodingInfo + { + Content = new List { "pkcs8", keyType.ToString().ToLower() }, + Type = new List { "scrypt", "xsalsa20-poly1305" }, + Version = "3" + }, + Address = address, + Meta = new Metadata + { + GenesisHash = Utils.Bytes2HexString(hash), + IsHardware = false, + Name = "SUBSTRATE", + Tags = new List(), + WhenCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + } + }; + } + + private bool Save(string password) + { + if (!IsValidPassword(password)) + { + Logger.Warning( + "Password is invalid, please provide a proper password. Minmimu eight size and must have upper, lower and digits."); + return false; + } + + if (!IsUnlocked) + { + Logger.Warning("Unlock wallet first, before you store it."); + return false; + } + + Caching.Persist(Wallet.ConcatWalletFileType(FileName), FileStore); + return true; } /// - /// Creates the asynchronous. + /// Encrypts the specified data. /// - /// The password. - /// The mnemonic. - /// Name of the wallet. + /// + /// + /// + /// + public static string Encrypt(byte[] data, string password, byte[] salt) + { + // Derive the encryption key from the password using Scrypt + byte[] derivedKey = DeriveKeyUsingScrypt(password, salt, 32); // 32 bytes = 256 bits + + // Encrypt the content using the derived key and XSalsa20-Poly1305 + byte[] nonce; + byte[] encryptedDataBytes = EncryptUsingXSalsa20Poly1305(data, derivedKey, out nonce); + + // Combine nonce and encrypted data + byte[] combined = new byte[nonce.Length + encryptedDataBytes.Length]; + Array.Copy(nonce, 0, combined, 0, nonce.Length); + Array.Copy(encryptedDataBytes, 0, combined, nonce.Length, encryptedDataBytes.Length); + + return Convert.ToBase64String(combined); + } + + /// + /// Decrypts the specified encoded. + /// + /// + /// + /// /// - public static bool CreateFromMnemonic(string password, string mnemonic, KeyType keyType, Mnemonic.BIP39Wordlist bIP39Wordlist, string walletName, out Wallet wallet) + public static byte[] Decrypt(string encoded, string password, byte[] salt) + { + byte[] combined = Convert.FromBase64String(encoded); + + // Extract nonce and encrypted data + byte[] nonce = new byte[24]; + byte[] encryptedDataBytes = new byte[combined.Length - nonce.Length]; + Array.Copy(combined, 0, nonce, 0, nonce.Length); + Array.Copy(combined, nonce.Length, encryptedDataBytes, 0, encryptedDataBytes.Length); + + // Derive the encryption key from the password using Scrypt + byte[] derivedKey = DeriveKeyUsingScrypt(password, salt, 32); // 32 bytes = 256 bits + + // Decrypt the content using the derived key and XSalsa20-Poly1305 + return DecryptUsingXSalsa20Poly1305(encryptedDataBytes, derivedKey, nonce); + } + + /// + /// Derives the key using scrypt. + /// + /// + /// + /// + /// + private static byte[] DeriveKeyUsingScrypt(string password, byte[] salt, int keyLengthInBytes) + => PasswordHash.ScryptHashBinary(Encoding.UTF8.GetBytes(password), salt, PasswordHash.Strength.Medium, keyLengthInBytes); + + /// + /// Encrypts the using XSalsa20Poly1305. + /// + /// + /// + /// + /// + private static byte[] EncryptUsingXSalsa20Poly1305(byte[] plainText, byte[] key, out byte[] nonce) + { + nonce = SecretBox.GenerateNonce(); + return SecretBox.Create(plainText, nonce, key); + } + + /// + /// Decrypts the using XSalsa20Poly1305. + /// + /// + /// + /// + /// + private static byte[] DecryptUsingXSalsa20Poly1305(byte[] cipherText, byte[] key, byte[] nonce) + { + return SecretBox.Open(cipherText, nonce, key); + } + + /// + /// Creates the from mnemonic. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool CreateFromMnemonic(string password, string mnemonic, KeyType keyType, Mnemonic.BIP39Wordlist bIP39Wordlist, string walletName, out Wallet wallet, bool tryPersist = true) { wallet = null; @@ -341,18 +537,22 @@ public static bool CreateFromMnemonic(string password, string mnemonic, KeyType var memoryBytes = randomBytes.AsMemory(); - var pswBytes = Encoding.UTF8.GetBytes(password); - - var salt = memoryBytes.Slice(0, 16).ToArray(); + var hash = memoryBytes.Slice(0, 16).ToArray(); - pswBytes = SHA256.Create().ComputeHash(pswBytes); + var fileStore = GetFileStore(account.Value, account.KeyType, hash, seed, password); - var encryptedSeed = - ManagedAes.EncryptStringToBytes_Aes( - Utils.Bytes2HexString(seed, Utils.HexStringFormat.Pure), pswBytes, salt); - - var fileStore = new FileStore(keyType, account.Bytes, encryptedSeed, salt); - Caching.Persist(Wallet.ConcatWalletFileType(walletName), fileStore); + if (tryPersist) + { + try + { + Caching.Persist(Wallet.ConcatWalletFileType(walletName), fileStore); + } + catch (Exception e) + { + Logger.Warning("Failed to persist wallet file '{walletFileName}'! {error}", walletName, e); + return false; + } + } wallet = new Wallet(account, walletName, fileStore);