Disable cold wallet creation by default

This commit is contained in:
nicolas.dorier
2025-03-13 19:23:23 +09:00
parent 517dd7d85b
commit c3c6473e35
12 changed files with 127 additions and 92 deletions

View File

@@ -43,13 +43,13 @@ namespace BTCPayServer.Controllers.Greenfield
}
var canUseHotWallet = await CanUseHotWallet();
if (request.SavePrivateKeys && !canUseHotWallet.HotWallet)
if (request.SavePrivateKeys && !canUseHotWallet.CanCreateHotWallet)
{
ModelState.AddModelError(nameof(request.SavePrivateKeys),
"This instance forbids non-admins from having a hot wallet for your store.");
}
if (request.ImportKeysToRPC && !canUseHotWallet.RPCImport)
if (request.ImportKeysToRPC && !canUseHotWallet.CanRPCImport)
{
ModelState.AddModelError(nameof(request.ImportKeysToRPC),
"This instance forbids non-admins from having importing the wallet addresses/keys to the underlying node.");
@@ -120,7 +120,7 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(result);
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
private async Task<WalletCreationPermissions> CanUseHotWallet()
{
return await _authorizationService.CanUseHotWallet(PoliciesSettings, User);
}

View File

@@ -383,7 +383,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
//This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation.
if (!(await CanUseHotWallet()).HotWallet)
if (!(await CanUseHotWallet()).CanCreateHotWallet)
{
return this.CreateAPIError(503, "not-available",
$"You need to allow non-admins to use hotwallets for their stores (in /server/policies)");
@@ -802,7 +802,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
private async Task<WalletCreationPermissions> CanUseHotWallet()
{
return await _authorizationService.CanUseHotWallet(PoliciesSettings, User);
}

View File

@@ -54,10 +54,9 @@ public partial class UIStoresController
return checkResult;
}
var (hotWallet, rpcImport) = await CanUseHotWallet();
var perm = await CanUseHotWallet();
vm.Network = network;
vm.CanUseHotWallet = hotWallet;
vm.CanUseRPCImport = rpcImport;
vm.SetPermission(perm);
vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot;
vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
@@ -218,14 +217,13 @@ public partial class UIStoresController
}
var isHotWallet = vm.Method == WalletSetupMethod.HotWallet;
var (hotWallet, rpcImport) = await CanUseHotWallet();
if (isHotWallet && !hotWallet)
{
var isColdWallet = vm.Method == WalletSetupMethod.WatchOnly;
var perm = await CanUseHotWallet();
if (isHotWallet && !perm.CanCreateHotWallet)
return NotFound();
}
vm.CanUseHotWallet = hotWallet;
vm.CanUseRPCImport = rpcImport;
if (isColdWallet && !perm.CanCreateColdWallet)
return NotFound();
vm.SetPermission(perm);
vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot;
vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
vm.Network = network;
@@ -236,7 +234,7 @@ public partial class UIStoresController
}
else
{
var canUsePayJoin = hotWallet && isHotWallet && network.SupportPayJoin;
var canUsePayJoin = perm.CanCreateHotWallet && isHotWallet && network.SupportPayJoin;
vm.SetupRequest = new WalletSetupRequest
{
SavePrivateKeys = isHotWallet,
@@ -260,8 +258,10 @@ public partial class UIStoresController
return checkResult;
}
var (hotWallet, rpcImport) = await CanUseHotWallet();
if (!hotWallet && request.SavePrivateKeys || !rpcImport && request.ImportKeysToRPC)
var perm = await CanUseHotWallet();
if ((!perm.CanCreateHotWallet && request.SavePrivateKeys) ||
(!perm.CanRPCImport && request.ImportKeysToRPC) ||
(!perm.CanCreateColdWallet && !request.SavePrivateKeys))
{
return NotFound();
}
@@ -279,12 +279,10 @@ public partial class UIStoresController
Source = isImport ? "SeedImported" : "NBXplorerGenerated",
IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet,
DerivationSchemeFormat = "BTCPay",
CanUseHotWallet = hotWallet,
CanUseRPCImport = rpcImport,
SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot,
SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit
};
vm.SetPermission(perm);
if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic))
{
ModelState.AddModelError(nameof(request.ExistingMnemonic), StringLocalizer["Please provide your existing seed"]);
@@ -404,7 +402,7 @@ public partial class UIStoresController
var storeBlob = store.GetStoreBlob();
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
(bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet();
var perm = await CanUseHotWallet();
var client = _explorerProvider.GetExplorerClient(network);
var handler = _handlers.GetBitcoinHandler(cryptoCode);
@@ -426,7 +424,7 @@ public partial class UIStoresController
Label = derivation.Label,
SelectedSigningKey = derivation.SigningKey?.ToString(),
NBXSeedAvailable = derivation.IsHotWallet &&
canUseHotWallet &&
perm.CanCreateHotWallet &&
!string.IsNullOrEmpty(await client.GetMetadataAsync<string>(derivation.AccountDerivation,
WellknownMetadataKeys.MasterHDKey)),
AccountKeys = (derivation.AccountKeySettings ?? [])
@@ -438,9 +436,9 @@ public partial class UIStoresController
}).ToList(),
Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()),
PayJoinEnabled = storeBlob.PayJoinEnabled,
CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
CanUseHotWallet = canUseHotWallet,
CanUseRPCImport = rpcImport,
CanUsePayJoin = perm.CanCreateHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
CanUseHotWallet = perm.CanCreateHotWallet,
CanUseRPCImport = perm.CanRPCImport,
StoreName = store.StoreName,
CanSetupMultiSig = (derivation.AccountKeySettings ?? []).Length > 1,
IsMultiSigOnServer = derivation.IsMultiSigOnServer,
@@ -589,11 +587,8 @@ public partial class UIStoresController
return NotFound();
}
(bool canUseHotWallet, bool _) = await CanUseHotWallet();
if (!canUseHotWallet)
{
if (!(await CanUseHotWallet()).CanCreateHotWallet)
return NotFound();
}
var client = _explorerProvider.GetExplorerClient(network);
if (await GetSeed(client, derivation) != null)
@@ -753,7 +748,7 @@ public partial class UIStoresController
!string.IsNullOrEmpty(seed) ? seed : null;
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
private async Task<WalletCreationPermissions> CanUseHotWallet()
{
return await _authorizationService.CanUseHotWallet(_policiesSettings, User);
}

