< Summary - Combined Code Coverage

Information
Class: NLightning.Infrastructure.Bitcoin.Managers.SecureKeyManager
Assembly: NLightning.Infrastructure.Bitcoin
File(s): /home/runner/work/NLightning/NLightning/src/NLightning.Infrastructure.Bitcoin/Managers/SecureKeyManager.cs
Tag: 57_24045730253
Line coverage
0%
Covered lines: 0
Uncovered lines: 206
Coverable lines: 206
Total lines: 302
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 44
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/NLightning/NLightning/src/NLightning.Infrastructure.Bitcoin/Managers/SecureKeyManager.cs

#LineLine coverage
 1using System.Runtime.InteropServices;
 2using System.Runtime.Serialization;
 3using System.Text;
 4using System.Text.Json;
 5using NBitcoin;
 6
 7namespace NLightning.Infrastructure.Bitcoin.Managers;
 8
 9using Domain.Bitcoin.Constants;
 10using Domain.Bitcoin.ValueObjects;
 11using Domain.Crypto.Constants;
 12using Domain.Crypto.ValueObjects;
 13using Domain.Protocol.Interfaces;
 14using Domain.Protocol.ValueObjects;
 15using Infrastructure.Crypto.Ciphers;
 16using Infrastructure.Crypto.Factories;
 17using Infrastructure.Crypto.Hashes;
 18using Node.Models;
 19
 20/// <summary>
 21/// Manages a securely stored private key using protected memory allocation.
 22/// This class ensures that the private key remains inaccessible from regular memory
 23/// and is securely wiped when no longer needed.
 24/// </summary>
 25public class SecureKeyManager : ISecureKeyManager, IDisposable
 026{
 027    private static readonly byte[] s_salt =
 028    [
 029        0xFF, 0x1D, 0x3B, 0xF5, 0x24, 0xA2, 0xB7, 0xA9,
 030        0xC3, 0x1B, 0x1F, 0x58, 0xE9, 0x48, 0xB5, 0x69
 031    ];
 32
 033    private readonly string _filePath;
 034    private readonly object _lastUsedIndexLock = new();
 035    private readonly Network _network;
 036    private readonly KeyPath _channelKeyPath = new(KeyConstants.ChannelKeyPathString);
 037    private readonly KeyPath _depositP2TrKeyPath = new(KeyConstants.P2TrKeyPathString);
 038    private readonly KeyPath _depositP2WpkhKeyPath = new(KeyConstants.P2WpkhKeyPathString);
 39
 40    private uint _lastUsedIndex;
 041    private ulong _privateKeyLength;
 42    private IntPtr _securePrivateKeyPtr;
 043
 044    public BitcoinKeyPath ChannelKeyPath => _channelKeyPath.ToBytes();
 045    public BitcoinKeyPath DepositP2TrKeyPath => _depositP2TrKeyPath.ToBytes();
 046    public BitcoinKeyPath DepositP2WpkhKeyPath => _depositP2WpkhKeyPath.ToBytes();
 47
 048    public string OutputChannelDescriptor { get; init; }
 049    public string OutputDepositP2TrDescriptor { get; init; }
 50
 051    public string OutputDepositP2WshDescriptor { get; init; }
 052    public string OutputChangeP2TrDescriptor { get; init; }
 53
 054    public string OutputChangeP2WshDescriptor { get; init; }
 055
 056    public uint HeightOfBirth { get; init; }
 057
 58    /// <summary>
 059    /// Manages secure key operations for generating and managing cryptographic keys.
 60    /// Provides functionality to safely store, load, and derive secure keys protected in memory.
 61    /// </summary>
 062    /// <param name="privateKey">The private key to be managed.</param>
 63    /// <param name="network">The network associated with the private key.</param>
 64    /// <param name="filePath">The file path for storing the key data.</param>
 065    /// <param name="heightOfBirth">Block Height when the wallet was created</param>
 066    public SecureKeyManager(byte[] privateKey, BitcoinNetwork network, string filePath, uint heightOfBirth)
 67    {
 068        _privateKeyLength = (ulong)privateKey.Length;
 069
 070        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 71
 072        // Allocate secure memory
 073        _securePrivateKeyPtr = cryptoProvider.MemoryAlloc(_privateKeyLength);
 074
 075        // Lock the memory to prevent swapping
 076        if (cryptoProvider.MemoryLock(_securePrivateKeyPtr, _privateKeyLength) == -1)
 077            throw new InvalidOperationException("Failed to lock memory.");
 078
 79        // Copy the private key to secure memory
 080        Marshal.Copy(privateKey, 0, _securePrivateKeyPtr, (int)_privateKeyLength);
 081
 82        // Get Output Descriptor
 083        _network = Network.GetNetwork(network)
 084                ?? throw new ArgumentException("Invalid network specified.", nameof(network));
 085        var extKey = new ExtKey(new Key(privateKey), network.ChainHash);
 086        var xpub = extKey.Neuter().ToString(_network);
 087        var fingerprint = extKey.GetPublicKey().GetHDFingerPrint();
 88
 089        OutputChannelDescriptor = $"wpkh([{fingerprint}/{ChannelKeyPath}/*]{xpub}/0/*)";
 090        OutputDepositP2TrDescriptor = $"tr([{fingerprint}/{DepositP2TrKeyPath}]{xpub}/0/*)";
 091        OutputChangeP2TrDescriptor = $"tr([{fingerprint}/{DepositP2TrKeyPath}]{xpub}/1/*)";
 092        OutputDepositP2WshDescriptor = $"wpkh([{fingerprint}/{DepositP2WpkhKeyPath}]{xpub}/0/*)";
 093        OutputChangeP2WshDescriptor = $"wpkh([{fingerprint}/{DepositP2WpkhKeyPath}]{xpub}/1/*)";
 94
 95        // Securely wipe the original key from regular memory
 096        cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(privateKey, 0), _privateKeyLength);
 097
 098        _filePath = filePath;
 099        HeightOfBirth = heightOfBirth;
 0100    }
 0101
 0102    public ExtPrivKey GetNextChannelKey(out uint index)
 0103    {
 0104        lock (_lastUsedIndexLock)
 0105        {
 0106            _lastUsedIndex++;
 0107            index = _lastUsedIndex;
 0108        }
 109
 0110        // Derive the key at m/6425'/0'/0'/0/index
 0111        var masterKey = GetMasterKey();
 0112        var derivedKey = masterKey.Derive(_channelKeyPath.Derive(index));
 113
 0114        _ = UpdateLastUsedChannelIndexOnFile().ContinueWith(task =>
 0115        {
 0116            if (task.IsFaulted)
 0117                Console.Error.WriteLine($"Failed to update last used index on file: {task.Exception.Message}");
 0118        }, TaskContinuationOptions.OnlyOnFaulted);
 119
 0120        return derivedKey.ToBytes();
 121    }
 0122
 0123    public ExtPrivKey GetChannelKeyAtIndex(uint index)
 124    {
 0125        var masterKey = GetMasterKey();
 0126        return masterKey.Derive(_channelKeyPath.Derive(index)).ToBytes();
 127    }
 0128
 0129    public ExtPrivKey GetDepositP2TrKeyAtIndex(uint index, bool isChange)
 0130    {
 0131        var masterKey = GetMasterKey();
 0132        return masterKey.Derive(_depositP2TrKeyPath.Derive(isChange ? "1" : "0")).Derive(index).ToBytes();
 133    }
 0134
 0135    public ExtPrivKey GetDepositP2WpkhKeyAtIndex(uint index, bool isChange)
 136    {
 0137        var masterKey = GetMasterKey();
 0138        return masterKey.Derive(_depositP2WpkhKeyPath.Derive(isChange ? "1" : "0")).Derive(index).ToBytes();
 0139    }
 0140
 141    public CryptoKeyPair GetNodeKeyPair()
 142    {
 0143        var masterKey = GetMasterKey();
 0144        return new CryptoKeyPair(masterKey.PrivateKey.ToBytes(), masterKey.PrivateKey.PubKey.ToBytes());
 145    }
 0146
 0147    public CompactPubKey GetNodePubKey()
 148    {
 0149        var masterKey = GetMasterKey();
 0150        return masterKey.PrivateKey.PubKey.ToBytes();
 0151    }
 152
 0153    public async Task UpdateLastUsedChannelIndexOnFile()
 0154    {
 0155        var jsonString = await File.ReadAllTextAsync(_filePath);
 0156        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0157                ?? throw new SerializationException("Invalid key file");
 158
 0159        lock (_lastUsedIndexLock)
 0160        {
 0161            data.LastUsedIndex = _lastUsedIndex;
 0162        }
 0163
 0164        jsonString = JsonSerializer.Serialize(data);
 0165
 0166        await File.WriteAllTextAsync(_filePath, jsonString);
 0167    }
 0168
 169    public void SaveToFile(string password)
 0170    {
 0171        lock (_lastUsedIndexLock)
 172        {
 0173            var extKey = GetMasterKey();
 0174            var extKeyBytes = Encoding.UTF8.GetBytes(extKey.ToString(_network));
 0175
 0176            Span<byte> key = stackalloc byte[CryptoConstants.PrivkeyLen];
 0177            Span<byte> nonce = stackalloc byte[CryptoConstants.Xchacha20Poly1305NonceLen];
 0178            Span<byte> cipherText = stackalloc byte[extKeyBytes.Length + CryptoConstants.Xchacha20Poly1305TagLen];
 0179
 0180            using var argon2Id = new Argon2Id();
 0181            argon2Id.DeriveKeyFromPasswordAndSalt(password, s_salt, key);
 182
 0183            using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0184            xChaCha20Poly1305.Encrypt(key, nonce, ReadOnlySpan<byte>.Empty, extKeyBytes, cipherText);
 0185
 0186            var data = new KeyFileData
 0187            {
 0188                Network = _network.ToString(),
 0189                LastUsedIndex = _lastUsedIndex,
 0190                Descriptor = OutputChannelDescriptor,
 0191                EncryptedExtKey = Convert.ToBase64String(cipherText),
 0192                HeightOfBirth = HeightOfBirth
 0193            };
 0194            var json = JsonSerializer.Serialize(data);
 0195            File.WriteAllText(_filePath, json);
 0196        }
 0197    }
 0198
 0199    public static SecureKeyManager FromMnemonic(string mnemonic, string passphrase, BitcoinNetwork network,
 0200                                                string? filePath = null, uint currentHeight = 0)
 201    {
 0202        if (string.IsNullOrWhiteSpace(filePath))
 0203            filePath = GetKeyFilePath(network);
 0204
 0205        var mnemonicObj = new Mnemonic(mnemonic, Wordlist.English);
 0206        var extKey = mnemonicObj.DeriveExtKey(passphrase);
 0207        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), network, filePath, currentHeight);
 208    }
 0209
 0210    public static SecureKeyManager FromFilePath(string filePath, BitcoinNetwork expectedNetwork, string password)
 0211    {
 0212        var jsonString = File.ReadAllText(filePath);
 0213        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0214                ?? throw new SerializationException("Invalid key file");
 215
 0216        if (expectedNetwork != data.Network.ToLowerInvariant())
 0217            throw new Exception($"Invalid network. Expected {expectedNetwork}, but got {data.Network}");
 218
 0219        var network = Network.GetNetwork(expectedNetwork)
 0220                   ?? throw new ArgumentException("Invalid network specified.", nameof(expectedNetwork));
 0221
 0222        var encryptedExtKey = Convert.FromBase64String(data.EncryptedExtKey);
 0223        Span<byte> nonce = stackalloc byte[CryptoConstants.Xchacha20Poly1305NonceLen];
 0224
 0225        Span<byte> key = stackalloc byte[CryptoConstants.PrivkeyLen];
 0226        using var argon2Id = new Argon2Id();
 0227        argon2Id.DeriveKeyFromPasswordAndSalt(password, s_salt, key);
 228
 0229        Span<byte> extKeyBytes = stackalloc byte[encryptedExtKey.Length - CryptoConstants.Xchacha20Poly1305TagLen];
 0230        using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0231        xChaCha20Poly1305.Decrypt(key, nonce, ReadOnlySpan<byte>.Empty, encryptedExtKey, extKeyBytes);
 232
 0233        var extKeyStr = Encoding.UTF8.GetString(extKeyBytes);
 0234        var extKey = ExtKey.Parse(extKeyStr, network);
 0235
 0236        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), expectedNetwork, filePath, data.HeightOfBirth)
 0237        {
 0238            _lastUsedIndex = data.LastUsedIndex,
 0239            OutputChannelDescriptor = data.Descriptor
 0240        };
 0241    }
 242
 0243    /// <summary>
 244    /// Gets the path for the Key file
 245    /// </summary>
 0246    public static string GetKeyFilePath(string configPath)
 247    {
 0248        return Path.Combine(configPath, "nltg.key.json");
 0249    }
 0250
 251    private ExtKey GetMasterKey()
 252    {
 0253        return new ExtKey(new Key(GetPrivateKeyBytes()), _network.GenesisHash.ToBytes());
 254    }
 255
 256    private void ReleaseUnmanagedResources()
 257    {
 0258        if (_securePrivateKeyPtr == IntPtr.Zero)
 0259            return;
 0260
 0261        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 0262
 0263        // Securely wipe the memory before freeing it
 0264        cryptoProvider.MemoryZero(_securePrivateKeyPtr, _privateKeyLength);
 0265
 266        // Unlock the memory
 0267        cryptoProvider.MemoryUnlock(_securePrivateKeyPtr, _privateKeyLength);
 268
 269        // MemoryFree the memory
 0270        cryptoProvider.MemoryFree(_securePrivateKeyPtr);
 0271
 0272        _privateKeyLength = 0;
 0273        _securePrivateKeyPtr = IntPtr.Zero;
 0274    }
 275
 0276    /// <summary>
 0277    /// Retrieves the private key stored in secure memory.
 278    /// </summary>
 279    /// <returns>The private key as a byte array.</returns>
 280    /// <exception cref="InvalidOperationException">Thrown if the key is not initialized.</exception>
 281    private byte[] GetPrivateKeyBytes()
 282    {
 0283        if (_securePrivateKeyPtr == IntPtr.Zero)
 0284            throw new InvalidOperationException("Secure key is not initialized.");
 285
 0286        var privateKey = new byte[_privateKeyLength];
 0287        Marshal.Copy(_securePrivateKeyPtr, privateKey, 0, (int)_privateKeyLength);
 288
 0289        return privateKey;
 290    }
 291
 292    public void Dispose()
 293    {
 0294        ReleaseUnmanagedResources();
 0295        GC.SuppressFinalize(this);
 0296    }
 297
 298    ~SecureKeyManager()
 299    {
 0300        ReleaseUnmanagedResources();
 0301    }
 302}

