Skip to content

Commit

Permalink
Merge pull request #1376 from guggero/recover-advanced-options
Browse files Browse the repository at this point in the history
Add advanced options on Recover Wallet tab
  • Loading branch information
nopara73 committed May 8, 2019
2 parents 8f88f34 + 0e68127 commit 92b6085
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 31 deletions.
12 changes: 12 additions & 0 deletions WalletWasabi.Gui/Tabs/WalletManager/RecoverWalletView.xaml
Expand Up @@ -29,6 +29,18 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Spacing="16">
<CheckBox IsChecked="{Binding ShowAdvancedOptions, Mode=TwoWay}">
<TextBlock Text="Show Advanced Options" />
</CheckBox>
<StackPanel IsVisible="{Binding ShowAdvancedOptions}">
<controls:ExtendedTextBox Text="{Binding AccountKeyPath}" Watermark="Account Key Path" UseFloatingWatermark="True" />
<TextBlock Text="Note that, Wasabi can only monitor native SegWit (bech32) addresses." Classes="warningMessage" />
</StackPanel>
<StackPanel IsVisible="{Binding ShowAdvancedOptions}">
<controls:ExtendedTextBox Text="{Binding MinGapLimit}" Watermark="Minimum Gap Limit" UseFloatingWatermark="True" />
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</controls:GroupBox>
Expand Down
60 changes: 57 additions & 3 deletions WalletWasabi.Gui/Tabs/WalletManager/RecoverWalletViewModel.cs
@@ -1,4 +1,4 @@
using AvalonStudio.Extensibility;
using AvalonStudio.Extensibility;
using AvalonStudio.Shell;
using NBitcoin;
using ReactiveUI;
Expand All @@ -22,6 +22,9 @@ internal class RecoverWalletViewModel : CategoryViewModel
private string _mnemonicWords;
private string _walletName;
private string _validationMessage;
private bool _showAdvancedOptions;
private string _accountKeyPath;
private int _minGapLimit;
private ObservableCollection<SuggestionViewModel> _suggestions;

public RecoverWalletViewModel(WalletManagerViewModel owner) : base("Recover Wallet")
Expand All @@ -46,14 +49,31 @@ public RecoverWalletViewModel(WalletManagerViewModel owner) : base("Recover Wall
}
else if (string.IsNullOrWhiteSpace(MnemonicWords))
{
ValidationMessage = $"Recovery Words were not supplied.";
ValidationMessage = "Recovery Words were not supplied.";
}
else if (string.IsNullOrWhiteSpace(AccountKeyPath))
{
ValidationMessage = "The account key path is not valid.";
}
else if (MinGapLimit < KeyManager.AbsoluteMinGapLimit)
{
ValidationMessage = $"Min Gap Limit cannot be smaller than {KeyManager.AbsoluteMinGapLimit}.";
}
else if (MinGapLimit > 1_000_000)
{
ValidationMessage = $"Min Gap Limit cannot be larger than {1_000_000}.";
}
else if (!TryParseKeyPath(AccountKeyPath))
{
ValidationMessage = "The account key path is not a valid derivation path.";
}
else
{
KeyPath keyPath = KeyPath.Parse(AccountKeyPath);
try
{
var mnemonic = new Mnemonic(MnemonicWords);
KeyManager.Recover(mnemonic, Password, walletFilePath);
KeyManager.Recover(mnemonic, Password, walletFilePath, keyPath, MinGapLimit);
owner.SelectLoadWallet();
}
Expand Down Expand Up @@ -132,6 +152,24 @@ public int CaretIndex
set => this.RaiseAndSetIfChanged(ref _caretIndex, value);
}

public bool ShowAdvancedOptions
{
get => _showAdvancedOptions;
set => this.RaiseAndSetIfChanged(ref _showAdvancedOptions, value);
}

public string AccountKeyPath
{
get => _accountKeyPath;
set => this.RaiseAndSetIfChanged(ref _accountKeyPath, value);
}

public int MinGapLimit
{
get => _minGapLimit;
set => this.RaiseAndSetIfChanged(ref _minGapLimit, value);
}

public ReactiveCommand<Unit, Unit> RecoverCommand { get; }

public void OnTermsClicked()
Expand All @@ -157,6 +195,9 @@ public override void OnCategorySelected()
MnemonicWords = "";
WalletName = Utils.GetNextWalletName();
ValidationMessage = null;
ShowAdvancedOptions = false;
AccountKeyPath = $"m/{KeyManager.DefaultAccountKeyPath}";
MinGapLimit = KeyManager.AbsoluteMinGapLimit;
}

private void UpdateSuggestions(string words)
Expand Down Expand Up @@ -203,6 +244,19 @@ public void OnAddWord(string word)
Suggestions.Clear();
}

private bool TryParseKeyPath(string keyPath)
{
try
{
KeyPath.Parse(keyPath);
return true;
}
catch (FormatException)
{
return false;
}
}

