Merge pull request #1745 from dennisreimann/recovery-seed

Recovery seed
This commit is contained in:
Nicolas Dorier 2020-07-22 20:55:20 +09:00 committed by GitHub
commit 27d7d03d4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 168 additions and 13 deletions

View File

@ -130,11 +130,16 @@ namespace BTCPayServer.Tests
Driver.WaitForElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
Logs.Tester.LogInformation("Trying to click btn-generate");
Driver.WaitForElement(By.Id("btn-generate")).ForceClick();
// Seed backup page
AssertHappyMessage();
if (string.IsNullOrEmpty(seed))
{
seed = Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text;
seed = Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
}
// Confirm seed backup
Driver.FindElement(By.Id("confirm")).Click();
Driver.FindElement(By.Id("submit")).Click();
WalletId = new WalletId(StoreId, cryptoCode);
return new Mnemonic(seed);
}

View File

@ -544,7 +544,6 @@ namespace BTCPayServer.Tests
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
sess.ListenAllTrackedSource();
var nextEvent = sess.NextEventAsync();
@ -556,6 +555,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GoToStore(storeId.storeId);
s.GenerateWallet("BTC", "", true, false);
@ -565,12 +565,10 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
var invoiceId = s.CreateInvoice(storeId.storeName);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
@ -664,13 +662,15 @@ namespace BTCPayServer.Tests
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings);
s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
s.AssertHappyMessage();
Assert.Equal(mnemonic.ToString(), s.Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text);
// Seed backup page
var recoveryPhrase = s.Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
Assert.Contains("The recovery phrase will also be stored on a server as a hot wallet.", s.Driver.PageSource);
}
}
void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Authorization;
@ -191,6 +192,13 @@ namespace BTCPayServer.Controllers
return View(vm);
}
[Route("recovery-seed-backup")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult RecoverySeedBackup(RecoverySeedBackupViewModel vm)
{
return View("RecoverySeedBackup", vm);
}
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });

View File

@ -305,8 +305,17 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"Your wallet has been generated. Please store your seed securely! <br/><code class=\"alert-link\">{response.Mnemonic}</code>"
Html = $"<span class='text-centered'>Your wallet has been generated.</span>"
});
var vm = new RecoverySeedBackupViewModel()
{
CryptoCode = cryptoCode,
Mnemonic = response.Mnemonic,
Passphrase = response.Passphrase,
IsStored = request.SavePrivateKeys,
ReturnUrl = Url.Action(nameof(UpdateStore), new { storeId })
};
return this.RedirectToRecoverySeedBackup(vm);
}
else
{

View File

@ -9,6 +9,7 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Security;
@ -1144,11 +1145,14 @@ namespace BTCPayServer.Controllers
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
var recoveryVm = new RecoverySeedBackupViewModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"Please store your seed securely! <br/><code class=\"alert-link\">{seed}</code>"
});
CryptoCode = walletId.CryptoCode,
Mnemonic = seed,
IsStored = true,
ReturnUrl = Url.Action(nameof(WalletSettings), new { walletId })
};
return this.RedirectToRecoverySeedBackup(recoveryVm);
}
return RedirectToAction(nameof(WalletSettings));

View File

@ -12,12 +12,14 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -429,5 +431,23 @@ namespace BTCPayServer
{
ctx.Items["BTCPAY.STORESDATA"] = storeData;
}
public static IActionResult RedirectToRecoverySeedBackup(this Controller controller, RecoverySeedBackupViewModel vm)
{
var redirectVm = new PostRedirectViewModel()
{
AspController = "Home",
AspAction = "RecoverySeedBackup",
Parameters =
{
new KeyValuePair<string, string>("cryptoCode", vm.CryptoCode),
new KeyValuePair<string, string>("mnemonic", vm.Mnemonic),
new KeyValuePair<string, string>("passphrase", vm.Passphrase),
new KeyValuePair<string, string>("isStored", vm.IsStored ? "true" : "false"),
new KeyValuePair<string, string>("returnUrl", vm.ReturnUrl)
}
};
return controller.View("PostRedirect", redirectVm);
}
}
}

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using NBitcoin;
namespace BTCPayServer.Models.StoreViewModels
{
public class RecoverySeedBackupViewModel
{
public string CryptoCode { get; set; }
public string Mnemonic { get; set; }
public string Passphrase { get; set; }
public bool IsStored { get; set; }
public string ReturnUrl { get; set; }
public string[] Words
{
get => Mnemonic.Split(" ");
}
}
}

