Implement KeyManager (functions will be implemented on demand)
MODIFY
MagicalCryptoWallet.Tests/KeyManagementTests.cs
MagicalCryptoWallet.Tests/KeyManagementTests.cs
@@ -98,16 +98,8 @@ namespace MagicalCryptoWallet.Tests
string password = "password";
var manager = KeyManager.CreateNew(out Mnemonic mnemonic, password);
- var filePath = "tmpWalletDir/tmpWallet.json";
- var dir = Path.GetDirectoryName(filePath);
- if (File.Exists(filePath))
- {
- File.Delete(filePath);
- }
- if(Directory.Exists(dir))
- {
- Directory.Delete(dir);
- }
+ var filePath = "WalletDir/Wallet.json";
+ DeleteFileAndDirectoryIfExists(filePath);
Assert.Throws<FileNotFoundException>(() => KeyManager.FromFile(filePath));
@@ -115,6 +107,17 @@ namespace MagicalCryptoWallet.Tests
manager.ToFile(filePath); // assert it doesn't throw
+ var random = new Random();
+
+ for (int i = 0; i < 1000; i++)
+ {
+ var isInternal = random.Next(2) == 0;
+ var label = RandomString.Generate(21);
+ var keyState = (KeyState)random.Next(3);
+ manager.GenerateNewKey(label, keyState, isInternal);
+ }
+ manager.ToFile(filePath);
+
Assert.True(File.Exists(filePath));
var sameManager = KeyManager.FromFile(filePath);
@@ -124,14 +127,52 @@ namespace MagicalCryptoWallet.Tests
Assert.Equal(manager.MasterPubKey, sameManager.MasterPubKey);
Assert.Equal(manager.ExtPubKey, sameManager.ExtPubKey);
+ DeleteFileAndDirectoryIfExists(filePath);
+ }
+
+ [Fact]
+ public void CanGenerateKeys()
+ {
+ string password = "password";
+ var manager = KeyManager.CreateNew(out Mnemonic mnemonic, password);
+
+ var random = new Random();
+
+ var k1 = manager.GenerateNewKey("", KeyState.Clean, true);
+ var k2 = manager.GenerateNewKey(null, KeyState.Clean, true);
+ Assert.Equal("", k1.Label);
+ Assert.Equal("", k2.Label);
+
+ for (int i = 0; i < 1000; i++)
+ {
+ var isInternal = random.Next(2) == 0;
+ var label = RandomString.Generate(21);
+ var keyState = (KeyState)random.Next(3);
+ var generatedKey = manager.GenerateNewKey(label, keyState, isInternal);
+
+ Assert.Equal(isInternal, generatedKey.IsInternal());
+ Assert.Equal(label, generatedKey.Label);
+ Assert.Equal(keyState, generatedKey.KeyState);
+ Assert.StartsWith("44'/0'/0'", generatedKey.FullKeyPath.ToString());
+ }
+ }
+
+ private static void DeleteFileAndDirectoryIfExists(string filePath)
+ {
+ var dir = Path.GetDirectoryName(filePath);
+
if (File.Exists(filePath))
{
File.Delete(filePath);
}
+
if (Directory.Exists(dir))
+ {
+ if(Directory.GetFiles(dir).Length == 0)
{
Directory.Delete(dir);
}
}
}
}
+}

MODIFY
MagicalCryptoWallet.Tests/MagicalCryptoWallet.Tests.csproj
MagicalCryptoWallet.Tests/MagicalCryptoWallet.Tests.csproj
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<NoWarn>1701;1702;1705;1591</NoWarn>
+ <ApplicationIcon />
+ <OutputType>Library</OutputType>
+ <StartupObject />
</PropertyGroup>
<ItemGroup>