private static IEnumerable<string> EnglishWords { get; } = Wordlist.English.GetWords();
}
}
9 changes: 7 additions & 2 deletions WalletWasabi.Tests/KeyManagementTests.cs
@@ -1,4 +1,4 @@
using NBitcoin;
using NBitcoin;
using System;
using System.IO;
using System.Security;
Expand Down Expand Up @@ -72,10 +72,15 @@ public void CanRecover()
Assert.Equal(manager.EncryptedSecret, sameManager.EncryptedSecret);
Assert.Equal(manager.ExtPubKey, sameManager.ExtPubKey);

var differentManager = KeyManager.Recover(mnemonic, "differentPassword");
var differentManager = KeyManager.Recover(mnemonic, "differentPassword", null, KeyPath.Parse("m/999'/999'/999'"), 55);
Assert.NotEqual(manager.ChainCode, differentManager.ChainCode);
Assert.NotEqual(manager.EncryptedSecret, differentManager.EncryptedSecret);
Assert.NotEqual(manager.ExtPubKey, differentManager.ExtPubKey);

differentManager.AssertCleanKeysIndexed();
var newKey = differentManager.GenerateNewKey("some-label", KeyState.Clean, true, false);
Assert.Equal(newKey.Index, differentManager.MinGapLimit);
Assert.Equal("999'/999'/999'/1/55", newKey.FullKeyPath.ToString());
}

[Fact]
Expand Down
39 changes: 39 additions & 0 deletions WalletWasabi/JsonConverters/KeyPathJsonConverter.cs
@@ -0,0 +1,39 @@
using NBitcoin;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace WalletWasabi.JsonConverters
{
public class KeyPathJsonConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(KeyPath);
}

/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var s = (string)reader.Value;
if (string.IsNullOrWhiteSpace(s))
{
return null;
}
var kp = KeyPath.Parse(s.Trim());

return kp;
}

/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var kp = (KeyPath)value;

var s = kp.ToString();
writer.WriteValue(s);
}
}
}
13 changes: 3 additions & 10 deletions WalletWasabi/KeyManagement/HdPubKey.cs
@@ -1,5 +1,4 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
using System;
using WalletWasabi.Helpers;
Expand Down Expand Up @@ -125,15 +124,9 @@ public void SetKeyState(KeyState state, KeyManager kmToFile = null)

public override int GetHashCode() => HashCode;

public static bool operator ==(HdPubKey x, HdPubKey y)
{
return x?.PubKeyHash == y?.PubKeyHash;
}
public static bool operator ==(HdPubKey x, HdPubKey y) => x?.PubKeyHash == y?.PubKeyHash;

public static bool operator !=(HdPubKey x, HdPubKey y)
{
return !(x == y);
}
public static bool operator !=(HdPubKey x, HdPubKey y) => !(x == y);

#endregion Equality
}
Expand Down
41 changes: 25 additions & 16 deletions WalletWasabi/KeyManagement/KeyManager.cs
Expand Up @@ -40,11 +40,15 @@ public class KeyManager
public int? MinGapLimit { get; private set; }

[JsonProperty(Order = 7)]
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath AccountKeyPath { get; private set; }

[JsonProperty(Order = 8)]
private BlockchainState BlockchainState { get; }

private object BlockchainStateLock { get; }

[JsonProperty(Order = 8)]
[JsonProperty(Order = 9)]
private List<HdPubKey> HdPubKeys { get; }

private object HdPubKeysLock { get; }
Expand All @@ -60,7 +64,7 @@ public class KeyManager
// BIP84-ish derivation scheme
// m / purpose' / coin_type' / account' / change / address_index
// https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
public static readonly KeyPath AccountKeyPath = new KeyPath("m/84'/0'/0'");
public static readonly KeyPath DefaultAccountKeyPath = new KeyPath("m/84'/0'/0'");

public string FilePath { get; private set; }
private object ToFileLock { get; }
Expand All @@ -69,10 +73,10 @@ public class KeyManager
public bool IsHardwareWallet => EncryptedSecret is null && MasterFingerprint != null;
public HardwareWalletInfo HardwareWalletInfo { get; set; }

private const int AbsoluteMinGapLimit = 21;
public const int AbsoluteMinGapLimit = 21;

[JsonConstructor]
public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, HDFingerprint? masterFingerprint, ExtPubKey extPubKey, bool? passwordVerified, int? minGapLimit, BlockchainState blockchainState, string filePath = null)
public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, HDFingerprint? masterFingerprint, ExtPubKey extPubKey, bool? passwordVerified, int? minGapLimit, BlockchainState blockchainState, string filePath = null, KeyPath accountKeyPath = null)
{
HdPubKeys = new List<HdPubKey>();
HdPubKeyScriptBytes = new List<byte[]>();
Expand All @@ -88,17 +92,18 @@ public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode,
ExtPubKey = Guard.NotNull(nameof(extPubKey), extPubKey);

PasswordVerified = passwordVerified;
SetMinGaplimit(minGapLimit);
SetMinGapLimit(minGapLimit);

BlockchainState = blockchainState ?? new BlockchainState();
HardwareWalletInfo = null;
AccountKeyPath = accountKeyPath ?? DefaultAccountKeyPath;

SetFilePath(filePath);
ToFileLock = new object();
ToFile();
}