View File

@@ -795,7 +795,7 @@ namespace BTCPayServer.Controllers
private async Task<bool> CanUseHotWallet()
{
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet;
return (await _authorizationService.CanUseHotWallet(policies, User)).CanCreateHotWallet;
}
[HttpGet("{walletId}/send")]

View File

@@ -1,3 +1,4 @@
#nullable enable
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
@@ -9,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer
{
public record WalletCreationPermissions(bool CanCreateHotWallet, bool CanCreateColdWallet, bool CanRPCImport);
public static class AuthorizationExtensions
{
public static async Task<bool> CanModifyStore(this IAuthorizationService authorizationService, ClaimsPrincipal user)
@@ -16,24 +18,26 @@ namespace BTCPayServer
return (await authorizationService.AuthorizeAsync(user, null,
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
}
public static async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet(
public static async Task<WalletCreationPermissions> CanUseHotWallet(
this IAuthorizationService authorizationService,
PoliciesSettings policiesSettings,
PoliciesSettings? policiesSettings,
ClaimsPrincipal user)
{
if (!user.Identity.IsAuthenticated)
return (false, false);
if (user.Identity?.IsAuthenticated is not true)
return new(false, false, false);
var claimUser = user.Identity as ClaimsIdentity;
if (claimUser is null)
return (false, false);
return new(false, false, false);
bool isAdmin = false;
if (claimUser.AuthenticationType == AuthenticationSchemes.Cookie)
isAdmin = user.IsInRole(Roles.ServerAdmin);
else if (claimUser.AuthenticationType == GreenfieldConstants.AuthenticationType)
isAdmin = (await authorizationService.AuthorizeAsync(user, Policies.CanModifyServerSettings)).Succeeded;
return isAdmin ? (true, true) :
(policiesSettings?.AllowHotWalletForAll is true, policiesSettings?.AllowHotWalletRPCImportForAll is true);
return isAdmin ? new(true, true, true) :
new(policiesSettings?.AllowHotWalletForAll is true,
policiesSettings?.AllowCreateColdWalletForAll is true,
policiesSettings?.AllowHotWalletRPCImportForAll is true);
}
}
}

View File

@@ -35,6 +35,8 @@ namespace BTCPayServer.Models.StoreViewModels
public BTCPayNetwork Network { get; set; }
[Display(Name = "Can use hot wallet")]
public bool CanUseHotWallet { get; set; }
[Display(Name = "Can create a new cold wallet")]
public bool CanCreateNewColdWallet { get; set; }
[Display(Name = "Can use RPC import")]
public bool CanUseRPCImport { get; set; }
public bool SupportSegwit { get; set; }

View File

@@ -1,3 +1,6 @@
using System;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Models.StoreViewModels
{
public enum WalletSetupMethod
@@ -34,5 +37,21 @@ namespace BTCPayServer.Models.StoreViewModels
WalletSetupMethod.WatchOnly => "GenerateWallet",
_ => "SetupWallet"
};
internal void SetPermission(WalletCreationPermissions perm)
{
this.CanCreateNewColdWallet = perm.CanCreateColdWallet;
this.CanUseHotWallet = perm.CanCreateHotWallet;
this.CanUseRPCImport = perm.CanRPCImport;
}
public void SetViewData(ViewDataDictionary ViewData)
{
ViewData.Add(nameof(CanUseHotWallet), CanUseHotWallet);
ViewData.Add(nameof(CanCreateNewColdWallet), CanCreateNewColdWallet);
ViewData.Add(nameof(CanUseRPCImport), CanUseRPCImport);
ViewData.Add(nameof(SupportSegwit), SupportSegwit);
ViewData.Add(nameof(SupportTaproot), SupportTaproot);
ViewData.Add(nameof(Method), Method);
}
}
}

