diff --git a/BTCPayServer.Client/Models/RegisterBoltcardRequest.cs b/BTCPayServer.Client/Models/RegisterBoltcardRequest.cs index 7072ff23e..d9958d582 100644 --- a/BTCPayServer.Client/Models/RegisterBoltcardRequest.cs +++ b/BTCPayServer.Client/Models/RegisterBoltcardRequest.cs @@ -15,6 +15,8 @@ namespace BTCPayServer.Client.Models } public class RegisterBoltcardRequest { + [JsonProperty("LNURLW")] + public string LNURLW { get; set; } [JsonConverter(typeof(HexJsonConverter))] [JsonProperty("UID")] public byte[] UID { get; set; } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 2ec3aac50..f969545c3 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -13,6 +13,7 @@ using BTCPayServer.Controllers; using BTCPayServer.Events; using BTCPayServer.Lightning; using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.NTag424; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.PayoutProcessors; @@ -24,6 +25,7 @@ using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NBitcoin; +using NBitcoin.DataEncoders; using NBitpayClient; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -1115,6 +1117,35 @@ namespace BTCPayServer.Tests OnExisting = OnExistingBehavior.KeepVersion }); Assert.Equal(card2.Version, card3.Version); + var p = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[8]).ToArray(); + var card4 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest() + { + OnExisting = OnExistingBehavior.KeepVersion, + LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}" + }); + Assert.Equal(card2.Version, card4.Version); + Assert.Equal(card2.K4, card4.K4); + // Can't define both properties + await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest() + { + OnExisting = OnExistingBehavior.KeepVersion, + UID = uid, + LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}" + })); + // p is malformed + await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest() + { + OnExisting = OnExistingBehavior.KeepVersion, + UID = uid, + LNURLW = card2.LNURLW + $"?p=lol" + })); + // p is invalid + p[0] = 0; + await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest() + { + OnExisting = OnExistingBehavior.KeepVersion, + LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}" + })); // Test with SATS denomination values var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs index b00a2ea9c..01fefb178 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.IO.IsolatedStorage; using System.Linq; using System.Text.RegularExpressions; @@ -215,7 +216,29 @@ namespace BTCPayServer.Controllers.Greenfield var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false); if (pp is null) return PullPaymentNotFound(); - _logs.PayServer.LogInformation(JsonConvert.SerializeObject(request, Formatting.Indented)); + var issuerKey = await _settingsRepository.GetIssuerKey(_env); + // LNURLW is used by deeplinks + if (request?.LNURLW is not null) + { + if (request.UID is not null) + { + ModelState.AddModelError(nameof(request.LNURLW), "You should pass either LNURLW or UID but not both"); + return this.CreateValidationError(ModelState); + } + var p = ExtractP(request.LNURLW); + if (p is null) + { + ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter"); + return this.CreateValidationError(ModelState); + } + if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc) + { + ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted"); + return this.CreateValidationError(ModelState); + } + request.UID = picc.Uid; + } + if (request?.UID is null || request.UID.Length != 7) { ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes"); @@ -234,7 +257,6 @@ namespace BTCPayServer.Controllers.Greenfield _ => request.OnExisting }; - var issuerKey = await _settingsRepository.GetIssuerKey(_env); var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting); var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey); @@ -254,6 +276,20 @@ namespace BTCPayServer.Controllers.Greenfield }); } + private string? ExtractP(string? url) + { + if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri)) + return null; + int num = uri.AbsoluteUri.IndexOf('?'); + if (num == -1) + return null; + string input = uri.AbsoluteUri.Substring(num); + Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})"); + if (!match.Success) + return null; + return match.Groups[1].Value; + } + [HttpGet("~/api/v1/pull-payments/{pullPaymentId}")] [AllowAnonymous] public async Task GetPullPayment(string pullPaymentId)