diff --git a/BTCPayServer.Abstractions/Security/ContentSecurityPolicies.cs b/BTCPayServer.Abstractions/Security/ContentSecurityPolicies.cs index 3f56bad90..f665b816e 100644 --- a/BTCPayServer.Abstractions/Security/ContentSecurityPolicies.cs +++ b/BTCPayServer.Abstractions/Security/ContentSecurityPolicies.cs @@ -100,6 +100,7 @@ namespace BTCPayServer.Security var sha = GetSha256(script); Add("script-src", $"'sha256-{sha}'"); } + static string GetSha256(string script) { return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal)))); diff --git a/BTCPayServer.Common/Extensions.cs b/BTCPayServer.Common/Extensions.cs index 40274cc6d..69c89e034 100644 --- a/BTCPayServer.Common/Extensions.cs +++ b/BTCPayServer.Common/Extensions.cs @@ -16,6 +16,8 @@ namespace BTCPayServer public static ScriptPubKeyType ScriptPubKeyType(this DerivationStrategyBase derivationStrategyBase) { + if (derivationStrategyBase is TaprootDerivationStrategy) + return NBitcoin.ScriptPubKeyType.TaprootBIP86; if (IsSegwitCore(derivationStrategyBase)) { return NBitcoin.ScriptPubKeyType.Segwit; diff --git a/BTCPayServer/APDUVaultTransport.cs b/BTCPayServer/APDUVaultTransport.cs index e98b262c0..1f3722ac3 100644 --- a/BTCPayServer/APDUVaultTransport.cs +++ b/BTCPayServer/APDUVaultTransport.cs @@ -36,7 +36,7 @@ namespace BTCPayServer ["apdu"] = Encoders.Hex.EncodeData(apdu) }, cancellationToken); var data = Encoders.Hex.DecodeData(resp["data"].Value()); - return new NtagResponse(data, resp["status"].Value()); + return new NtagResponse(data, resp["status"]!.Value()); } } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 2f68e2e46..bf437f34b 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -54,7 +54,7 @@ - + diff --git a/BTCPayServer/Blazor/Icon.razor b/BTCPayServer/Blazor/Icon.razor index 584b36e38..7e505f654 100644 --- a/BTCPayServer/Blazor/Icon.razor +++ b/BTCPayServer/Blazor/Icon.razor @@ -3,13 +3,15 @@ @inject IFileVersionProvider FileVersionProvider @inject BTCPayServerOptions BTCPayServerOptions - + @code { [Parameter, EditorRequired] public string Symbol { get; set; } + [Parameter] + public string Class { get; set; } private string GetPathTo(string symbol) { var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg"); diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/Feedback.razor b/BTCPayServer/Blazor/VaultBridge/Elements/Feedback.razor new file mode 100644 index 000000000..d8ab0a456 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/Feedback.razor @@ -0,0 +1,64 @@ +@inherits VaultElement +@using System.IO +@using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.AspNetCore.Razor.TagHelpers +@using Microsoft.Extensions.Localization + +
+ + + @if (Html is not null) + { + @((MarkupString)Html) + } + else if (Text is not null) + { + @Text + } + +
+ +@code { + public Feedback() + { + } + + public Feedback(LocalizedString str, FeedbackType state) + { + this.State = state; + this.Text = str.ToString(); + } + + public Feedback(LocalizedHtmlString str, FeedbackType state) + { + this.State = state; + var txt = new StringWriter(); + str.WriteTo(txt, NullHtmlEncoder.Default); + this.Html = txt.ToString(); + } + + + + public FeedbackType State { get; set; } + + public string GetClass() + => State switch + { + FeedbackType.Loading => "icon-dots feedback-icon-loading", + FeedbackType.Success => "icon-checkmark feedback-icon-success", + FeedbackType.Failed => "icon-cross feedback-icon-failed", + _ => "" + }; + + public string GetSymbol() + => State switch + { + FeedbackType.Loading => "dots", + FeedbackType.Success => "checkmark", + FeedbackType.Failed => "cross", + _ => "" + }; + + public string Html { get; set; } + public string Text { get; set; } +} diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/FeedbackType.cs b/BTCPayServer/Blazor/VaultBridge/Elements/FeedbackType.cs new file mode 100644 index 000000000..53723f517 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/FeedbackType.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer.Blazor.VaultBridge.Elements; + +public enum FeedbackType +{ + Loading, + Success, + Failed +} diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/Passphrase.razor b/BTCPayServer/Blazor/VaultBridge/Elements/Passphrase.razor new file mode 100644 index 000000000..5db3b5409 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/Passphrase.razor @@ -0,0 +1,69 @@ +@inherits VaultElement +@implements IDisposable + +
+
+ +
+ + +
+
+
+ +
+ + +
+ @if (Error != "") + { + @Error + } +
+ +
+ +@code { + private readonly VaultBridgeUI ui; + + public Passphrase(VaultBridgeUI ui) + { + this.ui = ui; + } + string PasswordConfirmation { get; set; } = ""; + string Password { get; set; } = ""; + string Error { get; set; } = ""; + + private TaskCompletionSource _cts; + public Task GetPassword() + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Enter the passphrase."]); + ui.AddElement(this); + _cts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return _cts.Task; + } + + public void OnConfirmPasswordClick() + { + if (Password != PasswordConfirmation) + { + Error = ui.StringLocalizer["Invalid password confirmation."].Value; + ui.StateHasChanged(); + return; + } + + ui.Elements.Remove(this); + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Password entered..."]); + + _cts?.TrySetResult(Password); + _cts = null; + } + + public void Dispose() => _cts?.TrySetCanceled(); +} diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/PinInput.razor b/BTCPayServer/Blazor/VaultBridge/Elements/PinInput.razor new file mode 100644 index 000000000..a51de1b17 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/PinInput.razor @@ -0,0 +1,104 @@ +@using System.Globalization +@inherits VaultElement +@implements IDisposable + +
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +@code { + private readonly VaultBridgeUI ui; + + public PinInput(VaultBridgeUI ui) + { + this.ui = ui; + } + + public int Value => int.TryParse(input, CultureInfo.InvariantCulture, out var v) ? v : 0; + public string input = ""; + public string Display = ""; + + public Task GetPin() + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Enter the pin."]); + ui.AddElement(this); + _cts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return _cts.Task; + } + + public void Click(int i) + { + if (input.Length > 10) + return; + input += i; + UpdateDisplay(); + } + + private void UpdateDisplay() + { + Display = new string('*', input.Length); + ui.StateHasChanged(); + } + + public void DeleteAll() + { + input = ""; + UpdateDisplay(); + } + + private TaskCompletionSource _cts; + + public void OnConfirmPinClick() + { + ui.Elements.Remove(this); + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Verifying pin..."]); + _cts?.TrySetResult(this.Value); + _cts = null; + } + + public void Dispose() => _cts?.TrySetCanceled(); +} diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/Retry.razor b/BTCPayServer/Blazor/VaultBridge/Elements/Retry.razor new file mode 100644 index 000000000..ac3f5e273 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/Retry.razor @@ -0,0 +1,18 @@ +@inherits VaultElement + + + +@code { + private readonly VaultBridgeUI ui; + + public Retry(VaultBridgeUI ui) + { + this.ui = ui; + } + + private async Task OnRetryClick() + { + ui.Elements.Clear(); + await ui.Connect(); + } +} diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/VerifyAddress.razor b/BTCPayServer/Blazor/VaultBridge/Elements/VerifyAddress.razor new file mode 100644 index 000000000..31aaffff3 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/VerifyAddress.razor @@ -0,0 +1,62 @@ +@using BTCPayServer.Hwi +@using NBitcoin +@inherits VaultElement +@implements IDisposable + +@if (ConfirmedOnDevice) +{ + +} +else +{ + +} + +@code { + + private readonly VaultBridgeUI ui; + public VerifyAddress(VaultBridgeUI ui) + { + this.ui = ui; + } + + public BitcoinAddress Address { get; set; } + public KeyPath KeyPath { get; set; } + public ScriptPubKeyType ScriptPubKeyType { get; set; } + public HwiDeviceClient Device { get; set; } + + private TaskCompletionSource _cts; + + public void OnConfirm() + { + ui.Elements.Remove(this); + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Address verified."]); + _cts?.TrySetResult(true); + _cts = null; + } + + bool ConfirmedOnDevice { get; set; } + + public async Task WaitConfirmed() + { + ui.ShowFeedback(FeedbackType.Loading, + ui.ViewLocalizer["Please verify that the address displayed on your device is {0}...", Address.ToString()]); + ui.AddElement(this); + + var deviceAddress = await Device.DisplayAddressAsync(ScriptPubKeyType, KeyPath, ui.CancellationToken); + // Note that the device returned here may be different from what on screen for Testnet/Regtest + if (deviceAddress != Address) + { + ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["Unexpected address returned by the device..."]); + return false; + } + + ConfirmedOnDevice = true; + ui.StateHasChanged(); + + _cts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return await _cts.Task; + } + + public void Dispose() => _cts?.TrySetCanceled(); +} diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/Warning.razor b/BTCPayServer/Blazor/VaultBridge/Elements/Warning.razor new file mode 100644 index 000000000..f89c5952a --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/Warning.razor @@ -0,0 +1,23 @@ +@using Microsoft.AspNetCore.Mvc.Localization +@inherits VaultElement + + + +@code { + private readonly VaultBridgeUI ui; + + public Warning(VaultBridgeUI ui, LocalizedHtmlString str) + { + this.ui = ui; + Html = str.Value; + } + + public string Html { get; set; } +} diff --git a/BTCPayServer/Blazor/VaultBridge/Elements/XPubSelect.razor b/BTCPayServer/Blazor/VaultBridge/Elements/XPubSelect.razor new file mode 100644 index 000000000..f9eb8309e --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/Elements/XPubSelect.razor @@ -0,0 +1,81 @@ +@using NBitcoin +@inherits VaultElement + +
+
+ + +
+
+ + +
+
+ + +@code { + private readonly VaultBridgeUI ui; + + public XPubSelect(VaultBridgeUI ui, Network network) + { + this.ui = ui; + CanUseTaproot = network.Consensus.SupportTaproot; + CanUseSegwit = network.Consensus.SupportSegwit; + AddressType = CanUseSegwit ? "segwit" : "legacy"; + } + + public KeyPath ToKeyPath() + => ToScriptPubKeyType() switch + { + ScriptPubKeyType.TaprootBIP86 => new KeyPath("86'"), + ScriptPubKeyType.Segwit => new KeyPath("84'"), + ScriptPubKeyType.SegwitP2SH => new KeyPath("49'"), + _ => new KeyPath("44'"), + }; + + public ScriptPubKeyType ToScriptPubKeyType() + => AddressType switch + { + "segwit" => ScriptPubKeyType.Segwit, + "segwitWrapped" => ScriptPubKeyType.SegwitP2SH, + "taproot" => ScriptPubKeyType.TaprootBIP86, + _ => ScriptPubKeyType.Legacy + }; + + public string AddressType { get; set; } + public int AccountNumber { get; set; } + public bool CanUseTaproot { get; } + public bool CanUseSegwit { get; } + TaskCompletionSource _cts; + + public Task GetXPubSelect() + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Select your address type and account"]); + ui.AddElement(this); + _cts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return _cts.Task; + } + + public void OnConfirmXPubClick() + { + ui.Elements.Remove(this); + ui.Elements.RemoveAt(ui.Elements.Count - 1); + ui.StateHasChanged(); + _cts?.TrySetResult(this); + _cts = null; + } + + public void Dispose() => _cts?.TrySetCanceled(); +} diff --git a/BTCPayServer/Blazor/VaultBridge/HWIController.cs b/BTCPayServer/Blazor/VaultBridge/HWIController.cs new file mode 100644 index 000000000..5a02dec9d --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/HWIController.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Blazor.VaultBridge.Elements; +using BTCPayServer.Data; +using BTCPayServer.Hwi; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; +using NBitcoin; +using NBXplorer.DerivationStrategy; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Blazor.VaultBridge; + +public abstract class HWIController : VaultController +{ + override protected string VaultUri => "http://127.0.0.1:65092/hwi-bridge/v1"; + public string CryptoCode { get; set; } + + private static bool IsTrezorT(HwiEnumerateEntry deviceEntry) + { + return deviceEntry.Model.Contains("Trezor_T", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsTrezorOne(HwiEnumerateEntry deviceEntry) + { + return deviceEntry.Model.Contains("trezor_1", StringComparison.OrdinalIgnoreCase); + } + + protected abstract Task Run(VaultBridgeUI ui, HwiClient hwi, HwiDeviceClient device, HDFingerprint fingerprint, BTCPayNetwork network, + CancellationToken cancellationToken); + + protected override async Task Run(VaultBridgeUI ui, VaultClient vaultClient, CancellationToken cancellationToken) + { + var networkProviders = ui.ServiceProvider.GetRequiredService(); + try + { + var network = networkProviders.GetNetwork(CryptoCode); + var hwi = new Hwi.HwiClient(network.NBitcoinNetwork) + { + Transport = new VaultHWITransport(vaultClient), IgnoreInvalidNetwork = network.NBitcoinNetwork.ChainName != ChainName.Mainnet + }; + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Fetching device..."]); + var version = await hwi.GetVersionAsync(cancellationToken); + if (version.Major < 2) + { + ui.ShowFeedback(FeedbackType.Failed, + ui.ViewLocalizer[ + "Your BTCPay Server Vault version is outdated. Please download the latest version."]); + } + + var gettingEntries = hwi.EnumerateEntriesAsync(cancellationToken); + var timeout = Task.Delay(TimeSpan.FromSeconds(7.0), cancellationToken); + var finished = await Task.WhenAny(gettingEntries, timeout); + // Wallets such as Trezor Safe 3 will block EnumerateEntriesAsync until password is set on the device. + // So if we wait for 7 sec and this doesn't returns, let's notify the user to look the hardware. + if (finished == timeout) + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please, enter the passphrase on the device."]); + var entries = await gettingEntries; + var deviceEntry = entries.FirstOrDefault(); + + if (deviceEntry is null) + { + ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["No device connected."]); + ui.ShowRetry(); + return; + } + + if (deviceEntry.Model is null) + { + ui.ShowFeedback(FeedbackType.Failed, + ui.StringLocalizer["Unsupported hardware wallet, try to update BTCPay Server Vault"]); + ui.ShowRetry(); + return; + } + + var device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint); + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Device found: {0}", device.GetNiceModelName()]); + + + HDFingerprint? fingerprint = deviceEntry.Fingerprint; + bool dirtyDevice = false; + if (deviceEntry is { Code: HwiErrorCode.DeviceNotReady }) + { + // It seems that this 'if (IsTrezorT(deviceEntry))' can be removed. + // I have not managed to trigger this anymore with latest 2.8.9 + // the passphrase is getting asked during EnumerateEntriesAsync + if (IsTrezorT(deviceEntry)) + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please, enter the passphrase on the device."]); + // The make the trezor T ask for password + await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); + dirtyDevice = true; + } + else if (deviceEntry.NeedsPinSent is true) + { + await device.PromptPinAsync(cancellationToken); + var pinElement = new PinInput(ui); + var pin = await pinElement.GetPin(); + if (!await device.SendPinAsync(pin, cancellationToken)) + { + ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["Incorrect pin code."]); + ui.ShowRetry(); + return; + } + + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Pin code verified."]); + dirtyDevice = true; + } + } + else if (deviceEntry is { Code: HwiErrorCode.DeviceNotInitialized }) + { + ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["The device has not been initialized."]); + ui.ShowRetry(); + return; + } + + if (IsTrezorOne(deviceEntry) && HasPassphraseProtection(deviceEntry)) + { + var passwordEl = new Passphrase(ui); + device.Password = await passwordEl.GetPassword(); + if (!string.IsNullOrEmpty(device.Password)) + { + device = new HwiDeviceClient(hwi, DeviceSelectors.FromDeviceType("trezor", deviceEntry.Path), deviceEntry.Model, null) + { + Password = device.Password + }; + fingerprint = null; + } + } + + if (dirtyDevice) + { + entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList(); + deviceEntry = entries.FirstOrDefault() ?? deviceEntry; + device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint); + fingerprint = deviceEntry.Fingerprint; + } + + if (fingerprint is null) + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Fetching wallet's fingerprint."]); + fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), ui.CancellationToken)).ExtPubKey.ParentFingerprint; + device = new HwiDeviceClient(hwi, DeviceSelectors.FromFingerprint(fingerprint.Value), deviceEntry.Model, fingerprint) + { + Password = device.Password + }; + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Wallet's fingerprint fetched."]); + } + + await Run(ui, hwi, device, fingerprint.Value, network, cancellationToken); + } + catch (HwiException e) + { + var message = e switch + { + { ErrorCode: HwiErrorCode.ActionCanceled } => ui.StringLocalizer["Action canceled by user"], + _ => ui.StringLocalizer["An unexpected error happened: {0}", $"{e.Message} ({e.ErrorCode})"], + }; + ui.ShowFeedback(FeedbackType.Failed, message); + ui.ShowRetry(); + } + } + + private bool HasPassphraseProtection(HwiEnumerateEntry deviceEntry) + { + if (deviceEntry.NeedsPassphraseSent is true) + return true; + if (deviceEntry.RawData["warnings"] is JArray arr) + { + return arr.Any(e => e.ToString().Contains("passphrase was not provided", StringComparison.OrdinalIgnoreCase)); + } + + return false; + } +} + +public class SignHWIController : HWIController +{ + public string StoreId { get; set; } + public string PSBT { get; set; } + + protected override async Task Run(VaultBridgeUI ui, HwiClient hwi, HwiDeviceClient device, HDFingerprint fingerprint, BTCPayNetwork network, + CancellationToken cancellationToken) + { + if (!NBitcoin.PSBT.TryParse(PSBT, network.NBitcoinNetwork, out var psbt)) + return; + var store = await ui.ServiceProvider.GetRequiredService().FindStore(StoreId ?? ""); + var handlers = ui.ServiceProvider.GetRequiredService(); + var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); + var derivationSettings = store?.GetPaymentMethodConfig(pmi, handlers); + if (store is null || derivationSettings is null) + return; + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Checking if this device can sign the transaction..."]); + // we ensure that the device fingerprint is part of the derivation settings + if (derivationSettings.AccountKeySettings.All(a => a.RootFingerprint != fingerprint)) + { + ui.ShowFeedback(FeedbackType.Failed, + ui.StringLocalizer[ + "This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)"]); + ui.ShowRetry(); + return; + } + + derivationSettings.RebaseKeyPaths(psbt); + // otherwise, let the device check if it can sign anything + var signableInputs = psbt.Inputs + .SelectMany(i => i.HDKeyPaths) + .Where(i => i.Value.MasterFingerprint == fingerprint) + .ToArray(); + if (signableInputs.Length > 0) + { + var actualPubKey = (await device.GetXPubAsync(signableInputs[0].Value.KeyPath, cancellationToken)).GetPublicKey(); + if (actualPubKey != signableInputs[0].Key) + { + ui.ShowFeedback(FeedbackType.Failed, + ui.StringLocalizer["This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)"]); + ui.ShowRetry(); + return; + } + + if (derivationSettings.IsMultiSigOnServer) + { + var alreadySigned = psbt.Inputs.Any(a => + a.PartialSigs.Any(a => a.Key == actualPubKey)); + if (alreadySigned) + { + ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["This device already signed PSBT."]); + ui.ShowRetry(); + return; + } + } + } + + ui.ShowFeedback(FeedbackType.Loading, + ui.StringLocalizer["Please review and confirm the transaction on your device..."]); + psbt = await device.SignPSBTAsync(psbt, cancellationToken); + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Transaction signed successfully, proceeding to review..."]); + await ui.JSRuntime.InvokeVoidAsync("vault.setSignedPSBT", cancellationToken, new System.Text.Json.Nodes.JsonObject() { ["psbt"] = psbt.ToBase64() }); + } +} + +public class GetXPubController : HWIController +{ + protected override async Task Run(VaultBridgeUI ui, HwiClient hwi, HwiDeviceClient device, HDFingerprint fingerprint, BTCPayNetwork network, + CancellationToken cancellationToken) + { + var xpubSelect = new XPubSelect(ui, network.NBitcoinNetwork); + var xpubInfo = await xpubSelect.GetXPubSelect(); + var scriptPubKeyTypeType = xpubInfo.ToScriptPubKeyType(); + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Fetching public keys..."]); + KeyPath keyPath = xpubInfo.ToKeyPath().Derive(network.CoinType).Derive(xpubInfo.AccountNumber, true); + BitcoinExtPubKey xpub = await device.GetXPubAsync(keyPath, cancellationToken); + + var factory = network.NBXplorerNetwork.DerivationStrategyFactory; + var strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = scriptPubKeyTypeType }); + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Public keys successfully fetched."]); + + var firstDepositPath = new KeyPath(0, 0); + var firstDepositAddr = + network.NBXplorerNetwork.CreateAddress(strategy, firstDepositPath, strategy.GetDerivation(firstDepositPath).ScriptPubKey); + ui.ShowFeedback(FeedbackType.Loading, + ui.ViewLocalizer["Please verify that the address displayed on your device is {0}...", firstDepositAddr.ToString()]); + + var verif = new VerifyAddress(ui) + { + Device = device, + KeyPath = keyPath.Derive(firstDepositPath), + Address = firstDepositAddr, + ScriptPubKeyType = xpubInfo.ToScriptPubKeyType() + }; + if (!await verif.WaitConfirmed()) + { + ui.ShowRetry(); + return; + } + + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Saving..."]); + + var settings = new DerivationSchemeSettings(strategy, network) { Source = "Vault" }; + settings.AccountKeySettings[0].AccountKeyPath = keyPath; + settings.AccountKeySettings[0].RootFingerprint = fingerprint; + + string[] mandatoryPrevUtxo = ["trezor", "jade"]; + settings.DefaultIncludeNonWitnessUtxo = (device.Model, scriptPubKeyTypeType) switch + { + (_, ScriptPubKeyType.TaprootBIP86) => false, + (_, ScriptPubKeyType.Legacy) => true, + ({ } s, _) when mandatoryPrevUtxo.Any(o => s.Contains(o, StringComparison.OrdinalIgnoreCase)) => true, + _ => false, + }; + + settings.Label = $"{device.GetNiceModelName()} ({fingerprint})"; + + var handlers = ui.ServiceProvider.GetRequiredService(); + var handler = handlers.GetBitcoinHandler(network.CryptoCode); + var dataProtector = ui.ServiceProvider.GetRequiredService().CreateProtector("ConfigProtector"); + + await ui.JSRuntime.InvokeVoidAsync("vault.setXPub", cancellationToken, + new System.Text.Json.Nodes.JsonObject() { ["config"] = dataProtector.ProtectString(JToken.FromObject(settings, handler.Serializer).ToString()) }); + } +} diff --git a/BTCPayServer/Blazor/VaultBridge/IController.cs b/BTCPayServer/Blazor/VaultBridge/IController.cs new file mode 100644 index 000000000..62673d3c5 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/IController.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace BTCPayServer.Blazor.VaultBridge; +public interface IController +{ + Task Run(VaultBridgeUI ui, CancellationToken cancellationToken); +} + diff --git a/BTCPayServer/Blazor/VaultBridge/NFCController.cs b/BTCPayServer/Blazor/VaultBridge/NFCController.cs new file mode 100644 index 000000000..dbd0c56bd --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/NFCController.cs @@ -0,0 +1,146 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Blazor.VaultBridge.Elements; +using BTCPayServer.Data; +using BTCPayServer.NTag424; +using BTCPayServer.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; +using static BTCPayServer.BoltcardDataExtensions; + +namespace BTCPayServer.Blazor.VaultBridge; + +record CardOrigin +{ + public record Blank() : CardOrigin; + + public record ThisIssuer(BoltcardRegistration Registration) : CardOrigin; + + public record ThisIssuerConfigured(string PullPaymentId, BoltcardRegistration Registration) : ThisIssuer(Registration); + + public record OtherIssuer() : CardOrigin; + + public record ThisIssuerReset(BoltcardRegistration Registration) : ThisIssuer(Registration); +} + +public class NFCController : VaultController +{ + protected override string VaultUri => "http://127.0.0.1:65092/nfc-bridge/v1"; + public bool NewCard { get; set; } + public string PullPaymentId { get; set; } + public string BoltcardUrl { get; set; } + + protected override async Task Run(VaultBridgeUI ui, VaultClient vaultClient, CancellationToken cancellationToken) + { + try + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Waiting for NFC to be presented..."]); + var transport = new APDUVaultTransport(vaultClient); + var ntag = new Ntag424(transport); + + await transport.WaitForCard(cancellationToken); + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["NFC detected."]); + + var settingsRepository = ui.ServiceProvider.GetRequiredService(); + var env = ui.ServiceProvider.GetRequiredService(); + var issuerKey = await settingsRepository.GetIssuerKey(env); + var dbContextFactory = ui.ServiceProvider.GetRequiredService(); + CardOrigin cardOrigin = await GetCardOrigin(dbContextFactory, PullPaymentId, ntag, issuerKey, cancellationToken); + + if (cardOrigin is CardOrigin.OtherIssuer) + { + ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["This card is already configured for another issuer"]); + ui.ShowRetry(); + return; + } + + if (NewCard) + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Configuring Boltcard..."]); + if (cardOrigin is CardOrigin.Blank || cardOrigin is CardOrigin.ThisIssuerReset) + { + await ntag.AuthenticateEV2First(0, AESKey.Default, cancellationToken); + var uid = await ntag.GetCardUID(cancellationToken); + try + { + var version = await dbContextFactory.LinkBoltcardToPullPayment(PullPaymentId, issuerKey, uid); + var cardKey = issuerKey.CreatePullPaymentCardKey(uid, version, PullPaymentId); + await ntag.SetupBoltcard(BoltcardUrl, BoltcardKeys.Default, cardKey.DeriveBoltcardKeys(issuerKey)); + } + catch + { + await dbContextFactory.SetBoltcardResetState(issuerKey, uid); + throw; + } + + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["The card is now configured"]); + } + else if (cardOrigin is CardOrigin.ThisIssuer) + { + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["This card is already properly configured"]); + } + } + else + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Resetting Boltcard..."]); + if (cardOrigin is CardOrigin.Blank) + { + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["This card is already in a factory state"]); + } + else if (cardOrigin is CardOrigin.ThisIssuer thisIssuer) + { + var cardKey = issuerKey.CreatePullPaymentCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version, PullPaymentId); + await ntag.ResetCard(issuerKey, cardKey); + await dbContextFactory.SetBoltcardResetState(issuerKey, thisIssuer.Registration.UId); + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Card reset succeed"]); + } + } + + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please remove the NFC from the card reader"]); + await transport.WaitForRemoved(cancellationToken); + ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Thank you!"]); + } + catch (UnexpectedResponseException e) + { + ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["An unexpected error happened: {0}", e.Message]); + ui.ShowRetry(); + return; + } + + // Give them time to read the message + await Task.Delay(1000, cancellationToken); + await ui.JSRuntime.InvokeVoidAsync("vault.done", cancellationToken); + } + + private async Task GetCardOrigin(ApplicationDbContextFactory dbContextFactory, string pullPaymentId, Ntag424 ntag, IssuerKey issuerKey, + CancellationToken cancellationToken) + { + CardOrigin cardOrigin; + Uri uri = await ntag.TryReadNDefURI(cancellationToken); + if (uri is null) + { + cardOrigin = new CardOrigin.Blank(); + } + else + { + var piccData = issuerKey.TryDecrypt(uri); + if (piccData is null) + { + cardOrigin = new CardOrigin.OtherIssuer(); + } + else + { + var res = await dbContextFactory.GetBoltcardRegistration(issuerKey, piccData.Uid); + if (res != null && res.PullPaymentId is null) + cardOrigin = new CardOrigin.ThisIssuerReset(res); + else if (res?.PullPaymentId != pullPaymentId) + cardOrigin = new CardOrigin.OtherIssuer(); + else + cardOrigin = new CardOrigin.ThisIssuerConfigured(res.PullPaymentId, res); + } + } + + return cardOrigin; + } +} diff --git a/BTCPayServer/Blazor/VaultBridge/VaultBridgeUI.razor b/BTCPayServer/Blazor/VaultBridge/VaultBridgeUI.razor new file mode 100644 index 000000000..73577f9fd --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/VaultBridgeUI.razor @@ -0,0 +1,122 @@ +@using System.Threading +@using BTCPayServer.Blazor.VaultBridge.Elements +@using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer stringLocalizer +@inject ViewLocalizer viewLocalizer +@inject IJSRuntime jsRuntime +@inject IServiceProvider serviceProvider +@implements IDisposable + +@foreach (var e in this.Elements) +{ + @e.RenderFragment +} + +@code { + [Parameter] public IController Controller { get; set; } + + public List Elements { get; set; } = new List(); + + public new void StateHasChanged() + { + base.StateHasChanged(); + } + + public async Task Connect() + { + try + { + await Controller.Run(this, CancellationToken); + } + catch when (CancellationToken.IsCancellationRequested) + { + } + catch (Exception e) + { + this.ShowFeedback(FeedbackType.Failed, StringLocalizer["An unexpected error happened: {0}", e.Message]); + if (e is VaultClient.VaultException) + ShowRetry(); + else + throw; + } + } + + public void ShowRetry() + { + if (!Elements.OfType().Any()) + { + Elements.Add(new Retry(this)); + this.StateHasChanged(); + } + } + + public void AddWarning(LocalizedHtmlString str) + { + Elements.Insert(0, new Warning(this, str)); + this.StateHasChanged(); + } + + protected override async Task OnInitializedAsync() + { + await Connect(); + } + + public void ShowFeedback(FeedbackType state, LocalizedString str) + { + var feedback = new Feedback(str, state); + ShowFeedback(feedback); + } + public void ShowFeedback(FeedbackType state, LocalizedHtmlString str) + { + var feedback = new Feedback(str, state); + ShowFeedback(feedback); + } + public void ShowFeedback(Feedback feedback) + { + var lastFeedback =(Feedback)(Elements.FindLastIndex(e => e is Feedback) switch + { + int i when i >= 0 => Elements[i], + _ => null + }); + + if (lastFeedback is null || lastFeedback.State == FeedbackType.Success) + { + Elements.Add(feedback); + } + else + { + // Replace the last non successful feedback by the new one + for (int i = Elements.Count - 1; i >= 0; i--) + { + if (Elements[i] == lastFeedback) + break; + Elements.RemoveAt(i); + } + Elements[^1] = feedback; + } + this.StateHasChanged(); + } + + public void AddElement(VaultElement el) + { + Elements.Add(el); + this.StateHasChanged(); + } + + CancellationTokenSource _Cts = new CancellationTokenSource(); + public CancellationToken CancellationToken => _Cts.Token; + public IStringLocalizer StringLocalizer => stringLocalizer; + public ViewLocalizer ViewLocalizer => viewLocalizer; + public IJSRuntime JSRuntime => jsRuntime; + public IServiceProvider ServiceProvider => serviceProvider; + public void Dispose() + { + _Cts.Cancel(); + foreach (var el in Elements.OfType()) + { + el.Dispose(); + } + } + +} diff --git a/BTCPayServer/Blazor/VaultBridge/VaultController.cs b/BTCPayServer/Blazor/VaultBridge/VaultController.cs new file mode 100644 index 000000000..580ae3791 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/VaultController.cs @@ -0,0 +1,55 @@ +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Blazor.VaultBridge.Elements; + +namespace BTCPayServer.Blazor.VaultBridge; + +public abstract class VaultController : IController +{ + protected abstract string VaultUri { get; } + protected abstract Task Run(VaultBridgeUI ui, VaultClient vaultClient, CancellationToken cancellationToken); + + public async Task Run(VaultBridgeUI ui, CancellationToken cancellationToken) + { + try + { + ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Checking BTCPay Server Vault is running..."]); + var client = new VaultClient(ui.JSRuntime, VaultUri); + var res = await client.AskPermission(cancellationToken); + var feedback = (status: res.HttpCode, browser: res.Browser) switch + { + (200, _) => new Feedback(ui.StringLocalizer["Access to vault granted by owner."], FeedbackType.Success), + (401, _) => new Feedback(ui.StringLocalizer["The user declined access to the vault."], + FeedbackType.Failed), + (_, "safari") => new Feedback( + ui.ViewLocalizer[ + "Safari doesn't support BTCPay Server Vault. Please use a different browser. (More information)"], + FeedbackType.Failed), + _ => new Feedback( + ui.ViewLocalizer[ + "BTCPay Server Vault does not seem to be running, you can download it on Github."], + FeedbackType.Failed), + }; + ui.ShowFeedback(feedback); + + if (res.HttpCode != 200) + { + if (res.HttpCode == 0 && res.Browser == "brave") + ui.AddWarning(ui.ViewLocalizer[ + "Brave supports BTCPay Server Vault, but you need to disable Brave Shields. (More information)"]); + ui.ShowRetry(); + return; + } + + await Run(ui, client, cancellationToken); + } + catch (VaultClient.VaultNotConnectedException) + { + ui.ShowFeedback(new Feedback( + ui.ViewLocalizer[ + "BTCPay Server Vault does not seem to be running, you can download it on Github."], + FeedbackType.Failed)); + ui.ShowRetry(); + } + } +} diff --git a/BTCPayServer/Blazor/VaultBridge/VaultElement.cs b/BTCPayServer/Blazor/VaultBridge/VaultElement.cs new file mode 100644 index 000000000..1cbae2201 --- /dev/null +++ b/BTCPayServer/Blazor/VaultBridge/VaultElement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace BTCPayServer.Blazor.VaultBridge; + +public class VaultElement +{ + protected virtual void BuildRenderTree(RenderTreeBuilder builder) { } + + public RenderFragment RenderFragment => BuildRenderTree; +} diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index ccbf244d6..19c68a1f5 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -1019,7 +1019,7 @@ namespace BTCPayServer.Controllers leases.Add(_EventAggregator.SubscribeAsync(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId))); while (true) { - var message = await webSocket.ReceiveAndPingAsync(DummyBuffer); + var message = await webSocket.ReceiveAndPingAsync(DummyBuffer, cancellationToken); if (message.MessageType == WebSocketMessageType.Close) break; } diff --git a/BTCPayServer/Controllers/UIPullPaymentController.Boltcard.cs b/BTCPayServer/Controllers/UIPullPaymentController.Boltcard.cs index d7e38b30e..cd681dfc2 100644 --- a/BTCPayServer/Controllers/UIPullPaymentController.Boltcard.cs +++ b/BTCPayServer/Controllers/UIPullPaymentController.Boltcard.cs @@ -1,16 +1,7 @@ using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.Lightning; using BTCPayServer.Models; -using BTCPayServer.NTag424; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using System; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Html; -using static BTCPayServer.BoltcardDataExtensions; namespace BTCPayServer.Controllers { @@ -20,13 +11,16 @@ namespace BTCPayServer.Controllers [HttpGet("pull-payments/{pullPaymentId}/boltcard/{command}")] public IActionResult SetupBoltcard(string pullPaymentId, string command) { - return View(nameof(SetupBoltcard), new SetupBoltcardViewModel - { - ReturnUrl = Url.Action(nameof(ViewPullPayment), "UIPullPayment", new { pullPaymentId }), - WebsocketPath = Url.Action(nameof(VaultNFCBridgeConnection), "UIPullPayment", new { pullPaymentId }), - Command = command - }); + return View(nameof(SetupBoltcard), + new SetupBoltcardViewModel + { + ReturnUrl = Url.Action(nameof(ViewPullPayment), "UIPullPayment", new { pullPaymentId }), + BoltcardUrl = Url.ActionAbsolute(this.Request, nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard").AbsoluteUri, + NewCard = command == "configure-boltcard", + PullPaymentId = pullPaymentId + }); } + [AllowAnonymous] [HttpPost("pull-payments/{pullPaymentId}/boltcard/{command}")] public IActionResult SetupBoltcardPost(string pullPaymentId, string command) @@ -34,166 +28,5 @@ namespace BTCPayServer.Controllers TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Boltcard is configured"].Value; return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId }); } - - record CardOrigin - { - public record Blank() : CardOrigin; - public record ThisIssuer(BoltcardRegistration Registration) : CardOrigin; - public record ThisIssuerConfigured(string PullPaymentId, BoltcardRegistration Registration) : ThisIssuer(Registration); - public record OtherIssuer() : CardOrigin; - public record ThisIssuerReset(BoltcardRegistration Registration) : ThisIssuer(Registration); - } - - [Route("pull-payments/{pullPaymentId}/nfc/bridge")] - public async Task VaultNFCBridgeConnection(string pullPaymentId) - { - if (!HttpContext.WebSockets.IsWebSocketRequest) - return NotFound(); - var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, false); - if (pp is null) - return NotFound(); - if (!_pullPaymentHostedService.SupportsLNURL(pp)) - return BadRequest(); - - var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard"); - boltcardUrl = Request.GetAbsoluteUri(boltcardUrl); - var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - var vaultClient = new VaultClient(websocket); - var transport = new APDUVaultTransport(vaultClient); - var ntag = new Ntag424(transport); - using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10))) - { -next: - while (websocket.State == System.Net.WebSockets.WebSocketState.Open) - { - try - { - var command = await vaultClient.GetNextCommand(cts.Token); - var permission = await vaultClient.AskPermission(VaultServices.NFC, cts.Token); - if (permission is null) - { - await vaultClient.Show(VaultMessageType.Error, StringLocalizer["BTCPay Server Vault does not seem to be running, you can download it on {0}.", new HtmlString("GitHub")], cts.Token); - goto next; - } - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["BTCPayServer successfully connected to the vault."], cts.Token); - if (permission is false) - { - await vaultClient.Show(VaultMessageType.Error, StringLocalizer["The user declined access to the vault."], cts.Token); - goto next; - } - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Access to vault granted by owner."], cts.Token); - - await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Waiting for NFC to be presented..."], cts.Token); - await transport.WaitForCard(cts.Token); - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["NFC detected."], cts.Token); - - var issuerKey = await _settingsRepository.GetIssuerKey(_env); - CardOrigin cardOrigin = await GetCardOrigin(pullPaymentId, ntag, issuerKey, cts.Token); - - if (cardOrigin is CardOrigin.OtherIssuer) - { - await vaultClient.Show(VaultMessageType.Error, StringLocalizer["This card is already configured for another issuer"], cts.Token); - goto next; - } - - bool success = false; - switch (command) - { - case "configure-boltcard": - await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Configuring Boltcard..."], cts.Token); - if (cardOrigin is CardOrigin.Blank || cardOrigin is CardOrigin.ThisIssuerReset) - { - await ntag.AuthenticateEV2First(0, AESKey.Default, cts.Token); - var uid = await ntag.GetCardUID(); - try - { - var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, uid); - var cardKey = issuerKey.CreatePullPaymentCardKey(uid, version, pullPaymentId); - await ntag.SetupBoltcard(boltcardUrl, BoltcardKeys.Default, cardKey.DeriveBoltcardKeys(issuerKey)); - } - catch - { - await _dbContextFactory.SetBoltcardResetState(issuerKey, uid); - throw; - } - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["The card is now configured"], cts.Token); - } - else if (cardOrigin is CardOrigin.ThisIssuer) - { - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["This card is already properly configured"], cts.Token); - } - success = true; - break; - case "reset-boltcard": - await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Resetting Boltcard..."], cts.Token); - if (cardOrigin is CardOrigin.Blank) - { - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["This card is already in a factory state"], cts.Token); - } - else if (cardOrigin is CardOrigin.ThisIssuer thisIssuer) - { - var cardKey = issuerKey.CreatePullPaymentCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version, pullPaymentId); - await ntag.ResetCard(issuerKey, cardKey); - await _dbContextFactory.SetBoltcardResetState(issuerKey, thisIssuer.Registration.UId); - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Card reset succeed"], cts.Token); - } - success = true; - break; - } - if (success) - { - await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Please remove the NFC from the card reader"], cts.Token); - await transport.WaitForRemoved(cts.Token); - await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Thank you!"], cts.Token); - await vaultClient.SendSimpleMessage("done", cts.Token); - } - } - catch (Exception) when (websocket.State != WebSocketState.Open || cts.IsCancellationRequested) - { - await WebsocketHelper.CloseSocket(websocket); - } - catch (Exception ex) - { - try - { - await vaultClient.Show(VaultMessageType.Error, StringLocalizer["Unexpected error: {0}", ex.Message], ex.ToString(), cts.Token); - } - catch { } - } - } - } - return new EmptyResult(); - } - - private async Task GetCardOrigin(string pullPaymentId, Ntag424 ntag, IssuerKey issuerKey, CancellationToken cancellationToken) - { - CardOrigin cardOrigin; - Uri uri = await ntag.TryReadNDefURI(cancellationToken); - - if (uri is null) - { - cardOrigin = new CardOrigin.Blank(); - } - else - { - var piccData = issuerKey.TryDecrypt(uri); - if (piccData is null) - { - cardOrigin = new CardOrigin.OtherIssuer(); - } - else - { - var res = await _dbContextFactory.GetBoltcardRegistration(issuerKey, piccData.Uid); - if (res != null && res.PullPaymentId is null) - cardOrigin = new CardOrigin.ThisIssuerReset(res); - else if (res?.PullPaymentId != pullPaymentId) - cardOrigin = new CardOrigin.OtherIssuer(); - else - cardOrigin = new CardOrigin.ThisIssuerConfigured(res.PullPaymentId, res); - } - } - - return cardOrigin; - } } } diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs index 463089d26..c4b65e31d 100644 --- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs +++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs @@ -152,7 +152,7 @@ public partial class UIStoresController { try { - strategy = handler.ParsePaymentMethodConfig(JToken.Parse(UnprotectString(vm.Config))); + strategy = handler.ParsePaymentMethodConfig(JToken.Parse(_dataProtector.UnprotectString(vm.Config))); } catch { @@ -167,7 +167,7 @@ public partial class UIStoresController return View(vm.ViewName, vm); } - vm.Config = ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString()); + vm.Config = _dataProtector.ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString()); ModelState.Remove(nameof(vm.Config)); var storeBlob = store.GetStoreBlob(); @@ -197,15 +197,6 @@ public partial class UIStoresController return ConfirmAddresses(vm, strategy, network.NBXplorerNetwork); } - private string ProtectString(string str) - { - return Convert.ToBase64String(_dataProtector.Protect(Encoding.UTF8.GetBytes(str))); - } - private string UnprotectString(string str) - { - return Encoding.UTF8.GetString(_dataProtector.Unprotect(Convert.FromBase64String(str))); - } - [HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task GenerateWallet(WalletSetupViewModel vm) @@ -278,7 +269,6 @@ public partial class UIStoresController Network = network, Source = isImport ? "SeedImported" : "NBXplorerGenerated", IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet, - DerivationSchemeFormat = "BTCPay", SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot, SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit }; @@ -329,7 +319,7 @@ public partial class UIStoresController vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(); vm.AccountKey = response.AccountHDKey.Neuter().ToWif(); vm.KeyPath = response.AccountKeyPath.KeyPath.ToString(); - vm.Config = ProtectString(JToken.FromObject(derivationSchemeSettings, handler.Serializer).ToString()); + vm.Config = _dataProtector.ProtectString(JToken.FromObject(derivationSchemeSettings, handler.Serializer).ToString()); var result = await UpdateWallet(vm); @@ -434,7 +424,7 @@ public partial class UIStoresController MasterFingerprint = e.RootFingerprint is { } fp ? fp.ToString() : null, AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}" }).ToList(), - Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()), + Config = _dataProtector.ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()), PayJoinEnabled = storeBlob.PayJoinEnabled, CanUsePayJoin = perm.CanCreateHotWallet && network.SupportPayJoin && derivation.IsHotWallet, CanUseHotWallet = perm.CanCreateHotWallet, @@ -717,8 +707,8 @@ public partial class UIStoresController var derivation = line.Derive(i); var address = network.CreateAddress(strategy.AccountDerivation, line.KeyPathTemplate.GetKeyPath(i), - derivation.ScriptPubKey).ToString(); - vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath)); + derivation.ScriptPubKey); + vm.AddressSamples.Add((keyPath.ToString(), address.ToString(), rootedKeyPath)); } } diff --git a/BTCPayServer/Controllers/UIVaultController.cs b/BTCPayServer/Controllers/UIVaultController.cs deleted file mode 100644 index e2bd9bec6..000000000 --- a/BTCPayServer/Controllers/UIVaultController.cs +++ /dev/null @@ -1,432 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Client; -using BTCPayServer.Data; -using BTCPayServer.Hwi; -using BTCPayServer.ModelBinders; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Services.Invoices; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using NBitcoin; -using NBXplorer.DerivationStrategy; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Controllers -{ - [Route("vault")] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)] - public class UIVaultController : Controller - { - private readonly PaymentMethodHandlerDictionary _handlers; - private readonly IAuthorizationService _authorizationService; - - public UIVaultController(PaymentMethodHandlerDictionary handlers, IAuthorizationService authorizationService) - { - _handlers = handlers; - _authorizationService = authorizationService; - } - - [Route("{cryptoCode}/xpub")] - [Route("wallets/{walletId}/xpub")] - public async Task VaultBridgeConnection(string cryptoCode = null, - [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId = null) - { - if (!HttpContext.WebSockets.IsWebSocketRequest) - return NotFound(); - cryptoCode = cryptoCode ?? walletId.CryptoCode; - bool versionChecked = false; - using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10))) - { - var cancellationToken = cts.Token; - if (!_handlers.TryGetValue(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), out var h) || h is not IHasNetwork { Network: var network }) - return NotFound(); - var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - var vaultClient = new VaultClient(websocket); - var hwi = new Hwi.HwiClient(network.NBitcoinNetwork) - { - Transport = new VaultHWITransport(vaultClient) - }; - Hwi.HwiDeviceClient device = null; - HwiEnumerateEntry deviceEntry = null; - HDFingerprint? fingerprint = null; - string password = null; - var websocketHelper = new WebSocketHelper(websocket); - async Task FetchFingerprint() - { - fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint; - device = new HwiDeviceClient(hwi, DeviceSelectors.FromFingerprint(fingerprint.Value), deviceEntry.Model, fingerprint) { Password = password }; - } - async Task RequireDeviceUnlocking() - { - if (deviceEntry == null) - { - await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); - return true; - } - if (deviceEntry.Code is HwiErrorCode.DeviceNotInitialized) - { - await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken); - return true; - } - if (deviceEntry.Code is HwiErrorCode.DeviceNotReady) - { - if (IsTrezorT(deviceEntry)) - { - await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); - return true; - } - else if (deviceEntry.NeedsPinSent is true) - { - await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); - return true; - } - else if (deviceEntry.NeedsPassphraseSent is true && password is null) - { - await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken); - return true; - } - } - if (IsTrezorOne(deviceEntry) && password is null) - { - fingerprint = null; // There will be a new fingerprint - device = new HwiDeviceClient(hwi, DeviceSelectors.FromDeviceType("trezor", deviceEntry.Path), deviceEntry.Model, null); - await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken); - return true; - } - return false; - } - - JObject o = null; - try - { - while (true) - { - var command = await websocketHelper.NextMessageAsync(cancellationToken); - switch (command) - { - case "set-passphrase": - device.Password = await websocketHelper.NextMessageAsync(cancellationToken); - password = device.Password; - break; - case "ask-sign": - if (await RequireDeviceUnlocking()) - { - continue; - } - if (walletId == null) - { - await websocketHelper.Send("{ \"error\": \"invalid-walletId\"}", cancellationToken); - continue; - } - if (fingerprint is null) - { - await FetchFingerprint(); - } - await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken); - o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); - var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings); - if (!authorization.Succeeded) - { - await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken); - continue; - } - var psbt = PSBT.Parse(o["psbt"].Value(), network.NBitcoinNetwork); - var derivationSettings = GetDerivationSchemeSettings(walletId); - derivationSettings.RebaseKeyPaths(psbt); - - // we ensure that the device fingerprint is part of the derivation settings - if (derivationSettings.AccountKeySettings.All(a => a.RootFingerprint != fingerprint)) - { - await websocketHelper.Send("{ \"error\": \"wrong-wallet\"}", cancellationToken); - continue; - } - - // otherwise, let the device check if it can sign anything - var signableInputs = psbt.Inputs - .SelectMany(i => i.HDKeyPaths) - .Where(i => i.Value.MasterFingerprint == fingerprint) - .ToArray(); - if (signableInputs.Length > 0) - { - var actualPubKey = (await device.GetXPubAsync(signableInputs[0].Value.KeyPath)).GetPublicKey(); - if (actualPubKey != signableInputs[0].Key) - { - await websocketHelper.Send("{ \"error\": \"wrong-keypath\"}", cancellationToken); - continue; - } - - if (derivationSettings.IsMultiSigOnServer) - { - var alreadySigned = psbt.Inputs.Any(a => - a.PartialSigs.Any(a => a.Key == actualPubKey)); - if (alreadySigned) - { - await websocketHelper.Send("{ \"error\": \"already-signed-psbt\"}", cancellationToken); - continue; - } - } - } - - try - { - psbt = await device.SignPSBTAsync(psbt, cancellationToken); - } - catch (HwiException) - { - await websocketHelper.Send("{ \"error\": \"user-reject\"}", cancellationToken); - continue; - } - o = new JObject(); - o.Add("psbt", psbt.ToBase64()); - await websocketHelper.Send(o.ToString(), cancellationToken); - break; - case "display-address": - if (await RequireDeviceUnlocking()) - { - continue; - } - var k = RootedKeyPath.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); - await device.DisplayAddressAsync(GetScriptPubKeyType(k), k.KeyPath, cancellationToken); - await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken); - break; - case "ask-pin": - if (device == null) - { - await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); - continue; - } - try - { - await device.PromptPinAsync(cancellationToken); - } - catch (HwiException ex) when (ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked) - { - await websocketHelper.Send("{ \"error\": \"device-already-unlocked\"}", cancellationToken); - continue; - } - await websocketHelper.Send("{ \"info\": \"prompted, please input the pin\"}", cancellationToken); - var pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture); - if (await device.SendPinAsync(pin, cancellationToken)) - { - goto askdevice; - } - else - { - await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken); - continue; - } - case "ask-xpub": - if (await RequireDeviceUnlocking()) - { - continue; - } - await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken); - var askedXpub = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); - var addressType = askedXpub["addressType"].Value(); - var accountNumber = askedXpub["accountNumber"].Value(); - JObject result = new JObject(); - var factory = network.NBXplorerNetwork.DerivationStrategyFactory; - if (fingerprint is null) - { - await FetchFingerprint(); - } - result["fingerprint"] = fingerprint.Value.ToString(); - - DerivationStrategyBase strategy = null; - - KeyPath keyPath = (addressType switch - { - "taproot" => new KeyPath("86'"), - "segwit" => new KeyPath("84'"), - "segwitWrapped" => new KeyPath("49'"), - "legacy" => new KeyPath("44'"), - _ => null - })?.Derive(network.CoinType).Derive(accountNumber, true); - if (keyPath is null) - { - await websocketHelper.Send("{ \"error\": \"invalid-addresstype\"}", cancellationToken); - continue; - } - BitcoinExtPubKey xpub = await device.GetXPubAsync(keyPath); - if (!network.NBitcoinNetwork.Consensus.SupportSegwit && addressType != "legacy") - { - await websocketHelper.Send("{ \"error\": \"segwit-notsupported\"}", cancellationToken); - continue; - } - - if (!network.NBitcoinNetwork.Consensus.SupportTaproot && addressType == "taproot") - { - await websocketHelper.Send("{ \"error\": \"taproot-notsupported\"}", cancellationToken); - continue; - } - if (addressType == "taproot") - { - strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() - { - ScriptPubKeyType = ScriptPubKeyType.TaprootBIP86 - }); - } - else if (addressType == "segwit") - { - strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() - { - ScriptPubKeyType = ScriptPubKeyType.Segwit - }); - } - else if (addressType == "segwitWrapped") - { - strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() - { - ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH - }); - } - else if (addressType == "legacy") - { - strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() - { - ScriptPubKeyType = ScriptPubKeyType.Legacy - }); - } - - result.Add(new JProperty("strategy", strategy.ToString())); - result.Add(new JProperty("accountKey", xpub.ToString())); - result.Add(new JProperty("keyPath", keyPath.ToString())); - await websocketHelper.Send(result.ToString(), cancellationToken); - break; - case "ask-passphrase": - if (command == "ask-passphrase") - { - if (deviceEntry == null) - { - await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); - continue; - } - // The make the trezor T ask for password - await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); - } - goto askdevice; - case "ask-device": -askdevice: - if (!versionChecked) - { - var version = await hwi.GetVersionAsync(cancellationToken); - if (version.Major < 2) - { - await websocketHelper.Send("{ \"error\": \"vault-outdated\"}", cancellationToken); - continue; - } - versionChecked = true; - } - password = null; - deviceEntry = null; - device = null; - var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList(); - deviceEntry = entries.FirstOrDefault(); - if (deviceEntry == null) - { - await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken); - continue; - } - var model = deviceEntry.Model ?? "Unsupported hardware wallet, try to update BTCPay Server Vault"; - device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, model, deviceEntry.Fingerprint); - fingerprint = device.Fingerprint; - JObject json = new JObject(); - json.Add("model", model); - await websocketHelper.Send(json.ToString(), cancellationToken); - break; - } - } - } - catch (FormatException ex) - { - JObject obj = new JObject(); - obj.Add("error", "invalid-network"); - obj.Add("details", ex.ToString()); - try - { - await websocketHelper.Send(obj.ToString(), cancellationToken); - } - catch { } - } - catch (Exception ex) - { - JObject obj = new JObject(); - obj.Add("error", "unknown-error"); - obj.Add("message", ex.Message); - obj.Add("details", ex.ToString()); - try - { - await websocketHelper.Send(obj.ToString(), cancellationToken); - } - catch { } - } - finally - { - await websocketHelper.DisposeAsync(cancellationToken); - } - } - return new EmptyResult(); - } - - private ScriptPubKeyType GetScriptPubKeyType(RootedKeyPath keyPath) - { - var path = keyPath.KeyPath.ToString(); - if (path.StartsWith("86'", StringComparison.OrdinalIgnoreCase)) - return ScriptPubKeyType.TaprootBIP86; - if (path.StartsWith("84'", StringComparison.OrdinalIgnoreCase)) - return ScriptPubKeyType.Segwit; - if (path.StartsWith("49'", StringComparison.OrdinalIgnoreCase)) - return ScriptPubKeyType.SegwitP2SH; - if (path.StartsWith("44'", StringComparison.OrdinalIgnoreCase)) - return ScriptPubKeyType.Legacy; - throw new NotSupportedException("Unsupported keypath"); - } - - private bool SameSelector(DeviceSelector a, DeviceSelector b) - { - var aargs = new List(); - a.AddArgs(aargs); - var bargs = new List(); - b.AddArgs(bargs); - if (aargs.Count != bargs.Count) - return false; - for (int i = 0; i < aargs.Count; i++) - { - if (aargs[i] != bargs[i]) - return false; - } - return true; - } - - private static bool IsTrezorT(HwiEnumerateEntry deviceEntry) - { - return deviceEntry.Model.Contains("Trezor_T", StringComparison.OrdinalIgnoreCase); - } - private static bool IsTrezorOne(HwiEnumerateEntry deviceEntry) - { - return deviceEntry.Model.Contains("trezor_1", StringComparison.OrdinalIgnoreCase); - } - - public StoreData CurrentStore - { - get - { - return HttpContext.GetStoreData(); - } - } - - private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId) - { - var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode); - return CurrentStore.GetPaymentMethodConfig(pmi, _handlers); - } - } -} diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 191af5190..bc60ea54e 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -1324,7 +1324,7 @@ namespace BTCPayServer.Controllers vm.Outputs.Last().Labels = vm.Outputs.Last().Labels.Concat(addressLabels.Select(tuple => tuple.Label)).ToArray(); } } - + private IActionResult ViewVault(WalletId walletId, WalletPSBTViewModel vm) { return View(nameof(WalletSendVault), @@ -1332,8 +1332,6 @@ namespace BTCPayServer.Controllers { SigningContext = vm.SigningContext, WalletId = walletId.ToString(), - WebsocketPath = Url.Action(nameof(UIVaultController.VaultBridgeConnection), "UIVault", - new { walletId = walletId.ToString() }), ReturnUrl = vm.ReturnUrl, BackUrl = vm.BackUrl }); diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index ede9a3670..fa9599430 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -21,6 +21,7 @@ using BTCPayServer.BIP78.Sender; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.HostedServices; +using BTCPayServer.Hwi; using BTCPayServer.Lightning; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; @@ -34,6 +35,7 @@ using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Reporting; using BTCPayServer.Services.Wallets; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; @@ -52,6 +54,39 @@ namespace BTCPayServer { public static class Extensions { + public static string GetNiceModelName(this HwiDeviceClient device) + => device.Model switch + { + "trezor_1" => "Trezor Model One", + "trezor_t" => "Trezor Model T", + "trezor_r" => "Trezor Model R", + "coldcard" => "Coldcard", + "coldcard_simulator" => "Coldcard (Simulator)", + "trezor_safe 3" => "Trezor Safe 3", + "trezor_safe 5" => "Trezor Safe 5", + "ledger_nano_s" => "Ledger Nano S", + "ledger_nano_x" => "Ledger Nano X", + "ledger_stax" => "Ledger Stax", + "ledger_flex" => "Ledger Flex", + "keepkey" => "KeepKey", + "digitalbitbox_01" or "digitalbitbox" => "Digital Bitbox", + "digitalbitbox_01_simulator" => "Digital Bitbox (Simulator)", + "bitbox02_multi" => "BitBox02 Multi", + "bitbox02_btconly" => "BitBox02 Bitcoin Only", + "ledger_nano_s_plus" => "Ledger Nano S Plus", + "jade" => "Jade", + _ => device.Model + }; + public static string ProtectString(this IDataProtector protector,string str) + { + return Convert.ToBase64String(protector.Protect(Encoding.UTF8.GetBytes(str))); + } + public static string UnprotectString(this IDataProtector protector, string str) + { + return Encoding.UTF8.GetString(protector.Unprotect(Convert.FromBase64String(str))); + } + + /// /// Outputs a serializer which will serialize default and null members. /// This is useful for discovering the API. diff --git a/BTCPayServer/Models/SetupBoltcardViewModel.cs b/BTCPayServer/Models/SetupBoltcardViewModel.cs index c45d0774e..9b9166d88 100644 --- a/BTCPayServer/Models/SetupBoltcardViewModel.cs +++ b/BTCPayServer/Models/SetupBoltcardViewModel.cs @@ -3,7 +3,8 @@ namespace BTCPayServer.Models public class SetupBoltcardViewModel { public string ReturnUrl { get; set; } - public string WebsocketPath { get; set; } - public string Command { get; set; } + public string BoltcardUrl { get; set; } + public bool NewCard { get; set; } + public string PullPaymentId { get; set; } } } diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index 7dc824fda..2d36b6b40 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -15,7 +15,6 @@ namespace BTCPayServer.Models.StoreViewModels { get; set; } = new List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)>(); - public string CryptoCode { get; set; } public string KeyPath { get; set; } [Display(Name = "Root fingerprint")] @@ -28,8 +27,6 @@ namespace BTCPayServer.Models.StoreViewModels public string WalletFileContent { get; set; } public string Config { get; set; } public string Source { get; set; } - [Display(Name = "Derivation scheme format")] - public string DerivationSchemeFormat { get; set; } [Display(Name = "Account key")] public string AccountKey { get; set; } public BTCPayNetwork Network { get; set; } diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendVaultModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendVaultModel.cs index 010ae9d0c..fd6c9aca1 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendVaultModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendVaultModel.cs @@ -3,7 +3,6 @@ namespace BTCPayServer.Models.WalletViewModels public class WalletSendVaultModel : IHasBackAndReturnUrl { public string WalletId { get; set; } - public string WebsocketPath { get; set; } public string BackUrl { get; set; } public string ReturnUrl { get; set; } public SigningContextModel SigningContext { get; set; } = new(); diff --git a/BTCPayServer/Services/Translations.Default.cs b/BTCPayServer/Services/Translations.Default.cs index 8b4e38b53..011ea6521 100644 --- a/BTCPayServer/Services/Translations.Default.cs +++ b/BTCPayServer/Services/Translations.Default.cs @@ -83,6 +83,7 @@ namespace BTCPayServer.Services "Account key path": "", "Account successfully created.": "", "Account successfully deleted.": "", + "Action canceled by user": "", "Actions": "", "Active": "", "Add": "", @@ -108,7 +109,7 @@ namespace BTCPayServer.Services "Additional text to provide an explanation for the field": "", "Address": "", "Address type": "", - "Address verification": "", + "Address verified.": "", "Adjust the design of your BTCPay Server instance to your needs.": "", "Adjusts the generated invoice amount — use as a prefix to have multiple adjustment fields": "", "Adjusts the generated invoice amount by multiplying with this value — use as a prefix to have multiple adjustment fields": "", @@ -146,6 +147,7 @@ namespace BTCPayServer.Services "An invitation email has been sent.
You may alternatively share this link with them: {0}": "", "An invitation email has not been sent, because the server does not have an email server configured.
You need to share this link with them: {0}": "", "An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.": "", + "An unexpected error happened: {0}": "", "Animation": "", "Any amount": "", "Any uploaded files are being saved on the same machine that hosts BTCPay; please pay attention to your storage space.": "", @@ -208,6 +210,7 @@ namespace BTCPayServer.Services "Boltcard is configured": "", "Brand Color": "", "Branding": "", + "Brave supports BTCPay Server Vault, but you need to disable Brave Shields. (More information)": "", "Broadcast (Payjoin)": "", "Broadcast (Simple)": "", "Broadcast transaction": "", @@ -218,9 +221,8 @@ namespace BTCPayServer.Services "BTCPay Server Configurator": "", "BTCPay Server currently supports:": "", "BTCPay Server Supporters": "", - "BTCPay Server Vault does not seem to be running, you can download it on {0}.": "", + "BTCPay Server Vault does not seem to be running, you can download it on Github.": "", "BTCPay will restart momentarily.": "", - "BTCPayServer successfully connected to the vault.": "", "Bump fee": "", "But now, what if you want to support DOGE? The problem with DOGE is that most exchange do not have any pair for it. But bitpay has a DOGE_BTC pair.
Luckily, the rule engine allow you to reference rules:": "", "Button Type": "", @@ -254,6 +256,8 @@ namespace BTCPayServer.Services "Cheat Mode: Send funds to this wallet": "", "Check if NFC is supported and enabled on this device": "", "Check releases on GitHub and notify when new BTCPay Server version is available": "", + "Checking BTCPay Server Vault is running...": "", + "Checking if this device can sign the transaction...": "", "Checkout": "", "Checkout Additional Query String": "", "Checkout Appearance": "", @@ -302,7 +306,6 @@ namespace BTCPayServer.Services "Confirm new password": "", "Confirm passphrase": "", "Confirm password": "", - "Confirm that you see the following address on the device:": "", "Confirmations": "", "confirmed": "", "Connect an existing wallet": "", @@ -433,7 +436,6 @@ namespace BTCPayServer.Services "Dependencies": "", "Dependencies not met.": "", "Derivation scheme": "", - "Derivation scheme format": "", "Description": "", "Description template of the lightning invoice": "", "Destination": "", @@ -442,6 +444,7 @@ namespace BTCPayServer.Services "Detects the language of the customer's browser.": "", "Determine the generated invoice amount": "", "Determine the generated invoice currency": "", + "Device found: {0}": "", "Dictionaries": "", "Dictionaries enable you to translate the BTCPay Server backend into different languages.": "", "Dictionary": "", @@ -558,6 +561,8 @@ namespace BTCPayServer.Services "Enter destination to claim funds": "", "Enter extended public key": "", "Enter the code in the confirmation box below.": "", + "Enter the passphrase.": "", + "Enter the pin.": "", "Enter the wallet seed": "", "Enter wallet seed": "", "Enter your extended public key": "", @@ -591,6 +596,9 @@ namespace BTCPayServer.Services "Fee rate": "", "Fee rate (sat/vB)": "", "Fee will be shown for BTC and LTC onchain payments only.": "", + "Fetching device...": "", + "Fetching public keys...": "", + "Fetching wallet's fingerprint.": "", "FIDO2 Authentication": "", "Field to mirror": "", "File Id": "", @@ -707,6 +715,7 @@ namespace BTCPayServer.Services "In use": "", "In-store": "", "Include archived": "", + "Incorrect pin code.": "", "Increase the security of your instance by disabling the ability to change the SSH settings in this BTCPay Server instance's user interface.": "", "Index": "", "Input the key string manually": "", @@ -726,6 +735,7 @@ namespace BTCPayServer.Services "Invalid login attempt.": "", "Invalid network": "", "Invalid passphrase confirmation": "", + "Invalid password confirmation.": "", "Invalid payout method": "", "Invalid PSBT": "", "Invalid role": "", @@ -875,6 +885,7 @@ namespace BTCPayServer.Services "No contributions allowed after the goal has been reached": "", "No contributions have been made yet.": "", "No deliveries for this webhook yet": "", + "No device connected.": "", "No documentation": "", "No end date has been set": "", "No expiry date has been set for this payment request": "", @@ -959,6 +970,7 @@ namespace BTCPayServer.Services "Passphrase confirmation": "", "Password": "", "Password (leave blank to generate invite-link)": "", + "Password entered...": "", "Password Reset": "", "Password successfully set": "", "Password successfully set.": "", @@ -1014,6 +1026,7 @@ namespace BTCPayServer.Services "Percentage must be a numeric value between 0 and 100": "", "Permanent Url": "", "Permissions": "", + "Pin code verified.": "", "Placeholder": "", "Placeholders": "", "Please check that your wallet is generating the same addresses as below.": "", @@ -1034,9 +1047,13 @@ namespace BTCPayServer.Services "Please provide your existing seed": "", "Please provide your extended public key": "", "Please remove the NFC from the card reader": "", + "Please review and confirm the transaction on your device...": "", "Please select an option before proceeding": "", "Please set NBXPlorer's PostgreSQL connection string to make this feature available.": "", + "Please verify that the address displayed on your device is {0}...": "", "Please wait for your node to be synched": "", + "Please, confirm on the device first...": "", + "Please, enter the passphrase on the device.": "", "Plugin action cancelled.": "", "Plugin scheduled to be installed.": "", "Plugin scheduled to be uninstalled.": "", @@ -1075,7 +1092,7 @@ namespace BTCPayServer.Services "PSBT to combine with…": "", "PSBT updated!": "", "Public Key": "", - "Public Key Information": "", + "Public keys successfully fetched.": "", "Public Node Info": "", "Pull payment archived": "", "Pull payment request created": "", @@ -1191,12 +1208,14 @@ namespace BTCPayServer.Services "Roles": "", "Root fingerprint": "", "RPC Error while broadcasting: {0}": "", + "Safari doesn't support BTCPay Server Vault. Please use a different browser. (More information)": "", "sale": "", "sales": "", "Sales": "", "Save": "", "Save comment": "", "Save Wallet Settings": "", + "Saving...": "", "Scan destination with camera": "", "Scan Login code with camera": "", "Scan QR code": "", @@ -1239,6 +1258,7 @@ namespace BTCPayServer.Services "Select the payout method used for refund": "", "Select the store to grant permission for": "", "Select this contribution perk": "", + "Select your address type and account": "", "selected": "", "Send": "", "Send {0}": "", @@ -1288,7 +1308,6 @@ namespace BTCPayServer.Services "Show current info": "", "Show export QR": "", "Show multi-sig examples": "", - "Show on device": "", "Show plugins in pre-release": "", "Show QR": "", "Show QR Code": "", @@ -1368,8 +1387,8 @@ namespace BTCPayServer.Services "Switch date format": "", "Syntax error": "", "System": "", + "Taproot": "", "Taproot (For advanced users)": "", - "Taproot (ONLY FOR DEVELOPMENT)": "", "Target Amount": "", "Template": "", "Template JSON": "", @@ -1411,6 +1430,7 @@ namespace BTCPayServer.Services "The crypto currency price, at the rate the invoice got paid.": "", "The currency to generate the invoice in when generated through this lightning address": "", "The custom color, logo and CSS are applied on the public/customer-facing pages (Invoice, Payment Request, Pull Payment, etc.). The brand color is used as the accent color for buttons, links, etc. It might get adapted to fit the light/dark color scheme.": "", + "The device has not been initialized.": "", "The DNS record has been refreshed:": "", "The Dynamic DNS has been successfully queried, your configuration is saved": "", "The Dynamic DNS service has been disabled": "", @@ -1515,6 +1535,9 @@ namespace BTCPayServer.Services "This card is already in a factory state": "", "This card is already properly configured": "", "This crowdfund page is not publicly viewable!": "", + "This device already signed PSBT.": "", + "This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)": "", + "This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)": "", "This feature is disabled": "", "This feature is only available to BTC wallets": "", "This full node does not support rescan of the UTXO set": "", @@ -1582,6 +1605,7 @@ namespace BTCPayServer.Services "Transaction Details": "", "Transaction fee rate:": "", "Transaction Id": "", + "Transaction signed successfully, proceeding to review...": "", "transactions": "", "Translations": "", "Translations are formatted as JSON; for example, {0} translates {1} to {2}.": "", @@ -1596,7 +1620,7 @@ namespace BTCPayServer.Services "Unarchive this payment request": "", "Unarchive this store": "", "Unavailable": "", - "Unexpected error: {0}": "", + "Unexpected address returned by the device...": "", "Unexpected payjoin error: {0}": "", "Unify on-chain and lightning payment URL/QR code": "", "Uninstall": "", @@ -1608,6 +1632,7 @@ namespace BTCPayServer.Services "Unnamed Store": "", "Unread": "", "Unsupported exchange": "", + "Unsupported hardware wallet, try to update BTCPay Server Vault": "", "Unusual": "", "Update": "", "Update Crowdfund": "", @@ -1681,6 +1706,7 @@ namespace BTCPayServer.Services "Verification email sent. Please check your email.": "", "Verify": "", "Verify that the orderId is from your backend, that the price is correct and that status is settled": "", + "Verifying pin...": "", "via HTTPS": "", "via TCP or unix domain socket connection": "", "via the REST API": "", @@ -1696,6 +1722,7 @@ namespace BTCPayServer.Services "Wallet file content": "", "Wallet Recovery Seed": "", "Wallet settings for {0} have been updated.": "", + "Wallet's fingerprint fetched.": "", "Wallet's private key is erased from the server. Higher security. To spend, you have to manually input the private key or import it into an external wallet.": "", "Wallet's private key is stored on the server. Spending the funds you received is convenient. To minimize the risk of theft, regularly withdraw funds to a different wallet.": "", "Wallets": "", @@ -1763,6 +1790,7 @@ namespace BTCPayServer.Services "Your account will no longer have this Lightning wallet as an option for two-factor authentication.": "", "Your account will no longer have this security device as an option for two-factor authentication.": "", "Your available balance is": "", + "Your BTCPay Server Vault version is outdated. Please download the latest version.": "", "Your dynamic DNS hostname": "", "Your email has been confirmed.": "", "Your email has been confirmed. Please set your password.": "", diff --git a/BTCPayServer/VaultClient.cs b/BTCPayServer/VaultClient.cs index 54a5b153f..59afabcba 100644 --- a/BTCPayServer/VaultClient.cs +++ b/BTCPayServer/VaultClient.cs @@ -1,128 +1,56 @@ -#nullable enable +#nullable enable using System; -using System.Net.WebSockets; -using System.Runtime.CompilerServices; +using System.Linq; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; -using Amazon.S3.Model.Internal.MarshallTransformations; +using Microsoft.JSInterop; using Newtonsoft.Json.Linq; -namespace BTCPayServer +namespace BTCPayServer; + +public class VaultClient(IJSRuntime js, string serviceUri) { - public enum VaultMessageType + public class VaultNotConnectedException() : Exception("Vault not connected"); + + public class VaultException(string message) : Exception(message); + + public async Task AskPermission(CancellationToken cancellationToken) { - Ok, - Error, - Processing + return await js.InvokeAsync("vault.askVaultPermission", cancellationToken, serviceUri); } - public enum VaultServices + public async Task SendVaultRequest(string? path, JObject? body, CancellationToken cancellationToken) { - HWI, - NFC + var isAbsolute = path is not null && Uri.IsWellFormedUriString(path, UriKind.Absolute); + var query = new JsonObject() + { + ["uri"] = isAbsolute ? path : serviceUri + path + }; + if (body is not null) + query["body"] = JsonObject.Parse(body.ToString()); + var resp = await js.InvokeAsync("vault.sendRequest", cancellationToken, query); + if (resp.HttpCode is not { } p) + throw new VaultNotConnectedException(); + if (p != 200) + throw new VaultException($"Unexpected response code from vault {p}"); + return (resp.Body)?.ToJsonString() is { } str ? JToken.Parse(str) : null; } - public class VaultNotConnectedException : VaultException + public class SendRequestResponse { - public VaultNotConnectedException() : base("BTCPay Vault isn't connected") - { - - } + public int? HttpCode { get; set; } + public JsonNode? Body { get; set; } } - public class VaultException : Exception + public class HwiResponse { - public VaultException(string message) : base(message) - { - - } - } - public class VaultClient - { - public VaultClient(WebSocket websocket) - { - Websocket = new WebSocketHelper(websocket); - } - - public WebSocketHelper Websocket { get; } - - public async Task GetNextCommand(CancellationToken cancellationToken) - { - return await Websocket.NextMessageAsync(cancellationToken); - } - - public async Task SendMessage(JObject mess, CancellationToken cancellationToken) - { - await Websocket.Send(mess.ToString(), cancellationToken); - } - - public Task Show(VaultMessageType type, string message, CancellationToken cancellationToken) - { - return Show(type, message, null, cancellationToken); - } - public async Task Show(VaultMessageType type, string message, string? debug, CancellationToken cancellationToken) - { - - await SendMessage(new JObject() - { - ["command"] = "showMessage", - ["message"] = message, - ["type"] = type.ToString(), - ["debug"] = debug - }, cancellationToken); - } - - string? _ServiceUri; - public async Task AskPermission(VaultServices service, CancellationToken cancellationToken) - { - var uri = service switch - { - VaultServices.HWI => "http://127.0.0.1:65092/hwi-bridge/v1", - VaultServices.NFC => "http://127.0.0.1:65092/nfc-bridge/v1", - _ => throw new NotSupportedException() - }; - - await this.SendMessage(new JObject() - { - ["command"] = "sendRequest", - ["uri"] = uri + "/request-permission" - }, cancellationToken); - var result = await GetNextMessage(cancellationToken); - if (result["httpCode"] is { } p) - { - var ok = p.Value() == 200; - if (ok) - _ServiceUri = uri; - return ok; - } - return null; - } - - public async Task SendVaultRequest(string? path, JObject? body, CancellationToken cancellationToken) - { - var isAbsolute = path is not null && Uri.IsWellFormedUriString(path, UriKind.Absolute); - var query = new JObject() - { - ["command"] = "sendRequest", - ["uri"] = isAbsolute ? path : _ServiceUri + path - }; - if (body is not null) - query["body"] = body; - await this.SendMessage(query, cancellationToken); - var resp = await GetNextMessage(cancellationToken); - if (resp["httpCode"] is not { } p) - throw new VaultNotConnectedException(); - if (p.Value() != 200) - throw new VaultException($"Unexpected response code from vault {p.Value()}"); - return resp["body"] as JToken; - } - - public async Task GetNextMessage(CancellationToken cancellationToken) - { - return JObject.Parse(await this.Websocket.NextMessageAsync(cancellationToken)); - } - - public Task SendSimpleMessage(string command, CancellationToken cancellationToken) - { - return SendMessage(new JObject() { ["command"] = command }, cancellationToken); - } + public int HttpCode { get; set; } + public string? Error { get; set; } + public JsonNode? Body { get; set; } } } + +public class VaultPermissionResult +{ + public int HttpCode { get; set; } + public string? Browser { get; set; } +} diff --git a/BTCPayServer/VaultHWITransport.cs b/BTCPayServer/VaultHWITransport.cs index 143e42d18..26263c644 100644 --- a/BTCPayServer/VaultHWITransport.cs +++ b/BTCPayServer/VaultHWITransport.cs @@ -1,26 +1,22 @@ -using System.Net.WebSockets; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -namespace BTCPayServer -{ - public class VaultHWITransport : Hwi.Transports.ITransport - { - private readonly VaultClient _vaultClient; +namespace BTCPayServer; - public VaultHWITransport(VaultClient vaultClient) +public class VaultHWITransport : Hwi.Transports.ITransport +{ + private readonly VaultClient _client; + + public VaultHWITransport(VaultClient client) + { + _client = client; + } + public async Task SendCommandAsync(string[] arguments, CancellationToken cancellationToken) + { + return (await _client.SendVaultRequest(null, new JObject() { - _vaultClient = vaultClient; - } - public async Task SendCommandAsync(string[] arguments, CancellationToken cancel) - { - var resp = await _vaultClient.SendVaultRequest("http://127.0.0.1:65092/hwi-bridge/v1", - new JObject() - { - ["params"] = new JArray(arguments) - }, cancel); - return (string)((JValue)resp).Value; - } + ["params"] = new JArray(arguments) + }, cancellationToken)).Value() ?? ""; } } diff --git a/BTCPayServer/Views/Shared/LocalhostBrowserSupport.cshtml b/BTCPayServer/Views/Shared/LocalhostBrowserSupport.cshtml deleted file mode 100644 index 49aecdfbb..000000000 --- a/BTCPayServer/Views/Shared/LocalhostBrowserSupport.cshtml +++ /dev/null @@ -1,22 +0,0 @@ - - diff --git a/BTCPayServer/Views/Shared/VaultElements.cshtml b/BTCPayServer/Views/Shared/VaultElements.cshtml deleted file mode 100644 index 9ffd44028..000000000 --- a/BTCPayServer/Views/Shared/VaultElements.cshtml +++ /dev/null @@ -1,69 +0,0 @@ - diff --git a/BTCPayServer/Views/Shared/_LayoutWizard.cshtml b/BTCPayServer/Views/Shared/_LayoutWizard.cshtml index 756fd919d..ba0e71eb4 100644 --- a/BTCPayServer/Views/Shared/_LayoutWizard.cshtml +++ b/BTCPayServer/Views/Shared/_LayoutWizard.cshtml @@ -1,6 +1,5 @@ @{ Layout = "_LayoutSimple"; - ViewData.SetBlazorAllowed(false); } @section PageHeadContent { diff --git a/BTCPayServer/Views/UIPullPayment/SetupBoltcard.cshtml b/BTCPayServer/Views/UIPullPayment/SetupBoltcard.cshtml index 8552f12ea..da6a1dcd3 100644 --- a/BTCPayServer/Views/UIPullPayment/SetupBoltcard.cshtml +++ b/BTCPayServer/Views/UIPullPayment/SetupBoltcard.cshtml @@ -1,7 +1,12 @@ -@using BTCPayServer.Controllers +@using BTCPayServer.Abstractions.TagHelpers +@using BTCPayServer.Blazor +@using BTCPayServer.Blazor.VaultBridge +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using BTCPayServer.TagHelpers @model SetupBoltcardViewModel @{ Layout = "_LayoutWizard"; + this.ViewData.SetBlazorAllowed(true); } @section Navbar { @@ -11,54 +16,44 @@ }
-

@ViewData["Title"]

+ @if (Model.NewCard) + { +

Setup Boltcard

+ } + else + { +

Reset Boltcard

+ } +

Using BTCPay Server Vault (NFC)

- +
+
+ +
+
-
- -
- @section PageFootContent { - - + } diff --git a/BTCPayServer/Views/UIStores/ImportWallet/ConfirmAddresses.cshtml b/BTCPayServer/Views/UIStores/ImportWallet/ConfirmAddresses.cshtml index ae3ad06b2..9c67b2703 100644 --- a/BTCPayServer/Views/UIStores/ImportWallet/ConfirmAddresses.cshtml +++ b/BTCPayServer/Views/UIStores/ImportWallet/ConfirmAddresses.cshtml @@ -15,41 +15,6 @@

Please check that your wallet is generating the same addresses as below.

-@if (!ViewContext.ModelState.IsValid) -{ -
-} -@if (Model.Method is WalletSetupMethod.Hardware) -{ - -} - - - - -
@@ -63,10 +28,6 @@ Key path Address - @if (Model.Source == "Vault") - { - - } @@ -75,13 +36,6 @@ @sample.KeyPath @sample.Address - @if (Model.Source == "Vault") - { - - @* Using single quotes for the data attributes on purpose *@ - Show on device - - } } @@ -93,54 +47,4 @@
-@section PageFootContent { - - - - - - - -} diff --git a/BTCPayServer/Views/UIStores/ImportWallet/Hardware.cshtml b/BTCPayServer/Views/UIStores/ImportWallet/Hardware.cshtml index 8a2734c56..90d5b237b 100644 --- a/BTCPayServer/Views/UIStores/ImportWallet/Hardware.cshtml +++ b/BTCPayServer/Views/UIStores/ImportWallet/Hardware.cshtml @@ -1,7 +1,14 @@ +@using BTCPayServer.Abstractions.TagHelpers +@using BTCPayServer.Blazor +@using BTCPayServer.Blazor.VaultBridge +@using BTCPayServer.TagHelpers +@using Microsoft.AspNetCore.Mvc.TagHelpers @model WalletSetupViewModel + @{ Layout = "_LayoutWalletSetup"; ViewData.SetActivePage(StoreNavPages.OnchainSettings, StringLocalizer["Connect your hardware wallet"], $"{Context.GetStoreData().Id}-{Model.CryptoCode}"); + this.ViewData.SetBlazorAllowed(true); } @section Navbar { @@ -15,97 +22,30 @@

In order to securely connect to your hardware wallet you must first download, install, and run the BTCPay Server Vault.

-@if (!ViewContext.ModelState.IsValid) -{ -
-} - - -
-
- -
- - -
+
- @section PageFootContent { - - - - - - + } diff --git a/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml b/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml index 383e2fa91..cd1190151 100644 --- a/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml +++ b/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml @@ -23,7 +23,6 @@
-
diff --git a/BTCPayServer/Views/UIStores/ImportWalletOptions.cshtml b/BTCPayServer/Views/UIStores/ImportWalletOptions.cshtml index fb6ae5254..237032d3d 100644 --- a/BTCPayServer/Views/UIStores/ImportWalletOptions.cshtml +++ b/BTCPayServer/Views/UIStores/ImportWalletOptions.cshtml @@ -29,7 +29,7 @@ { -
-
- - -
- -
} +
+
+ + +
+ +
diff --git a/BTCPayServer/Views/UIStores/_LayoutWalletSetup.cshtml b/BTCPayServer/Views/UIStores/_LayoutWalletSetup.cshtml index 395a5ce49..3e8f99bbe 100644 --- a/BTCPayServer/Views/UIStores/_LayoutWalletSetup.cshtml +++ b/BTCPayServer/Views/UIStores/_LayoutWalletSetup.cshtml @@ -21,3 +21,8 @@ @RenderBody() + +@if (User.Identity.IsAuthenticated && ViewData.IsBlazorAllowed()) +{ + +} diff --git a/BTCPayServer/Views/UIWallets/WalletSendVault.cshtml b/BTCPayServer/Views/UIWallets/WalletSendVault.cshtml index 07368739a..9a3331086 100644 --- a/BTCPayServer/Views/UIWallets/WalletSendVault.cshtml +++ b/BTCPayServer/Views/UIWallets/WalletSendVault.cshtml @@ -1,10 +1,14 @@ -@using BTCPayServer.Controllers +@using BTCPayServer.Abstractions.TagHelpers +@using BTCPayServer.Blazor.VaultBridge +@using BTCPayServer.TagHelpers +@using Microsoft.AspNetCore.Mvc.TagHelpers @model WalletSendVaultModel @{ - var walletId = Context.GetRouteValue("walletId").ToString(); + var walletId = Context.GetRouteValue("walletId")?.ToString() ?? ""; Model.ReturnUrl ??= Url.WalletTransactions(walletId); Layout = "_LayoutWizard"; ViewData.SetActivePage(WalletsNavPages.Send, StringLocalizer["Sign the transaction"], walletId); + this.ViewData.SetBlazorAllowed(true); } @section Navbar { @@ -16,52 +20,37 @@

Using BTCPay Server Vault

- +
+
+ +
+
- -
- -
- @section PageFootContent { - - + } diff --git a/BTCPayServer/WebSocketHelper.cs b/BTCPayServer/WebSocketHelper.cs deleted file mode 100644 index ee67837d6..000000000 --- a/BTCPayServer/WebSocketHelper.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using NBXplorer; - -namespace BTCPayServer -{ - public class WebSocketHelper - { - private readonly WebSocket _Socket; - public WebSocket Socket - { - get - { - return _Socket; - } - } - - public WebSocketHelper(WebSocket socket) - { - _Socket = socket; - var buffer = new byte[ORIGINAL_BUFFER_SIZE]; - _Buffer = new ArraySegment(buffer, 0, buffer.Length); - } - - const int ORIGINAL_BUFFER_SIZE = 1024 * 5; - const int MAX_BUFFER_SIZE = 1024 * 1024 * 5; - readonly ArraySegment _Buffer; - readonly UTF8Encoding UTF8 = new UTF8Encoding(false, true); - public async Task NextMessageAsync(CancellationToken cancellation) - { - var buffer = _Buffer; - var array = _Buffer.Array; - var originalSize = _Buffer.Array.Length; - var newSize = _Buffer.Array.Length; - while (true) - { - var message = await Socket.ReceiveAndPingAsync(buffer, cancellation); - if (message.MessageType == WebSocketMessageType.Close) - { - await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation); - break; - } - if (message.MessageType != WebSocketMessageType.Text) - { - await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation); - break; - } - if (message.EndOfMessage) - { - buffer = new ArraySegment(array, 0, buffer.Offset + message.Count); - try - { - var o = UTF8.GetString(buffer.Array, 0, buffer.Count); - if (newSize != originalSize) - { - Array.Resize(ref array, originalSize); - } - return o; - } - catch (Exception ex) - { - await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation); - } - } - else - { - if (buffer.Count - message.Count <= 0) - { - newSize *= 2; - if (newSize > MAX_BUFFER_SIZE) - await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation); - Array.Resize(ref array, newSize); - buffer = new ArraySegment(array, buffer.Offset, newSize - buffer.Offset); - } - - buffer = buffer.Slice(message.Count, buffer.Count - message.Count); - } - } - throw new InvalidOperationException("Should never happen"); - } - - private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation) - { - var array = _Buffer.Array; - if (array.Length != ORIGINAL_BUFFER_SIZE) - Array.Resize(ref array, ORIGINAL_BUFFER_SIZE); - await Socket.CloseSocket(status, description, cancellation); - throw new WebSocketException($"The socket has been closed ({status}: {description})"); - } - - public async Task Send(string evt, CancellationToken cancellation = default) - { - var bytes = UTF8.GetBytes(evt); - using var cts = new CancellationTokenSource(5000); - using var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cancellation); - await Socket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, cts2.Token); - } - - public async Task DisposeAsync(CancellationToken cancellation) - { - try - { - await Socket.CloseSocket(WebSocketCloseStatus.NormalClosure, "Disposing NotificationServer", cancellation); - } - catch { } - finally { try { Socket.Dispose(); } catch { } } - } - } -} diff --git a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js b/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js deleted file mode 100644 index 8173d0f44..000000000 --- a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js +++ /dev/null @@ -1,58 +0,0 @@ -function getVaultUI() { - var websocketPath = $("#WebsocketPath").text(); - var loc = window.location, ws_uri; - if (loc.protocol === "https:") { - ws_uri = "wss:"; - } else { - ws_uri = "ws:"; - } - ws_uri += "//" + loc.host; - ws_uri += websocketPath; - return new vaultui.VaultBridgeUI(ws_uri); -} - -function showModal() { - var html = $("#btcpayservervault_template").html(); - $("#btcpayservervault").html(html); - html = $("#VaultConnection").html(); - $("#vaultPlaceholder").html(html); - $('#btcpayservervault').modal(); -} - -async function showAddress(rootedKeyPath, address) { - $(".showaddress").addClass("disabled"); - showModal(); - $("#btcpayservervault #displayedAddress").text(address); - var vaultUI = getVaultUI(); - $('#btcpayservervault').on('hidden.bs.modal', function () { - vaultUI.closeBridge(); - $(".showaddress").removeClass("disabled"); - }); - if (await vaultUI.askForDevice()) - await vaultUI.askForDisplayAddress(rootedKeyPath); - $('#btcpayservervault').modal("hide"); -} - -$(document).ready(function () { - function displayXPubs(xpub) { - $("#DerivationScheme").val(xpub.strategy); - $("#RootFingerprint").val(xpub.fingerprint); - $("#AccountKey").val(xpub.accountKey); - $("#Source").val("Vault"); - $("#DerivationSchemeFormat").val("BTCPay"); - $("#KeyPath").val(xpub.keyPath); - $(".modal").modal('hide'); - $(".hw-fields").show(); - } - - $(".check-for-vault").on("click", async function () { - var vaultUI = getVaultUI(); - showModal(); - $('#btcpayservervault').on('hidden.bs.modal', function () { - vaultUI.closeBridge(); - }); - while (! await vaultUI.askForDevice() || ! await vaultUI.askForXPubs()) { - } - displayXPubs(vaultUI.xpub); - }); -}); diff --git a/BTCPayServer/wwwroot/js/vaultbridge.js b/BTCPayServer/wwwroot/js/vaultbridge.js index d4883e047..a1ff3522a 100644 --- a/BTCPayServer/wwwroot/js/vaultbridge.js +++ b/BTCPayServer/wwwroot/js/vaultbridge.js @@ -1,118 +1,52 @@ var vault = (function () { - /** @param {WebSocket} websocket - */ - function VaultBridge(websocket) { - var self = this; - /** - * @type {WebSocket} - */ - this.socket = websocket; - this.close = function () { if (websocket) websocket.close(); }; - /** - * @returns {Promise} - */ - this.waitBackendMessage = function () { - return new Promise(function (resolve, reject) { - self.nextResolveBackendMessage = resolve; - }); - }; - this.socket.onmessage = function (event) { - if (typeof event.data === "string") { - if (event.data === "ping") - return; - var jsonObject = JSON.parse(event.data); - if (jsonObject.command == "sendRequest") { - var request = new XMLHttpRequest(); - request.onreadystatechange = function () { - if (request.readyState == 4) { - if (request.status === 0) { - self.socket.send("{\"error\": \"Failed to connect to uri\"}"); - } - else if (self.socket.readyState == 1) { - var body = null; - if (request.responseText) { - var contentType = request.getResponseHeader('Content-Type') || 'text/plain'; - if (contentType === 'text/plain') - body = request.responseText; - else - body = JSON.parse(request.responseText); - } - - self.socket.send(JSON.stringify( - { - httpCode: request.status, - body: body - })); - } - } - }; - request.overrideMimeType("text/plain"); - request.open('POST', jsonObject.uri); - jsonObject.body = jsonObject.body || {}; - request.send(JSON.stringify(jsonObject.body)); - } - else { - if (self.nextResolveBackendMessage) - self.nextResolveBackendMessage(jsonObject); + async function sendRequest(req) + { + + try { + const response = await fetch(req.uri, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain' + }, + body: JSON.stringify(req.body) + }); + if (!response.ok) { + return { httpCode: response.status }; } + + const contentType = response.headers.get('Content-Type') || 'text/plain'; + const body = contentType.includes('application/json') + ? await response.json() + : await response.text(); + return { httpCode: response.status, body }; + } catch (e) { + return { httpCode: 0, error: e.message }; } + } + + async function askVaultPermission(url) { + url = url + "/request-permission"; + var browser = "other"; + if (window.safari !== undefined) + browser = "safari"; + if (navigator.brave !== undefined) + browser = "brave"; + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'text/plain' + } + }); + return {httpCode: response.status, browser: browser}; + } catch (e) { + return {httpCode: 0, browser: browser}; + } + } + + return { + askVaultPermission: askVaultPermission, + sendRequest: sendRequest }; } - - /** - * @param {string} ws_uri - * @returns {Promise} - */ - function connectToBackendSocket(ws_uri) { - return new Promise(function (resolve, reject) { - var supportWebSocket = "WebSocket" in window && window.WebSocket.CLOSING === 2; - if (!supportWebSocket) { - reject(vault.errors.socketNotSupported); - return; - } - var socket = new WebSocket(ws_uri); - socket.onerror = function (error) { - console.warn(error); - reject(vault.errors.socketError); - }; - socket.onopen = function () { - resolve(new vault.VaultBridge(socket)); - }; - }); - } - - /** - * @returns {Promise} - */ - function askVaultPermission() { - return new Promise(function (resolve, reject) { - var request = new XMLHttpRequest(); - request.onreadystatechange = function () { - if (request.readyState == 4 && request.status == 200) { - resolve(); - } - if (request.readyState == 4 && request.status == 0) { - reject(vault.errors.notRunning); - } - if (request.readyState == 4 && request.status == 401) { - reject(vault.errors.denied); - } - }; - request.overrideMimeType("text/plain"); - request.open('GET', 'http://127.0.0.1:65092/hwi-bridge/v1/request-permission'); - request.send(); - }); - } - - return { - errors: { - notRunning: "NotRunning", - denied: "Denied", - socketNotSupported: "SocketNotSupported", - socketError: "SocketError" - }, - askVaultPermission: askVaultPermission, - connectToBackendSocket: connectToBackendSocket, - VaultBridge: VaultBridge - }; -})(); +)(); diff --git a/BTCPayServer/wwwroot/js/vaultbridge.ui.js b/BTCPayServer/wwwroot/js/vaultbridge.ui.js deleted file mode 100644 index 5e80323a4..000000000 --- a/BTCPayServer/wwwroot/js/vaultbridge.ui.js +++ /dev/null @@ -1,459 +0,0 @@ -/// -/// file: vaultbridge.js - -var vaultui = (function () { - - /** - * @param {string} type - * @param {string} txt - * @param {string} id - */ - function VaultFeedback(type, txt, id) { - var self = this; - this.type = type; - this.txt = txt; - this.id = id; - /** - * @param {string} str - * @param {string} by - */ - this.replace = function (str, by) { - return new VaultFeedback(self.type, self.txt.replace(str, by), self.id); - }; - } - - var VaultFeedbacks = { - vaultLoading: new VaultFeedback("?", "Checking BTCPay Server Vault is running...", "vault-loading"), - vaultDenied: new VaultFeedback("failed", "The user declined access to the vault.", "vault-denied"), - vaultGranted: new VaultFeedback("ok", "Access to vault granted by owner.", "vault-granted"), - noVault: new VaultFeedback("failed", "BTCPay Server Vault does not seem to be running, you can download it on
Github.", "no-vault"), - noWebsockets: new VaultFeedback("failed", "Web sockets are not supported by the browser.", "no-websocket"), - errorWebsockets: new VaultFeedback("failed", "Error of the websocket while connecting to the backend.", "error-websocket"), - bridgeConnected: new VaultFeedback("ok", "BTCPayServer successfully connected to the vault.", "bridge-connected"), - vaultNeedUpdate: new VaultFeedback("failed", "Your BTCPay Server Vault version is outdated. Please download the latest version.", "vault-outdated"), - noDevice: new VaultFeedback("failed", "No device connected.", "no-device"), - needInitialized: new VaultFeedback("failed", "The device has not been initialized.", "need-initialized"), - fetchingDevice: new VaultFeedback("?", "Fetching device...", "fetching-device"), - deviceFound: new VaultFeedback("ok", "Device found: {{0}}", "device-selected"), - fetchingXpubs: new VaultFeedback("?", "Fetching public keys...", "fetching-xpubs"), - askXpubs: new VaultFeedback("?", "Select your address type and account", "fetching-xpubs"), - fetchedXpubs: new VaultFeedback("ok", "Public keys successfully fetched.", "xpubs-fetched"), - unexpectedError: new VaultFeedback("failed", "An unexpected error happened. ({{0}})", "unknown-error"), - invalidNetwork: new VaultFeedback("failed", "The device is targeting a different chain.", "invalid-network"), - needPin: new VaultFeedback("?", "Enter the pin.", "need-pin"), - incorrectPin: new VaultFeedback("failed", "Incorrect pin code.", "incorrect-pin"), - invalidPasswordConfirmation: new VaultFeedback("failed", "Invalid password confirmation.", "invalid-password-confirm"), - wrongWallet: new VaultFeedback("failed", "This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)", "wrong-wallet"), - wrongKeyPath: new VaultFeedback("failed", "This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)", "wrong-keypath"), - needPassphrase: new VaultFeedback("?", "Enter the passphrase.", "need-passphrase"), - needPassphraseOnDevice: new VaultFeedback("?", "Please, enter the passphrase on the device.", "need-passphrase-on-device"), - signingTransaction: new VaultFeedback("?", "Please review and confirm the transaction on your device...", "ask-signing"), - reviewAddress: new VaultFeedback("?", "Sending... Please review the address on your device...", "ask-signing"), - signingRejected: new VaultFeedback("failed", "The user refused to sign the transaction", "user-reject"), - alreadySignedPsbt: new VaultFeedback("failed", "This device already signed PSBT.", "already-signed-psbt"), - }; - - /** - * @param {string} backend_uri - */ - function VaultBridgeUI(backend_uri) { - /** - * @type {VaultBridgeUI} - */ - var self = this; - /** - * @type {string} - */ - this.backend_uri = backend_uri; - /** - * @type {vault.VaultBridge} - */ - this.bridge = null; - /** - * @type {string} - */ - this.psbt = null; - - this.xpub = null; - - this.retryShowing = false; - - function showRetry() { - var button = $("#vault-retry"); - self.retryShowing = true; - button.show(); - } - - this.currentFeedback = 1; - - /** - * @param {VaultFeedback} feedback - */ - function show(feedback) { - const $icon = document.querySelector(`.vault-feedback.vault-feedback${self.currentFeedback} .vault-feedback-icon`); - let iconClasses = ''; - if (feedback.type == "?") { - iconClasses = "icon-dots feedback-icon-loading"; - } - else if (feedback.type == "ok") { - iconClasses = "icon-checkmark feedback-icon-success"; - $icon.innerHTML = $icon.innerHTML.replace("#dots", "#checkmark"); - } - else if (feedback.type == "failed") { - iconClasses = "icon-cross feedback-icon-failed"; - $icon.innerHTML = $icon.innerHTML.replace("#dots", "#cross"); - showRetry(); - } - $icon.setAttribute('class', `vault-feedback-icon icon me-2 ${iconClasses}`); - const $content = document.querySelector(`.vault-feedback.vault-feedback${self.currentFeedback} .vault-feedback-content`); - $content.innerHTML = feedback.txt; - if (feedback.type === 'ok') - self.currentFeedback++; - if (feedback.type === 'failed') - self.currentFeedback = 1; - } - function showError(json) { - if (json.hasOwnProperty("error")) { - for (var key in VaultFeedbacks) { - if (VaultFeedbacks.hasOwnProperty(key) && VaultFeedbacks[key].id == json.error) { - if (VaultFeedbacks.unexpectedError === VaultFeedbacks[key]) { - show(VaultFeedbacks.unexpectedError.replace("{{0}}", json.message)); - } - else { - show(VaultFeedbacks[key]); - } - if (json.hasOwnProperty("details")) - console.warn(json.details); - return; - } - } - show(VaultFeedbacks.unexpectedError.replace("{{0}}", json.message)); - if (json.hasOwnProperty("details")) - console.warn(json.details); - } - } - - function showMessage(message) { - let type = 'ok'; - if (message.type === 'Error') - type = 'failed'; - if (message.type === 'Processing') - type = '?'; - show(new VaultFeedback(type, message.message, "")); - if (type.debug) - console.warn(type.debug); - } - - async function needRetry(json) { - if (json.hasOwnProperty("error")) { - var handled = false; - if (json.error === "need-device") { - handled = true; - if (await self.askForDevice()) - return true; - } - if (json.error === "need-pin") { - handled = true; - if (await self.askForPin()) - return true; - } - if (json.error === "need-passphrase") { - handled = true; - if (await self.askForPassphrase()) - return true; - } - if (json.error === "need-passphrase-on-device") { - handled = true; - show(VaultFeedbacks.needPassphraseOnDevice); - self.bridge.socket.send("ask-passphrase"); - var json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - showError(json); - return false; - } - return true; - } - - if (!handled) { - showError(json); - } - } - return false; - } - - this.waitRetryPushed = function () { - var button = $("#vault-retry"); - return new Promise(function (resolve) { - button.click(function () { - // Reset feedback statuses - $(".vault-feedback").each(function () { - var icon = $(this).find(".vault-feedback-icon"); - icon.removeClass().addClass("vault-feedback-icon d-none"); - $(this).find(".vault-feedback-content").html(''); - }); - - button.hide(); - self.retryShowing = false; - resolve(true); - }); - }); - }; - - this.ensureConnectedToBackend = async function () { - if (self.retryShowing) { - await self.waitRetryPushed(); - } - if (!self.bridge || self.bridge.socket.readyState !== 1) { - $("#vault-dropdown").css("display", "none"); - show(VaultFeedbacks.vaultLoading); - try { - await vault.askVaultPermission(); - } catch (ex) { - if (ex == vault.errors.notRunning) - show(VaultFeedbacks.noVault); - else if (ex == vault.errors.denied) - show(VaultFeedbacks.vaultDenied); - return false; - } - show(VaultFeedbacks.vaultGranted); - try { - self.bridge = await vault.connectToBackendSocket(self.backend_uri); - show(VaultFeedbacks.bridgeConnected); - } catch (ex) { - if (ex == vault.errors.socketNotSupported) - show(VaultFeedbacks.noWebsockets); - if (ex == vault.errors.socketError) - show(VaultFeedbacks.errorWebsockets); - return false; - } - } - return true; - }; - this.sendBackendCommand = async function (command) { - if (!self.bridge || self.bridge.socket.readyState !== 1) { - self.bridge = await vault.connectToBackendSocket(self.backend_uri); - } - show(VaultFeedbacks.vaultLoading); - self.bridge.socket.send(command); - while (true) { - var json = await self.bridge.waitBackendMessage(); - if (json.command === 'showMessage') { - showMessage(json); - if (json.type === "Error") { - showRetry(); - return false; - } - } - if (json.command == 'done') { - return true; - } - } - } - this.askForDisplayAddress = async function (rootedKeyPath) { - if (!await self.ensureConnectedToBackend()) - return false; - show(VaultFeedbacks.reviewAddress); - self.bridge.socket.send("display-address"); - self.bridge.socket.send(rootedKeyPath); - var json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - if (await needRetry(json)) - return await self.askForDisplayAddress(rootedKeyPath); - return false; - } - return true; - } - this.askForDevice = async function () { - if (!await self.ensureConnectedToBackend()) - return false; - show(VaultFeedbacks.fetchingDevice); - self.bridge.socket.send("ask-device"); - var json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - showError(json); - return false; - } - show(VaultFeedbacks.deviceFound.replace("{{0}}", json.model)); - return true; - }; - - this.askForXPubs = async function () { - if (!await self.ensureConnectedToBackend()) - return false; - self.bridge.socket.send("ask-xpub"); - var json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - if (await needRetry(json)) - return await self.askForXPubs(); - return false; - } - try { - var selectedXPubs = await self.getXpubSettings(); - self.bridge.socket.send(JSON.stringify(selectedXPubs)); - show(VaultFeedbacks.fetchingXpubs); - json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - if (await needRetry(json)) - return await self.askForXPubs(); - return false; - } - show(VaultFeedbacks.fetchedXpubs); - self.xpub = json; - return true; - } catch (err) { - showError({ error: true, message: err }); - } - }; - - /** - * @returns {Promise<{addressType:string, accountNumber:number}>} - */ - this.getXpubSettings = function () { - show(VaultFeedbacks.askXpubs); - $("#vault-xpub").css("display", "block"); - $("#vault-confirm").css("display", "block"); - $("#vault-confirm").text("Confirm"); - return new Promise(function (resolve, reject) { - $("#vault-confirm").click(async function (e) { - e.preventDefault(); - $("#vault-xpub").css("display", "none"); - $("#vault-confirm").css("display", "none"); - $(this).unbind(); - const addressType = $("select[name=\"addressType\"]").val(); - const accountNumber = parseInt($("input[name=\"accountNumber\"]").val()); - if (addressType && !isNaN(accountNumber)) { - resolve({ addressType, accountNumber }); - } else { - reject("Provide an address type and account number") - } - }); - }); - }; - - /** - * @returns {Promise} - */ - this.getUserEnterPin = function () { - show(VaultFeedbacks.needPin); - $("#pin-input").css("display", "block"); - $("#vault-confirm").css("display", "block"); - $("#vault-confirm").text("Confirm the pin code"); - return new Promise(function (resolve, reject) { - var pinCode = ""; - $("#vault-confirm").click(async function (e) { - e.preventDefault(); - $("#pin-input").css("display", "none"); - $("#vault-confirm").css("display", "none"); - $(this).unbind(); - $(".pin-button").unbind(); - $("#pin-display-delete").unbind(); - resolve(pinCode); - }); - $("#pin-display-delete").click(function () { - pinCode = ""; - $("#pin-display").val(""); - }); - $(".pin-button").click(function () { - var id = $(this).attr('id').replace("pin-", ""); - pinCode = pinCode + id; - $("#pin-display").val($("#pin-display").val() + "*"); - }); - }); - }; - - /** - * @returns {Promise} - */ - this.getUserPassphrase = function () { - show(VaultFeedbacks.needPassphrase); - $("#passphrase-input").css("display", "block"); - $("#vault-confirm").css("display", "block"); - $("#vault-confirm").text("Confirm the passphrase"); - return new Promise(function (resolve, reject) { - $("#vault-confirm").click(async function (e) { - e.preventDefault(); - var passphrase = $("#Password").val(); - if (passphrase !== $("#PasswordConfirmation").val()) { - show(VaultFeedbacks.invalidPasswordConfirmation); - return; - } - $("#passphrase-input").css("display", "none"); - $("#vault-confirm").css("display", "none"); - $(this).unbind(); - resolve(passphrase); - }); - }); - }; - - this.askForPassphrase = async function () { - if (!await self.ensureConnectedToBackend()) - return false; - var passphrase = await self.getUserPassphrase(); - self.bridge.socket.send("set-passphrase"); - self.bridge.socket.send(passphrase); - return true; - } - - /** - * @returns {Promise} - */ - this.askForPin = async function () { - if (!await self.ensureConnectedToBackend()) - return false; - - self.bridge.socket.send("ask-pin"); - var json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - if (json.error == "device-already-unlocked") - return true; - if (await needRetry(json)) - return await self.askForPin(); - return false; - } - - var pinCode = await self.getUserEnterPin(); - self.bridge.socket.send(pinCode); - var json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - showError(json); - return false; - } - return true; - } - - /** - * @returns {Promise} - */ - this.askSignPSBT = async function (args) { - if (!await self.ensureConnectedToBackend()) - return false; - show(VaultFeedbacks.signingTransaction); - self.bridge.socket.send("ask-sign"); - var json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - if (await needRetry(json)) - return await self.askSignPSBT(args); - return false; - } - self.bridge.socket.send(JSON.stringify(args)); - json = await self.bridge.waitBackendMessage(); - if (json.hasOwnProperty("error")) { - if (await needRetry(json)) - return await self.askSignPSBT(args); - return false; - } - self.psbt = json.psbt; - return true; - }; - - this.closeBridge = function () { - if (self.bridge) { - self.bridge.close(); - } - }; - } - return { - VaultFeedback: VaultFeedback, - VaultBridgeUI: VaultBridgeUI - }; -})();