View File

@@ -52,6 +52,8 @@ namespace BTCPayServer.Services
[Display(Name = "Non-admins can create Hot Wallets for their Store")]
public bool AllowHotWalletForAll { get; set; }
[Display(Name = "Non-admins can create Cold Wallets for their Store")]
public bool AllowCreateColdWalletForAll { get; set; }
[Display(Name = "Non-admins can import Hot Wallets for their Store")]
public bool AllowHotWalletRPCImportForAll { get; set; }

View File

@@ -120,6 +120,13 @@
</div>
</div>
</div>
<div class="d-flex my-3">
<input asp-for="AllowCreateColdWalletForAll" type="checkbox" class="btcpay-toggle me-3" />
<div>
<label asp-for="AllowCreateColdWalletForAll" class="form-check-label"></label>
<span asp-validation-for="AllowCreateColdWalletForAll" class="text-danger"></span>
</div>
</div>
<div class="d-flex my-3">
<input asp-for="AllowHotWalletRPCImportForAll" type="checkbox" class="btcpay-toggle me-3"/>
<div>

View File

@@ -1,14 +1,10 @@
@model WalletSetupViewModel
@{
var isHotWallet = Model.Method == WalletSetupMethod.HotWallet;
var title = isHotWallet ? StringLocalizer["Create {0} Hot Wallet", Model.CryptoCode] : StringLocalizer["Create {0} Watch-Only Wallet", Model.CryptoCode];
Layout = "_LayoutWalletSetup";
ViewData.SetActivePage(StoreNavPages.OnchainSettings, title, $"{Context.GetStoreData().Id}-{Model.CryptoCode}");
ViewData.Add(nameof(Model.CanUseHotWallet), Model.CanUseHotWallet);
ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport);
ViewData.Add(nameof(Model.SupportSegwit), Model.SupportSegwit);
ViewData.Add(nameof(Model.SupportTaproot), Model.SupportTaproot);
ViewData.Add(nameof(Model.Method), Model.Method);
var isHotWallet = Model.Method == WalletSetupMethod.HotWallet;
var title = isHotWallet ? StringLocalizer["Create {0} Hot Wallet", Model.CryptoCode] : StringLocalizer["Create {0} Watch-Only Wallet", Model.CryptoCode];
Layout = "_LayoutWalletSetup";
ViewData.SetActivePage(StoreNavPages.OnchainSettings, title, $"{Context.GetStoreData().Id}-{Model.CryptoCode}");
Model.SetViewData(ViewData);
}
@section Navbar {

View File

@@ -8,56 +8,71 @@
}
@section Navbar {
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
<vc:icon symbol="back" />
</a>
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
<vc:icon symbol="back" />
</a>
}
<h1 class="text-center" text-translate="true">Choose your wallet option</h1>
<div class="list-group mt-5">
@if (Model.CanUseHotWallet)
{
<a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.HotWallet.ToString()" id="GenerateHotwalletLink" class="list-group-item list-group-item-action">
<div class="image">
<vc:icon symbol="wallet-hot"/>
</div>
<div class="content">
@if (Model.CanUseHotWallet)
{
<a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.HotWallet.ToString()" id="GenerateHotwalletLink" class="list-group-item list-group-item-action">
<div class="image">
<vc:icon symbol="wallet-hot"/>
</div>
<div class="content">
<h4 text-translate="true">Hot wallet</h4>
<p class="mb-0 text-secondary" text-translate="true">
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.
</p>
</div>
<vc:icon symbol="caret-right"/>
</a>
}
else
{
<div class="list-group-item text-muted">
<div class="image">
<vc:icon symbol="wallet-hot"/>
</div>
<div class="content">
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.
</p>
</div>
<vc:icon symbol="caret-right"/>
</a>
}
else
{
<div class="list-group-item text-muted">
<div class="image">
<vc:icon symbol="wallet-hot"/>
</div>
<div class="content">
<h4 text-translate="true">Hot wallet</h4>
<p class="mb-0" text-translate="true">Please note that creating a hot wallet is not supported by this instance for non administrators.</p>
</div>
</div>
}
</div>
</div>
}
</div>
<div class="list-group mt-4">
<a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.WatchOnly.ToString()" id="GenerateWatchonlyLink" class="list-group-item list-group-item-action">
<div class="image">
<vc:icon symbol="wallet-watchonly"/>
</div>
<div class="content">
<h4 text-translate="true">Watch-only wallet</h4>
<p class="mb-0 text-secondary" text-translate="true">
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.
</p>
</div>
<vc:icon symbol="caret-right" />
</a>
@if (Model.CanCreateNewColdWallet)
{
<a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.WatchOnly.ToString()" id="GenerateWatchonlyLink" class="list-group-item list-group-item-action">
<div class="image">
<vc:icon symbol="wallet-watchonly"/>
</div>
<div class="content">
<h4 text-translate="true">Watch-only wallet</h4>
<p class="mb-0 text-secondary" text-translate="true">
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.
</p>
</div>
<vc:icon symbol="caret-right" />
</a>
}
else
{
<div class="list-group-item text-muted">
<div class="image">
<vc:icon symbol="wallet-watchonly" />
</div>
<div class="content">
<h4 text-translate="true">Watch-only wallet</h4>
<p class="mb-0" text-translate="true">Please note that this instance does not support creating a new cold wallet for non-administrators. However, you can import one from other wallet software.</p>
</div>
</div>
}
</div>

View File

@@ -18,12 +18,7 @@
<div class="my-5">
@if (Model.CanUseHotWallet)
{
ViewData.Add(nameof(Model.CanUseHotWallet), Model.CanUseHotWallet);
ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport);
ViewData.Add(nameof(Model.SupportSegwit), Model.SupportSegwit);
ViewData.Add(nameof(Model.SupportTaproot), Model.SupportTaproot);
ViewData.Add(nameof(Model.Method), Model.Method);
Model.SetViewData(ViewData);
<partial name="_GenerateWalletForm" model="Model.SetupRequest" />
}
else