From 0dd1b668cd6a679d008956295324e57a97ddddbd Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Mon, 22 Jun 2020 08:39:29 +0200 Subject: [PATCH] Support wasabi file format input (#1671) * Support wasabi file format input This adds support to importing the wasabi file format which seems to be used more for imports by Wasabi,ColdCard,BlueWallet & Cobo Vault (and way better than electrum anyway) * fixes * add test --- BTCPayServer.Tests/UnitTest1.cs | 51 +++++-- .../Controllers/StoresController.BTCLike.cs | 6 +- BTCPayServer/DerivationSchemeSettings.cs | 128 +++++++++++++----- .../DerivationSchemeViewModel.cs | 4 +- .../Views/Stores/AddDerivationScheme.cshtml | 2 +- ...vationSchemes_HardwareWalletDialogs.cshtml | 11 +- 6 files changed, 149 insertions(+), 53 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index fd251cf19..f8dc9f3d3 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2248,13 +2248,41 @@ namespace BTCPayServer.Tests derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; Assert.True(derivationVM.Confirmation); - - // Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network) + + + //cobo vault file + var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}"; derivationVM = (DerivationSchemeViewModel)Assert .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - string content = + derivationVM.WalletFile = TestUtils.GetFormFile("wallet3.json", content); + derivationVM.Enabled = true; + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); + Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") + .GetAwaiter().GetResult()); + + //wasabi wallet file + content = + "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}"; + + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM.WalletFile = TestUtils.GetFormFile("wallet4.json", content); + derivationVM.Enabled = true; + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); + Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") + .GetAwaiter().GetResult()); + + + // Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network) + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; - derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet.json", content); + derivationVM.WalletFile = TestUtils.GetFormFile("wallet.json", content); derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; Assert.False(derivationVM @@ -2265,19 +2293,20 @@ namespace BTCPayServer.Tests "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; derivationVM = (DerivationSchemeViewModel)Assert .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet2.json", content); + derivationVM.WalletFile = TestUtils.GetFormFile("wallet2.json", content); derivationVM.Enabled = true; derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; Assert.True(derivationVM.Confirmation); Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") .GetAwaiter().GetResult()); - + + // Now let's check that no data has been lost in the process var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult(); var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks) .OfType().First(o => o.PaymentId.IsBTCOnChain); - DerivationSchemeSettings.TryParseFromElectrumWallet(content, onchainBTC.Network, out var expected); + DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected); Assert.Equal(expected.ToJson(), onchainBTC.ToJson()); // Let's check that the root hdkey and account key path are taken into account when making a PSBT @@ -3581,7 +3610,7 @@ normal: var root = new Mnemonic( "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage") .DeriveExtKey(); - Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( + Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", mainnet, out var settings)); Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint); @@ -3598,20 +3627,20 @@ normal: var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork("BTC"); // Should be legacy - Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( + Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit); // Should be segwit p2sh - Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( + Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p && p.Inner is DirectDerivationStrategy s2 && s2.Segwit); // Should be segwit - Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( + Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit); diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index c5174aa0c..ee14995aa 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -110,14 +110,14 @@ namespace BTCPayServer.Controllers } } - if (vm.ElectrumWalletFile != null) + if (vm.WalletFile != null) { - if (!DerivationSchemeSettings.TryParseFromElectrumWallet(await ReadAllText(vm.ElectrumWalletFile), network, out strategy)) + if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy)) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Error, - Message = "Electrum wallet/Air-gapped hardware wallet file was not in the correct format" + Message = "Wallet file was not in the correct format" }); vm.Confirmation = false; return View(nameof(AddDerivationScheme),vm); diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index 78a8223d0..00ba2ed77 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Payments; using NBitcoin; +using NBitcoin.DataEncoders; using NBXplorer.DerivationStrategy; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -38,12 +40,12 @@ namespace BTCPayServer return strategy != null; } - private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings) + private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true) { try { derivationSchemeSettings.AccountOriginal = xpub.Trim(); - derivationSchemeSettings.AccountDerivation = derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal); + derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal); derivationSchemeSettings.AccountKeySettings = new AccountKeySettings[1]; derivationSchemeSettings.AccountKeySettings[0] = new AccountKeySettings(); derivationSchemeSettings.AccountKeySettings[0].AccountKey = derivationSchemeSettings.AccountDerivation.GetExtPubKeys().Single().GetWif(derivationSchemeParser.Network); @@ -57,58 +59,122 @@ namespace BTCPayServer } } - public static bool TryParseFromElectrumWallet(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings) + public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings) { settings = null; - if (coldcardExport == null) - throw new ArgumentNullException(nameof(coldcardExport)); + if (fileContents == null) + throw new ArgumentNullException(nameof(fileContents)); if (network == null) throw new ArgumentNullException(nameof(network)); var result = new DerivationSchemeSettings(); - result.Source = "Electrum/Airgap hardware wallet"; var derivationSchemeParser = new DerivationSchemeParser(network); JObject jobj = null; try { - jobj = JObject.Parse(coldcardExport); - jobj = (JObject)jobj["keystore"]; + jobj = JObject.Parse(fileContents); } catch { - return TryParseXpub(coldcardExport, derivationSchemeParser, ref result); + result.Source = "GenericFile"; + return TryParseXpub(fileContents, derivationSchemeParser, ref result); } - if (!jobj.ContainsKey("xpub") || - !TryParseXpub(jobj["xpub"].Value(), derivationSchemeParser, ref result)) + //electrum + if (jobj.ContainsKey("keystore")) { - return false; - } - - if (jobj.ContainsKey("label")) - { - try + result.Source = "ElectrumFile"; + jobj = (JObject)jobj["keystore"]; + + if (!jobj.ContainsKey("xpub") || + !TryParseXpub(jobj["xpub"].Value(), derivationSchemeParser, ref result)) { - result.Label = jobj["label"].Value(); + return false; } - catch { return false; } - } - if (jobj.ContainsKey("ckcc_xfp")) - { - try + if (jobj.ContainsKey("label")) { - result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value()); + try + { + result.Label = jobj["label"].Value(); + } + catch { return false; } } - catch { return false; } - } - if (jobj.ContainsKey("derivation")) - { - try + if (jobj.ContainsKey("ckcc_xfp")) { - result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value()); + try + { + result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value()); + } + catch { return false; } + } + + if (jobj.ContainsKey("derivation")) + { + try + { + result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value()); + } + catch { return false; } + } + } + else + { + result.Source = "WasabiFile"; + //wasabi format + if (!jobj.ContainsKey("ExtPubKey") || + !TryParseXpub(jobj["ExtPubKey"].Value(), derivationSchemeParser, ref result, false)) + { + return false; + } + if (jobj.ContainsKey("MasterFingerprint")) + { + try + { + var mfpString = jobj["MasterFingerprint"].ToString().Trim(); + // https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066 + + if(uint.TryParse(mfpString, out var fingerprint)) + { + result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint); + } + else + { + var shouldReverseMfp = jobj.ContainsKey("ColdCardFirmwareVersion") && + jobj["ColdCardFirmwareVersion"].ToString() == "2.1.0"; + var bytes = Encoders.Hex.DecodeData(mfpString); + result.AccountKeySettings[0].RootFingerprint = shouldReverseMfp ? new HDFingerprint(bytes.Reverse().ToArray()) : new HDFingerprint(bytes); + } + } + + catch { return false; } + } + if (jobj.ContainsKey("AccountKeyPath")) + { + try + { + result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["AccountKeyPath"].Value()); + } + catch { return false; } + } + if (jobj.ContainsKey("DerivationPath")) + { + try + { + result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["DerivationPath"].Value().ToLowerInvariant()); + } + catch { return false; } + } + + if (jobj.ContainsKey("ColdCardFirmwareVersion")) + { + result.Source = "ColdCard"; + } + + if (jobj.ContainsKey("CoboVaultFirmwareVersion")) + { + result.Source = "CoboVault"; } - catch { return false; } } settings = result; settings.Network = network; diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index 629da7244..4ef16c661 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -35,8 +35,8 @@ namespace BTCPayServer.Models.StoreViewModels public KeyPath RootKeyPath { get; set; } - [Display(Name = "Electrum Wallet File")] - public IFormFile ElectrumWalletFile{ get; set; } + [Display(Name = "Electrum/Hardware Wallet File")] + public IFormFile WalletFile{ get; set; } public string Config { get; set; } public string Source { get; set; } public string DerivationSchemeFormat { get; set; } diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index 3c3d50138..97bcf0bb6 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -85,7 +85,7 @@ { } - + @if (Model.CanUseHotWallet) { diff --git a/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml b/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml index 60e35af9b..bd636dfa8 100644 --- a/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml @@ -11,7 +11,7 @@