public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, string password, int minGapLimit = AbsoluteMinGapLimit, string filePath = null)
public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, string password, int minGapLimit = AbsoluteMinGapLimit, string filePath = null, KeyPath accountKeyPath = null)
{
HdPubKeys = new List<HdPubKey>();
HdPubKeyScriptBytes = new List<byte[]>();
Expand All @@ -115,13 +120,14 @@ public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode,
password = "";
}

SetMinGaplimit(minGapLimit);
SetMinGapLimit(minGapLimit);

EncryptedSecret = Guard.NotNull(nameof(encryptedSecret), encryptedSecret);
ChainCode = Guard.NotNull(nameof(chainCode), chainCode);
var extKey = new ExtKey(encryptedSecret.GetKey(password), chainCode);

MasterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
AccountKeyPath = accountKeyPath ?? DefaultAccountKeyPath;
ExtPubKey = extKey.Derive(AccountKeyPath).Neuter();

SetFilePath(filePath);
Expand All @@ -141,8 +147,9 @@ public static KeyManager CreateNew(out Mnemonic mnemonic, string password, strin
var encryptedSecret = extKey.PrivateKey.GetEncryptedBitcoinSecret(password, Network.Main);

HDFingerprint masterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
ExtPubKey extPubKey = extKey.Derive(AccountKeyPath).Neuter();
return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerprint, extPubKey, false, AbsoluteMinGapLimit, new BlockchainState(), filePath);
KeyPath keyPath = DefaultAccountKeyPath;
ExtPubKey extPubKey = extKey.Derive(keyPath).Neuter();
return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerprint, extPubKey, false, AbsoluteMinGapLimit, new BlockchainState(), filePath, keyPath);
}

public static KeyManager CreateNewWatchOnly(ExtPubKey extPubKey, string filePath = null)
Expand All @@ -155,7 +162,7 @@ public static KeyManager CreateNewHardwareWalletWatchOnly(HDFingerprint masterFi
return new KeyManager(null, null, masterFingerpring, extPubKey, null, AbsoluteMinGapLimit, new BlockchainState(), filePath);
}

public static KeyManager Recover(Mnemonic mnemonic, string password, string filePath = null)
public static KeyManager Recover(Mnemonic mnemonic, string password, string filePath = null, KeyPath accountKeyPath = null, int minGapLimit = AbsoluteMinGapLimit)
{
Guard.NotNull(nameof(mnemonic), mnemonic);
if (password is null)
Expand All @@ -166,12 +173,14 @@ public static KeyManager Recover(Mnemonic mnemonic, string password, string file
ExtKey extKey = mnemonic.DeriveExtKey(password);
var encryptedSecret = extKey.PrivateKey.GetEncryptedBitcoinSecret(password, Network.Main);

HDFingerprint masterFingerpring = extKey.Neuter().PubKey.GetHDFingerPrint();
ExtPubKey extPubKey = extKey.Derive(AccountKeyPath).Neuter();
return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerpring, extPubKey, true, AbsoluteMinGapLimit, new BlockchainState(), filePath);
HDFingerprint masterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();

KeyPath keyPath = accountKeyPath ?? DefaultAccountKeyPath;
ExtPubKey extPubKey = extKey.Derive(keyPath).Neuter();
return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerprint, extPubKey, true, minGapLimit, new BlockchainState(), filePath, keyPath);
}

public void SetMinGaplimit(int? minGapLimit)
public void SetMinGapLimit(int? minGapLimit)
{
if (minGapLimit is int val)
{
Expand Down Expand Up @@ -301,7 +310,7 @@ public static bool TryGetExtPubKeyFromFile(string filePath, out ExtPubKey extPub

// Example text to handle: "ExtPubKey": "03BF8271268000000013B9013C881FE456DDF524764F6322F611B03CF6".
var extpubkeyline = File.ReadLines(filePath) // Enumerated read.
.Take(10) // Limit reads to x lines.
.Take(21) // Limit reads to x lines.
.FirstOrDefault(line => line.Contains("\"ExtPubKey\": \"", StringComparison.InvariantCulture));

if (string.IsNullOrEmpty(extpubkeyline))
Expand Down Expand Up @@ -333,7 +342,7 @@ public static bool TryGetMasterFingerprintFromFile(string filePath, out HDFinger

// Example text to handle: "ExtPubKey": "03BF8271268000000013B9013C881FE456DDF524764F6322F611B03CF6".
var masterfpline = File.ReadLines(filePath) // Enumerated read.
.Take(10) // Limit reads to x lines.
.Take(21) // Limit reads to x lines.
.FirstOrDefault(line => line.Contains("\"MasterFingerprint\": \"", StringComparison.InvariantCulture));

if (string.IsNullOrEmpty(masterfpline))
Expand Down

0 comments on commit 92b6085

Please sign in to comment.