View File

@ -0,0 +1,75 @@
@model RecoverySeedBackupViewModel
@{
Layout = "_LayoutSimple";
ViewData["Title"] = "Your recovery phrase";
}
<style>
@@media (min-width: 476px) { ol#recovery-phrase {max-height:16em;} }
@@media (min-width: 768px) { ol#recovery-phrase {max-height:12em;} }
@@media (min-width: 1200px) { ol#recovery-phrase {max-height:8em;} }
form#recovery-confirmation button { position: absolute; bottom:0; left:50%; width:200px; margin-left:-100px; }
form#recovery-confirmation button:not([disabled]) { display: none; }
form#recovery-confirmation input:checked ~ button[disabled] { display: none; }
form#recovery-confirmation input:checked + button:not([disabled]) { display: inline-block; }
</style>
<div class="row justify-content-md-center">
<div class="col-lg-10 text-center">
<partial name="_StatusMessage" />
<div class="d-flex flex-column align-items-center justify-content-center">
<span class="fa fa-info-circle align-self-center p-3" style="font-size:4em;"></span>
<h1 class="text-center text-primary mb-5">Secure your recovery&nbsp;phrase</h1>
</div>
<div class="lead text-center">
<p>
<span class="d-sm-block">The words below are called your recovery phrase.</span>
<span class="d-sm-block"><strong>Write them down on a piece of paper in the exact order.</strong></span>
</p>
</div>
<ol id="recovery-phrase" data-mnemonic="@Model.Mnemonic" class="my-5 d-flex flex-column flex-wrap justify-content-center align-items-center text-left p-0">
@foreach (var word in Model.Words)
{
<li class="ml-4 px-3 py-2 text-secondary" style="flex: 0 1;min-width:10em;">
<span class="text-dark h5">@word</span>
</li>
}
</ol>
<div class="lead text-center" style="max-width:36em;margin-left:auto;margin-right:auto;">
@if (Model.IsStored)
{
<p>
The recovery phrase is a backup that allows you to restore your wallet in case of a server crash.
If you lose it or write it down incorrectly, you may permanently lose access to your funds.
Do not photograph it. Do not store it digitally.
</p>
<p>
<strong>The recovery phrase will also be stored on a server as a hot wallet.</strong>
</p>
}
else
{
<p>
The recovery phrase allows you to access and restore your wallet.
If you lose it or write it down incorrectly, you will permanently lose access to your funds.
Do not photograph the recovery phrase and do not store it digitally.
</p>
<p>
<strong>The recovery phrase will permanently be erased from the server.</strong>
</p>
}
@if (!string.IsNullOrEmpty(Model.Passphrase))
{
<p class="mt-3 mb-0">Please make sure to also write down your passphrase.</p>
}
</div>
<form id="recovery-confirmation" action="@Model.ReturnUrl" class="d-flex align-items-start justify-content-center" style="margin-top:4rem;padding-bottom: 80px">
<label class="form-check-label lead order-2" for="confirm">I have written down my recovery phrase and stored it in a secure location</label>
<input type="checkbox" class="mt-2 mr-3 order-1" id="confirm">
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" id="submit">Done</button>
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" disabled>Done</button>
</form>
</div>
</div>

View File

@ -1,12 +1,25 @@
@model PostRedirectViewModel
@{
Layout = null;
var routeData = Context.GetRouteData();
var routeParams = new Dictionary<string, string>();
if (routeData != null)
{
routeParams["walletId"] = routeData.Values["walletId"]?.ToString();
}
}
<html>
<head></head>
<body>
<form method="post" id="postform" asp-action="@Model.AspAction" asp-controller="@Model.AspController" asp-route-walletId="@this.Context.GetRouteValue("walletId").ToString()">
<form
method="post"
id="postform"
asp-action="@Model.AspAction"
asp-controller="@Model.AspController"
asp-all-route-data="@routeParams"
>
@foreach(var o in Model.Parameters) {
<input type="hidden" name="@o.Key" value="@o.Value" />
}