...
 
Commits (6)
......@@ -29,7 +29,17 @@ apt-get install git cmake build-essential libssl-dev pkg-config libboost-all-dev
<para>and latest MS .Net Core Framework</para>
<code language="cs">
https://dotnet.microsoft.com/download/linux-package-manager/ubuntu16-04/sdk-current
https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1604
wget -q https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
And install SDK 2.2
sudo apt-get update
sudo apt-get install apt-transport-https
sudo apt-get update
apt-get install dotnet-sdk-2.2
</code>
<para>After completing these steps you can continue with a first run.</para>
......@@ -121,6 +131,14 @@ To:
server=1
</code>
<para>To enable UPnp set:</para>
<code language="cs">
Change line:
#upnp=0
To:
upnp=1
</code>
<para>Configure API RPC-JSON access authentication:</para>
<para><legacyBold>If you don't need to secure access to API RPC-JSON just set server=1 only.</legacyBold></para>
<code language="cs">
......
......@@ -21,6 +21,8 @@ Join our community on [Discord](https://t.co/ns9nldLSrv).
.NET Core is required to build and run the node software. The installation and setup notes below have been tested on Ubuntu 16.04+. There is a convenience wrapper around most processes is provided to make setup quick.
**Follow full installation process at http://wiki.bitcoinrh.org/.**
1. Clone the repository:
```
......
using System;
using System.Threading;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
......@@ -15,6 +16,7 @@ using BRhodium.Node.Utilities;
using BRhodium.Node.Utilities.JsonConverters;
using Xunit;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
namespace BRhodium.Bitcoin.Features.Wallet.Tests
{
......@@ -2954,6 +2956,39 @@ namespace BRhodium.Bitcoin.Features.Wallet.Tests
Assert.Equal(0, remainingTrxs.Count);
}
[Fact]
public void TestLockAndUnlockWallet()
{
DataFolder dataFolder = CreateDataFolder(this);
var walletManager = new WalletManager(
this.LoggerFactory.Object, this.Network, new Mock<ConcurrentChain>().Object, NodeSettings.Default(this.Network), new Mock<WalletSettings>().Object,
dataFolder, new Mock<IWalletFeePolicy>().Object, new Mock<IAsyncLoopFactory>().Object, new NodeLifetime(), DateTimeProvider.Default, null, null, new EphemeralDataProtectionProvider());
var fixture = new WalletFixture();
fixture.GenerateBlankWallet("default", "test");
Assert.NotNull(walletManager.WalletSecrets);
walletManager.WalletSecrets.UnlockWallet("default", "test", DateTime.Now.AddSeconds(10));
Assert.Equal("test", walletManager.WalletSecrets.GetWalletPassword("default"));
walletManager.WalletSecrets.LockWallet("default");
Assert.Null(walletManager.WalletSecrets.GetWalletPassword("default"));
}
[Fact]
public void TestExpiration()
{
DataFolder dataFolder = CreateDataFolder(this);
var walletManager = new WalletManager(
this.LoggerFactory.Object, this.Network, new Mock<ConcurrentChain>().Object, NodeSettings.Default(this.Network), new Mock<WalletSettings>().Object,
dataFolder, new Mock<IWalletFeePolicy>().Object, new Mock<IAsyncLoopFactory>().Object, new NodeLifetime(), DateTimeProvider.Default, null,null, new EphemeralDataProtectionProvider());
var fixture = new WalletFixture();
fixture.GenerateBlankWallet("default", "test");
walletManager.WalletSecrets.UnlockWallet("default", "test", DateTime.Now.AddSeconds(1));
Assert.Equal("test", walletManager.WalletSecrets.GetWalletPassword("default"));
Thread.Sleep(1000);
Assert.Null(walletManager.WalletSecrets.GetWalletPassword("default"));
}
private (Mnemonic mnemonic, Wallet wallet) CreateWalletOnDiskAndDeleteWallet(WalletManager walletManager, string password, string passphrase, string walletName, ConcurrentChain chain)
{
......
using System.Linq.Expressions;
using System.Linq.Expressions;
using System;
using System.Collections.Generic;
using System.Linq;
......@@ -69,9 +69,6 @@ namespace BRhodium.Bitcoin.Features.Wallet.Controllers
/// <summary>Hd addresses to address list</summary>
public static ConcurrentDictionary<string, HdAddress> HdAddressByAddressMap = new ConcurrentDictionary<string, HdAddress>();
private static ConcurrentDictionary<string, string> walletPassword = new ConcurrentDictionary<string, string>();
private static ConcurrentDictionary<string, DateTime> walletPasswordExpiration = new ConcurrentDictionary<string, DateTime>();
private static bool inRescan = false;
public WalletRPCController(
......@@ -158,9 +155,7 @@ namespace BRhodium.Bitcoin.Features.Wallet.Controllers
}
var dateExpiration = DateTime.Now.AddSeconds(timeout);
walletPassword.AddOrReplace(walletName, password);
walletPasswordExpiration.AddOrReplace(walletName, dateExpiration);
this.walletManager.WalletSecrets.UnlockWallet(walletName, password, dateExpiration);
return this.Json(ResultHelper.BuildResultResponse(true));
}
catch (Exception e)
......@@ -214,24 +209,8 @@ namespace BRhodium.Bitcoin.Features.Wallet.Controllers
throw new RPCException(RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY, "Wallet not initialized", null, false);
}
var passwordExpiration = walletPasswordExpiration.TryGet(currWallet.Name);
string password = null;
if (passwordExpiration == null)
{
throw new ArgumentNullException("password");
}
else
{
if (passwordExpiration < DateTime.Now)
{
walletPassword.TryRemove(currWallet.Name, out password);
throw new ArgumentNullException("password");
}
else
{
walletPassword.TryGetValue(currWallet.Name, out password);
}
}
var password = this.walletManager.WalletSecrets.GetWalletPassword(currWallet.Name);
if (hdAddress == null)
{
throw new RPCException(RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY, "Wallet not initialized", null, false);
......@@ -279,19 +258,7 @@ namespace BRhodium.Bitcoin.Features.Wallet.Controllers
{
try
{
if (string.IsNullOrEmpty(walletName))
{
walletPassword = new ConcurrentDictionary<string, string>();
walletPasswordExpiration = new ConcurrentDictionary<string, DateTime>();
}
else
{
string password;
DateTime passwordExpiration;
walletPassword.TryRemove(walletName, out password);
walletPasswordExpiration.TryRemove(walletName, out passwordExpiration);
}
this.walletManager.WalletSecrets.LockWallet(walletName);
return this.Json(ResultHelper.BuildResultResponse(true));
}
catch (Exception e)
......@@ -742,20 +709,7 @@ namespace BRhodium.Bitcoin.Features.Wallet.Controllers
{
var address = param1;
var amount = Decimal.Parse(param2);
string password;
DateTime passwordExpiration;
walletPasswordExpiration.TryGetValue(WalletRPCUtil.DEFAULT_WALLET, out passwordExpiration);
if (passwordExpiration < DateTime.Now)
{
walletPassword.TryRemove(WalletRPCUtil.DEFAULT_WALLET, out password);
throw new ArgumentNullException(nameof(password));
}
else
{
walletPassword.TryGetValue(WalletRPCUtil.DEFAULT_WALLET, out password);
}
var password = this.walletManager.WalletSecrets.GetWalletPassword(WalletRPCUtil.DEFAULT_WALLET);
Guard.NotEmpty(password, nameof(password));
Guard.NotEmpty(address, nameof(address));
......@@ -1575,7 +1529,7 @@ namespace BRhodium.Bitcoin.Features.Wallet.Controllers
var txCount = wallet.GetAllTransactionsByCoinType((CoinType)this.Network.Consensus.CoinType);
result.TxCount = txCount == null ? 0 : txCount.Count();
var passwordExpiration = walletPasswordExpiration.TryGet(wallet.Name);
var passwordExpiration = this.walletManager.WalletSecrets.GetWalletPasswordExpiration(wallet.Name);
if (passwordExpiration == null)
{
throw new ArgumentNullException("password");
......
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using BRhodium.Bitcoin.Features.Consensus.Models;
......@@ -357,5 +357,11 @@ namespace BRhodium.Bitcoin.Features.Wallet.Interfaces
/// </summary>
/// <value></value>
WalletSettings WalletSettings { get; }
/// <summary>
/// Store and access wallet secrets.
/// </summary>
/// <value></value>
WalletSecrets WalletSecrets { get; }
}
}
......@@ -19,6 +19,7 @@ using BRhodium.Node.Interfaces;
using BRhodium.Node.Signals;
using System.Globalization;
using BRhodium.Node.Utilities;
using Microsoft.AspNetCore.DataProtection;
namespace BRhodium.Bitcoin.Features.Wallet
{
......@@ -206,6 +207,7 @@ namespace BRhodium.Bitcoin.Features.Wallet
services.AddSingleton<BroadcasterBehavior>();
services.AddSingleton<WalletSettings>(walletSettings);
services.AddSingleton<IWalletKeyPool, WalletKeyPool>();
services.AddSingleton<IDataProtectionProvider, EphemeralDataProtectionProvider>();
});
});
......
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
......@@ -17,6 +17,7 @@ using BRhodium.Bitcoin.Features.Consensus.Models;
using System.Diagnostics;
using System.IO;
using BRhodium.Bitcoin.Features.BlockStore;
using Microsoft.AspNetCore.DataProtection;
[assembly: InternalsVisibleTo("BRhodium.Bitcoin.Features.Wallet.Tests")]
......@@ -27,6 +28,9 @@ namespace BRhodium.Bitcoin.Features.Wallet
/// </summary>
public class WalletManager : IWalletManager
{
// <summary>As per RPC method definition this should be the max allowable expiry duration.</summary>
private const int MaxWalletUnlockDurationInSeconds = 60 * 60 * 5;
/// <summary>Size of the buffer of unused addresses maintained in an account. </summary>
private const int UnusedAddressesBuffer = 20;
......@@ -85,6 +89,9 @@ namespace BRhodium.Bitcoin.Features.Wallet
/// <summary>The settings for the wallet feature.</summary>
private readonly WalletSettings walletSettings;
private WalletSecrets walletSecrets;
IDataProtector _protector;
/// <summary>
/// Makes the wallet settings public for other functions.
/// </summary>
......@@ -96,6 +103,18 @@ namespace BRhodium.Bitcoin.Features.Wallet
}
}
/// <summary>
/// Store and manage wallet secrets.
/// </summary>
/// <value></value>
public WalletSecrets WalletSecrets
{
get
{
return this.walletSecrets;
}
}
public uint256 WalletTipHash { get; set; }
/// <summary>Memory locked unspendable transaction parts (tx hash, index vount)</summary>
......@@ -123,9 +142,9 @@ namespace BRhodium.Bitcoin.Features.Wallet
IAsyncLoopFactory asyncLoopFactory,
INodeLifetime nodeLifetime,
IDateTimeProvider dateTimeProvider,
//IBlockStoreCache blockStoreCache,
IBroadcasterManager broadcasterManager = null,
WalletRepository walletRepository = null) // no need to know about transactions the node will broadcast to.
IBroadcasterManager broadcasterManager = null, // no need to know about transactions the node will broadcast to.
WalletRepository walletRepository = null,
IDataProtectionProvider provider = null)
{
Guard.NotNull(loggerFactory, nameof(loggerFactory));
Guard.NotNull(network, nameof(network));
......@@ -168,6 +187,7 @@ namespace BRhodium.Bitcoin.Features.Wallet
this.addressByScriptLookup = new ConcurrentDictionary<ScriptId, WalletLinkedHdAddress>();
this.addressLookup = new ConcurrentDictionary<string, WalletLinkedHdAddress>();
this.outpointLookup = new ConcurrentDictionary<OutPoint, TransactionData>();
this.walletSecrets = new WalletSecrets(provider);
}
......
using System.Diagnostics.Tracing;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System;
using System.Collections.Concurrent;
using BRhodium.Node.Utilities;
using Microsoft.AspNetCore.DataProtection;
using NBitcoin;
namespace BRhodium.Bitcoin.Features.Wallet
{
/// <summary>
/// Store wallet secrets without leaving passwords left unencrypted in memory.
/// Can timeout passwords.
/// </summary>
public class WalletSecrets
{
private ConcurrentDictionary<string, string> walletPasswords;
private ConcurrentDictionary<string, DateTime> walletPasswordsExpiration;
private IDataProtectionProvider provider;
private IDataProtector protector;
/// <summary>
/// Generate a WalletSecrets storage with a data protection provider.
/// </summary>
/// <param name="provider"></param>
public WalletSecrets(IDataProtectionProvider provider)
{
this.provider = provider;
this.protector = provider.CreateProtector("BRhodium.Bitcoin.Features.Wallet.WalletSecrets");
this.walletPasswords = new ConcurrentDictionary<string, string>();
this.walletPasswordsExpiration = new ConcurrentDictionary<string, DateTime>();
}
/// <summary>
/// Stora and cache a reference to the wallet password in memory for [timeout] seconds.
/// </summary>
/// <param name="walletName"></param>
/// <param name="password"></param>
/// <param name="timeout"></param>
public async void UnlockWallet(string walletName, string password, DateTime timeout)
{
Guard.NotEmpty(walletName, nameof(walletName));
Guard.NotEmpty(password, nameof(password));
var protectedPassword = this.protector.Protect(password);
this.walletPasswords.TryAdd(walletName, protectedPassword);
this.walletPasswordsExpiration.TryAdd(walletName, timeout);
DateTime value;
this.walletPasswordsExpiration.TryGetValue(walletName, out value);
await Task.Run(async delegate
{
await Task.Delay((int)(Utils.DateTimeToUnixTime(timeout) - Utils.DateTimeToUnixTime(DateTime.Now)));
this.LockWallet(walletName);
});
this.walletPasswordsExpiration.TryGetValue(walletName, out value);
}
/// <summary>
/// Lock the wallet by removing password information.
/// </summary>
/// <param name="walletName"></param>
public void LockWallet(string walletName)
{
string password;
DateTime passwordExpiration;
this.walletPasswords.TryRemove(walletName, out password);
this.walletPasswordsExpiration.TryRemove(walletName, out passwordExpiration);
}
/// <summary>
/// Gets wallet password only if it has not expired already.
/// </summary>
/// <param name="walletName"></param>
/// <returns></returns>
public string GetWalletPassword(string walletName)
{
DateTime passwordExpiration;
this.walletPasswordsExpiration.TryGetValue(walletName, out passwordExpiration);
if (passwordExpiration < DateTime.Now)
{
this.LockWallet(walletName);
return null;
}
string password;
this.walletPasswords.TryGetValue(walletName, out password);
return this.protector.Unprotect(password);
}
/// <summary>
/// Get the wallet password expiration.
/// </summary>
/// <param name="walletName"></param>
/// <returns></returns>
public DateTime GetWalletPasswordExpiration(string walletName)
{
DateTime expiration;
this.walletPasswordsExpiration.TryGetValue(walletName, out expiration);
return expiration;
}
}
}
\ No newline at end of file