Methods/Properties

.cctor()
.cctor()
.ctor(System.Byte[],NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,System.String,System.UInt32)
.ctor(System.Byte[],NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,System.String,System.UInt32)
get_KeyPath()
get_OutputDescriptor()
get_ChannelKeyPath()
get_HeightOfBirth()
get_DepositP2TrKeyPath()
get_DepositP2WpkhKeyPath()
get_OutputChannelDescriptor()
get_OutputDepositP2TrDescriptor()
get_OutputDepositP2WshDescriptor()
get_OutputChangeP2TrDescriptor()
get_OutputChangeP2WshDescriptor()
get_HeightOfBirth()
GetNextKey(System.UInt32&)
GetNextChannelKey(System.UInt32&)
GetKeyAtIndex(System.UInt32)
GetNodeKeyPair()
GetNodePubKey()
GetChannelKeyAtIndex(System.UInt32)
UpdateLastUsedIndexOnFile()
GetDepositP2TrKeyAtIndex(System.UInt32,System.Boolean)
GetDepositP2WpkhKeyAtIndex(System.UInt32,System.Boolean)
GetNodeKeyPair()
SaveToFile(System.String)
GetNodePubKey()
UpdateLastUsedChannelIndexOnFile()
SaveToFile(System.String)
FromMnemonic(System.String,System.String,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,System.String,System.UInt32)
FromFilePath(System.String,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,System.String)
FromMnemonic(System.String,System.String,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,System.String,System.UInt32)
FromFilePath(System.String,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,System.String)
GetKeyFilePath(System.String)
GetMasterKey()
ReleaseUnmanagedResources()
GetKeyFilePath(System.String)
GetMasterKey()
ReleaseUnmanagedResources()
GetPrivateKeyBytes()
Dispose()
Finalize()
GetPrivateKeyBytes()
Dispose()
Finalize()