diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 1f66d0a1b..c3f1b1662 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -557,22 +557,18 @@ namespace BTCPayServer.Controllers if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary)) throw new Exception($"{network.CryptoCode}: not started or fully synched"); - - - var strategy = GetDirectDerivationStrategy(derivationSettings.AccountDerivation); - // Some deployment does not have the AccountKeyPath set, let's fix this... if (derivationSettings.AccountKeyPath == null) { // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy - var foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token); - if (foundKeyPath == null) - throw new HardwareWalletException($"This store is not configured to use this ledger"); - derivationSettings.AccountKeyPath = foundKeyPath; + var foundKeyPath = await hw.FindKeyPathFromPubkeys(network, + derivationSettings.AccountDerivation.GetExtPubKeys().Select(p => p.GetPublicKey()).ToArray(), + normalOperationTimeout.Token); + derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger"); storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } - // If it has the AccountKeyPath, let's check if we opened the right ledger + // If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger else { // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub, @@ -587,8 +583,8 @@ namespace BTCPayServer.Controllers // We have the root fingerprint, we can check the root from it else { - var actualPubKey = await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token); - if (actualPubKey.GetPublicKey().GetHDFingerPrint() != derivationSettings.RootFingerprint.Value) + var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token); + if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value) throw new HardwareWalletException($"This store is not configured to use this ledger"); } } @@ -596,7 +592,7 @@ namespace BTCPayServer.Controllers // Some deployment does not have the RootFingerprint set, let's fix this... if (derivationSettings.RootFingerprint == null) { - derivationSettings.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetPublicKey().GetHDFingerPrint(); + derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint(); storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } @@ -648,16 +644,6 @@ namespace BTCPayServer.Controllers } return new EmptyResult(); } - - private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy) - { - if (strategy == null) - throw new Exception("The derivation scheme is not provided"); - var directStrategy = strategy as DirectDerivationStrategy; - if (directStrategy == null) - directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy; - return directStrategy; - } } diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index 87131c9ca..a4300df00 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -20,189 +20,46 @@ namespace BTCPayServer.Services public HardwareWalletException(string message) : base(message) { } public HardwareWalletException(string message, Exception inner) : base(message, inner) { } } - public class HardwareWalletService : IDisposable + public abstract class HardwareWalletService : IDisposable { - class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable + public abstract string Device { get; } + public abstract Task Test(CancellationToken cancellation); + + public abstract Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation); + public virtual async Task GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) { - private readonly WebSocket webSocket; - - public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket) - { - if (webSocket == null) - throw new ArgumentNullException(nameof(webSocket)); - this.webSocket = webSocket; - } - - SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1); - public async Task Exchange(byte[][] apdus, CancellationToken cancellationToken) - { - await _Semaphore.WaitAsync(); - List responses = new List(); - try - { - foreach (var apdu in apdus) - { - await this.webSocket.SendAsync(new ArraySegment(apdu), WebSocketMessageType.Binary, true, cancellationToken); - } - foreach (var apdu in apdus) - { - byte[] response = new byte[300]; - var result = await this.webSocket.ReceiveAsync(new ArraySegment(response), cancellationToken); - Array.Resize(ref response, result.Count); - responses.Add(response); - } - } - finally - { - _Semaphore.Release(); - } - return responses.ToArray(); - } - - public void Dispose() - { - _Semaphore.Dispose(); - } + return (await GetExtPubKey(network, keyPath, cancellation)).GetPublicKey(); } - private readonly LedgerClient _Ledger; - public LedgerClient Ledger - { - get - { - return _Ledger; - } - } - - public string Device => "Ledger wallet"; - - WebSocketTransport _Transport = null; - public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet) - { - if (ledgerWallet == null) - throw new ArgumentNullException(nameof(ledgerWallet)); - _Transport = new WebSocketTransport(ledgerWallet); - _Ledger = new LedgerClient(_Transport); - _Ledger.MaxAPDUSize = 90; - } - - public async Task Test(CancellationToken cancellation) - { - var version = await Ledger.GetFirmwareVersionAsync(cancellation); - return new LedgerTestResult() { Success = true }; - } - - public async Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) - { - if (network == null) - throw new ArgumentNullException(nameof(network)); - return await GetExtPubKey(Ledger, network, keyPath, false, cancellation); - } - - private static async Task GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation) - { - try - { - var pubKey = await ledger.GetWalletPubKeyAsync(account, cancellation: cancellation); - try - { - pubKey.GetAddress(network.NBitcoinNetwork); - } - catch - { - if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) - throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}."); - } - var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint(); - var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), - pubKey.ChainCode, - (byte)account.Indexes.Length, - parentFP, - account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork); - return extpubkey; - } - catch (FormatException) - { - throw new HardwareWalletException("Unsupported ledger app"); - } - } - - public async Task FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation) + public async Task FindKeyPathFromPubkeys(BTCPayNetwork network, PubKey[] pubKeys, CancellationToken cancellation) { List derivations = new List(); if (network.NBitcoinNetwork.Consensus.SupportSegwit) derivations.Add(new KeyPath("49'")); derivations.Add(new KeyPath("44'")); + derivations.Add(new KeyPath("84'")); KeyPath foundKeyPath = null; foreach (var account in derivations .Select(purpose => purpose.Derive(network.CoinType)) .SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true)))) { - try + var pubkey = await GetPubKey(network, account, cancellation); + if (pubKeys.Contains(pubkey)) { - var extpubkey = await GetExtPubKey(Ledger, network, account, true, cancellation); - if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) - { - foundKeyPath = account; - break; - } - } - catch (FormatException) - { - throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}"); + foundKeyPath = account; + break; } } return foundKeyPath; } - public async Task SignTransactionAsync(PSBT psbt, Script changeHint, - CancellationToken cancellationToken) - { - try - { - var unsigned = psbt.GetGlobalTransaction(); - var changeKeyPath = psbt.Outputs - .Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey) - .Where(o => o.HDKeyPaths.Any()) - .Select(o => o.HDKeyPaths.First().Value.Item2) - .FirstOrDefault(); - var signatureRequests = psbt - .Inputs - .Where(o => o.HDKeyPaths.Any()) - .Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key)) - .Select(i => new SignatureRequest() - { - InputCoin = i.GetSignableCoin(), - InputTransaction = i.NonWitnessUtxo, - KeyPath = i.HDKeyPaths.First().Value.Item2, - PubKey = i.HDKeyPaths.First().Key - }).ToArray(); - var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken); - if (signedTransaction == null) - throw new Exception("The ledger failed to sign the transaction"); + public abstract Task SignTransactionAsync(PSBT psbt, Script changeHint, + CancellationToken cancellationToken); - psbt = psbt.Clone(); - foreach (var signature in signatureRequests) - { - var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint); - if (input == null) - continue; - input.PartialSigs.Add(signature.PubKey, signature.Signature); - } - return psbt; - } - catch (Exception ex) - { - throw new Exception("The ledger failed to sign the transaction", ex); - } - } - - public void Dispose() + public virtual void Dispose() { - if (_Transport != null) - _Transport.Dispose(); } } diff --git a/BTCPayServer/Services/LedgerHardwareWalletService.cs b/BTCPayServer/Services/LedgerHardwareWalletService.cs new file mode 100644 index 000000000..6d37e3477 --- /dev/null +++ b/BTCPayServer/Services/LedgerHardwareWalletService.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using LedgerWallet; +using NBitcoin; + +namespace BTCPayServer.Services +{ + public class LedgerHardwareWalletService : HardwareWalletService + { + class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable + { + private readonly WebSocket webSocket; + + public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket) + { + if (webSocket == null) + throw new ArgumentNullException(nameof(webSocket)); + this.webSocket = webSocket; + } + + SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1); + public async Task Exchange(byte[][] apdus, CancellationToken cancellationToken) + { + await _Semaphore.WaitAsync(); + List responses = new List(); + try + { + foreach (var apdu in apdus) + { + await this.webSocket.SendAsync(new ArraySegment(apdu), WebSocketMessageType.Binary, true, cancellationToken); + } + foreach (var apdu in apdus) + { + byte[] response = new byte[300]; + var result = await this.webSocket.ReceiveAsync(new ArraySegment(response), cancellationToken); + Array.Resize(ref response, result.Count); + responses.Add(response); + } + } + finally + { + _Semaphore.Release(); + } + return responses.ToArray(); + } + + public void Dispose() + { + _Semaphore.Dispose(); + } + } + + private readonly LedgerClient _Ledger; + public LedgerClient Ledger + { + get + { + return _Ledger; + } + } + + public override string Device => "Ledger wallet"; + + WebSocketTransport _Transport = null; + public LedgerHardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet) + { + if (ledgerWallet == null) + throw new ArgumentNullException(nameof(ledgerWallet)); + _Transport = new WebSocketTransport(ledgerWallet); + _Ledger = new LedgerClient(_Transport); + _Ledger.MaxAPDUSize = 90; + } + + public override async Task Test(CancellationToken cancellation) + { + var version = await Ledger.GetFirmwareVersionAsync(cancellation); + return new LedgerTestResult() { Success = true }; + } + + public override async Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + return await GetExtPubKey(network, keyPath, false, cancellation); + } + public override async Task GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + return (await GetExtPubKey(network, keyPath, false, cancellation)).GetPublicKey(); + } + + private async Task GetExtPubKey(BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation) + { + var pubKey = await Ledger.GetWalletPubKeyAsync(account, cancellation: cancellation); + try + { + pubKey.GetAddress(network.NBitcoinNetwork); + } + catch + { + if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) + throw new HardwareWalletException($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}."); + } + var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await Ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint(); + var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), + pubKey.ChainCode, + (byte)account.Indexes.Length, + parentFP, + account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork); + return extpubkey; + } + + public override async Task SignTransactionAsync(PSBT psbt, Script changeHint, CancellationToken cancellationToken) + { + var unsigned = psbt.GetGlobalTransaction(); + var changeKeyPath = psbt.Outputs + .Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey) + .Where(o => o.HDKeyPaths.Any()) + .Select(o => o.HDKeyPaths.First().Value.Item2) + .FirstOrDefault(); + var signatureRequests = psbt + .Inputs + .Where(o => o.HDKeyPaths.Any()) + .Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key)) + .Select(i => new SignatureRequest() + { + InputCoin = i.GetSignableCoin(), + InputTransaction = i.NonWitnessUtxo, + KeyPath = i.HDKeyPaths.First().Value.Item2, + PubKey = i.HDKeyPaths.First().Key + }).ToArray(); + var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken); + if (signedTransaction == null) + throw new HardwareWalletException("The ledger failed to sign the transaction"); + + psbt = psbt.Clone(); + foreach (var signature in signatureRequests) + { + var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint); + if (input == null) + continue; + input.PartialSigs.Add(signature.PubKey, signature.Signature); + } + return psbt; + } + + public override void Dispose() + { + if (_Transport != null) + _Transport.Dispose(); + } + } +}