using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Data; using BTCPayServer.Lightning; using BTCPayServer.Models; using BTCPayServer.NTag424; using Dapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NBitcoin.DataEncoders; using System; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using static BTCPayServer.BoltcardDataExtensions; namespace BTCPayServer.Controllers { public partial class UIPullPaymentController { [AllowAnonymous] [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 }); } [AllowAnonymous] [HttpPost("pull-payments/{pullPaymentId}/boltcard/{command}")] public IActionResult SetupBoltcardPost(string pullPaymentId, string command) { TempData[WellKnownTempData.SuccessMessage] = "Boltcard is configured"; 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); } [HttpGet] [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.GetBlob())) 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, "BTCPay Server Vault does not seem to be running, you can download it on Github.", cts.Token); goto next; } await vaultClient.Show(VaultMessageType.Ok, "BTCPayServer successfully connected to the vault.", cts.Token); if (permission is false) { await vaultClient.Show(VaultMessageType.Error, "The user declined access to the vault.", cts.Token); goto next; } await vaultClient.Show(VaultMessageType.Ok, "Access to vault granted by owner.", cts.Token); await vaultClient.Show(VaultMessageType.Processing, "Waiting for NFC to be presented...", cts.Token); await transport.WaitForCard(cts.Token); await vaultClient.Show(VaultMessageType.Ok, "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, "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, "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, "The card is now configured", cts.Token); } else if (cardOrigin is CardOrigin.ThisIssuer) { await vaultClient.Show(VaultMessageType.Ok, "This card is already properly configured", cts.Token); } success = true; break; case "reset-boltcard": await vaultClient.Show(VaultMessageType.Processing, "Resetting Boltcard...", cts.Token); if (cardOrigin is CardOrigin.Blank) { await vaultClient.Show(VaultMessageType.Ok, "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, "Card reset succeed", cts.Token); } success = true; break; } if (success) { await vaultClient.Show(VaultMessageType.Processing, "Please remove the NFC from the card reader", cts.Token); await transport.WaitForRemoved(cts.Token); await vaultClient.Show(VaultMessageType.Ok, "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, "Unexpected error: " + 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; } } }