MODIFY
MagicalCryptoWallet/KeyManagement/KeyManager.cs
MagicalCryptoWallet/KeyManagement/KeyManager.cs
@@ -6,6 +6,7 @@ using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
namespace MagicalCryptoWallet.KeyManagement
@@ -15,15 +16,24 @@ namespace MagicalCryptoWallet.KeyManagement
{
[JsonProperty(Order = 1)]
[JsonConverter(typeof(BitcoinEncryptedSecretNoECConverter))]
- public BitcoinEncryptedSecretNoEC EncryptedSecret { get; private set; }
+ public BitcoinEncryptedSecretNoEC EncryptedSecret { get; }
[JsonProperty(Order = 2)]
[JsonConverter(typeof(ByteArrayConverter))]
- public byte[] ChainCode { get; private set; }
+ public byte[] ChainCode { get; }
[JsonProperty(Order = 3)]
[JsonConverter(typeof(PubKeyConverter))]
- public PubKey MasterPubKey { get; private set; }
+ public PubKey MasterPubKey { get; }
+
+ [JsonProperty(Order = 4)]
+ private List<HdPubKey> HdPubKeys { get; }
+
+ // BIP44-ish derivation scheme
+ // m / purpose' / coin_type' / account' / change / address_index
+ private KeyPath _accountKeyPath;
+
+ private readonly object HdPubKeysLock;
private ExtPubKey _extPubKey;
public ExtPubKey ExtPubKey
@@ -42,6 +52,9 @@ namespace MagicalCryptoWallet.KeyManagement
public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, PubKey masterPubKey)
{
_extPubKey = null;
+ HdPubKeys = new List<HdPubKey>();
+ HdPubKeysLock = new object();
+ _accountKeyPath = new KeyPath("m/44'/0'/0'");
EncryptedSecret = Guard.NotNull(nameof(encryptedSecret), encryptedSecret);
ChainCode = Guard.NotNull(nameof(chainCode), chainCode);
@@ -51,6 +64,9 @@ namespace MagicalCryptoWallet.KeyManagement
public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, string password)
{
_extPubKey = null;
+ HdPubKeys = new List<HdPubKey>();
+ HdPubKeysLock = new object();
+ _accountKeyPath = new KeyPath("m/44'/0'/0'");
if (password == null)
{
@@ -59,7 +75,9 @@ namespace MagicalCryptoWallet.KeyManagement
EncryptedSecret = Guard.NotNull(nameof(encryptedSecret), encryptedSecret);
ChainCode = Guard.NotNull(nameof(chainCode), chainCode);
- MasterPubKey = encryptedSecret.GetKey(password).PubKey;
+ var extKey = new ExtKey(encryptedSecret.GetKey(password), chainCode);
+
+ MasterPubKey = extKey.Derive(_accountKeyPath).PrivateKey.PubKey;
}
public static KeyManager CreateNew(out Mnemonic mnemonic, string password)
@@ -115,5 +133,43 @@ namespace MagicalCryptoWallet.KeyManagement
string jsonString = File.ReadAllText(filePath, Encoding.UTF8);
return JsonConvert.DeserializeObject<KeyManager>(jsonString);
}
+
+ public HdPubKey GenerateNewKey(string label, KeyState keyState, bool isInternal)
+ {
+ // BIP44-ish derivation scheme
+ // m / purpose' / coin_type' / account' / change / address_index
+ lock (HdPubKeysLock)
+ {
+ var change = isInternal ? 1 : 0;
+
+ IEnumerable<HdPubKey> relevantHdPubKeys;
+ if (isInternal)
+ {
+ relevantHdPubKeys = HdPubKeys.Where(x => x.IsInternal());
+ }
+ else
+ {
+ relevantHdPubKeys = HdPubKeys.Where(x => !x.IsInternal());
+ }
+
+ KeyPath path;
+ if (relevantHdPubKeys.Count() == 0)
+ {
+ path = new KeyPath($"{change}/0");
+ }
+ else
+ {
+ path = relevantHdPubKeys.OrderBy(x => x.GetIndex()).Last().GetNonHardenedKeyPath().Increment();
+ }
+
+ var fullPath = _accountKeyPath.Derive(path);
+ var pubKey = ExtPubKey.Derive(path).PubKey;
+
+ var hdPubKey = new HdPubKey(pubKey, fullPath, label, keyState);
+ HdPubKeys.Add(hdPubKey);
+
+ return hdPubKey;
+ }
+ }
}
}

Implement KeyManager (functions will be implemented on demand)