< 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: 36_15743069263
Line coverage
0%
Covered lines: 0
Uncovered lines: 124
Coverable lines: 124
Total lines: 273
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)0%2040%
get_KeyPath()100%210%
get_OutputDescriptor()100%210%
GetNextKey(...)0%620%
GetKeyAtIndex(...)100%210%
GetNodeKeyPair()100%210%
GetNodePubKey()100%210%
UpdateLastUsedIndexOnFile()0%620%
SaveToFile(...)100%210%
FromMnemonic(...)0%620%
FromFilePath(...)0%4260%
GetKeyFilePath(...)100%210%
GetMasterKey()100%210%
ReleaseUnmanagedResources()0%620%
GetPrivateKeyBytes()0%620%
Dispose()100%210%
Finalize()100%210%

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.ValueObjects;
 10using Domain.Crypto.Constants;
 11using Domain.Crypto.ValueObjects;
 12using Domain.Protocol.Interfaces;
 13using Domain.Protocol.ValueObjects;
 14using Infrastructure.Crypto.Ciphers;
 15using Infrastructure.Crypto.Factories;
 16using Infrastructure.Crypto.Hashes;
 17using Node.Models;
 18
 19/// <summary>
 20/// Manages a securely stored private key using protected memory allocation.
 21/// This class ensures that the private key remains inaccessible from regular memory
 22/// and is securely wiped when no longer needed.
 23/// </summary>
 24public class SecureKeyManager : ISecureKeyManager, IDisposable
 25{
 026    private static readonly byte[] s_salt =
 027    [
 028        0xFF, 0x1D, 0x3B, 0xF5, 0x24, 0xA2, 0xB7, 0xA9,
 029        0xC3, 0x1B, 0x1F, 0x58, 0xE9, 0x48, 0xB5, 0x69
 030    ];
 31
 32    private readonly string _filePath;
 033    private readonly object _lastUsedIndexLock = new();
 34    private readonly Network _network;
 035    private readonly KeyPath _keyPath = new("m/6425'/0'/0'/0");
 36
 37    private uint _lastUsedIndex;
 38    private ulong _privateKeyLength;
 39    private IntPtr _securePrivateKeyPtr;
 40
 041    public BitcoinKeyPath KeyPath => _keyPath.ToBytes();
 42
 043    public string OutputDescriptor { get; init; }
 44
 45    /// <summary>
 46    /// Manages secure key operations for generating and managing cryptographic keys.
 47    /// Provides functionality to safely store, load, and derive secure keys protected in memory.
 48    /// </summary>
 49    /// <param name="privateKey">The private key to be managed.</param>
 50    /// <param name="network">The network associated with the private key.</param>
 51    /// <param name="filePath">The file path for storing the key data.</param>
 052    public SecureKeyManager(byte[] privateKey, BitcoinNetwork network, string filePath)
 53    {
 054        _privateKeyLength = (ulong)privateKey.Length;
 55
 056        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 57
 58        // Allocate secure memory
 059        _securePrivateKeyPtr = cryptoProvider.MemoryAlloc(_privateKeyLength);
 60
 61        // Lock the memory to prevent swapping
 062        if (cryptoProvider.MemoryLock(_securePrivateKeyPtr, _privateKeyLength) == -1)
 063            throw new InvalidOperationException("Failed to lock memory.");
 64
 65        // Copy the private key to secure memory
 066        Marshal.Copy(privateKey, 0, _securePrivateKeyPtr, (int)_privateKeyLength);
 67
 68        // Get Output Descriptor
 069        _network = Network.GetNetwork(network)
 070                ?? throw new ArgumentException("Invalid network specified.", nameof(network));
 071        var extKey = new ExtKey(new Key(privateKey), network.ChainHash);
 072        var xpub = extKey.Neuter().ToString(_network);
 073        var fingerprint = extKey.GetPublicKey().GetHDFingerPrint();
 74
 075        OutputDescriptor = $"wpkh([{fingerprint}/{KeyPath}/*]{xpub}/0/*)";
 76
 77        // Securely wipe the original key from regular memory
 078        cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(privateKey, 0), _privateKeyLength);
 79
 080        _filePath = filePath;
 081    }
 82
 83    public ExtPrivKey GetNextKey(out uint index)
 84    {
 085        lock (_lastUsedIndexLock)
 86        {
 087            _lastUsedIndex++;
 088            index = _lastUsedIndex;
 089        }
 90
 91        // Derive the key at m/6425'/0'/0'/0/index
 092        var masterKey = GetMasterKey();
 093        var derivedKey = masterKey.Derive(_keyPath.Derive(index));
 94
 095        _ = UpdateLastUsedIndexOnFile().ContinueWith(task =>
 096        {
 097            if (task.IsFaulted)
 098                Console.Error.WriteLine($"Failed to update last used index on file: {task.Exception.Message}");
 099        }, TaskContinuationOptions.OnlyOnFaulted);
 100
 0101        return derivedKey.ToBytes();
 102    }
 103
 104    public ExtPrivKey GetKeyAtIndex(uint index)
 105    {
 0106        var masterKey = GetMasterKey();
 0107        return masterKey.Derive(_keyPath.Derive(index)).ToBytes();
 108    }
 109
 110    public CryptoKeyPair GetNodeKeyPair()
 111    {
 0112        var masterKey = GetMasterKey();
 0113        return new CryptoKeyPair(masterKey.PrivateKey.ToBytes(), masterKey.PrivateKey.PubKey.ToBytes());
 114    }
 115
 116    public CompactPubKey GetNodePubKey()
 117    {
 0118        var masterKey = GetMasterKey();
 0119        return masterKey.PrivateKey.PubKey.ToBytes();
 120    }
 121
 122    public async Task UpdateLastUsedIndexOnFile()
 123    {
 0124        var jsonString = await File.ReadAllTextAsync(_filePath);
 0125        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0126                ?? throw new SerializationException("Invalid key file");
 127
 0128        lock (_lastUsedIndexLock)
 129        {
 0130            data.LastUsedIndex = _lastUsedIndex;
 0131        }
 132
 0133        jsonString = JsonSerializer.Serialize(data);
 134
 0135        await File.WriteAllTextAsync(_filePath, jsonString);
 0136    }
 137
 138    public void SaveToFile(string password)
 139    {
 0140        lock (_lastUsedIndexLock)
 141        {
 0142            var extKey = GetMasterKey();
 0143            var extKeyBytes = Encoding.UTF8.GetBytes(extKey.ToString(_network));
 144
 0145            Span<byte> key = stackalloc byte[CryptoConstants.PrivkeyLen];
 0146            Span<byte> nonce = stackalloc byte[CryptoConstants.Xchacha20Poly1305NonceLen];
 0147            Span<byte> cipherText = stackalloc byte[extKeyBytes.Length + CryptoConstants.Xchacha20Poly1305TagLen];
 148
 0149            using var argon2Id = new Argon2Id();
 0150            argon2Id.DeriveKeyFromPasswordAndSalt(password, s_salt, key);
 151
 0152            using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0153            xChaCha20Poly1305.Encrypt(key, nonce, ReadOnlySpan<byte>.Empty, extKeyBytes, cipherText);
 154
 0155            var data = new KeyFileData
 0156            {
 0157                Network = _network.ToString(),
 0158                LastUsedIndex = _lastUsedIndex,
 0159                Descriptor = OutputDescriptor,
 0160                EncryptedExtKey = Convert.ToBase64String(cipherText)
 0161            };
 0162            var json = JsonSerializer.Serialize(data);
 0163            File.WriteAllText(_filePath, json);
 164        }
 0165    }
 166
 167    public static SecureKeyManager FromMnemonic(string mnemonic, string passphrase, BitcoinNetwork network,
 168                                                string? filePath = null)
 169    {
 0170        if (string.IsNullOrWhiteSpace(filePath))
 0171            filePath = GetKeyFilePath(network);
 172
 0173        var mnemonicObj = new Mnemonic(mnemonic, Wordlist.English);
 0174        var extKey = mnemonicObj.DeriveExtKey(passphrase);
 0175        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), network, filePath);
 176    }
 177
 178    public static SecureKeyManager FromFilePath(string filePath, BitcoinNetwork expectedNetwork, string password)
 179    {
 0180        var jsonString = File.ReadAllText(filePath);
 0181        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0182                ?? throw new SerializationException("Invalid key file");
 183
 0184        if (expectedNetwork != data.Network.ToLowerInvariant())
 0185            throw new Exception($"Invalid network. Expected {expectedNetwork}, but got {data.Network}");
 186
 0187        var network = Network.GetNetwork(expectedNetwork)
 0188                   ?? throw new ArgumentException("Invalid network specified.", nameof(expectedNetwork));
 189
 0190        var encryptedExtKey = Convert.FromBase64String(data.EncryptedExtKey);
 0191        Span<byte> nonce = stackalloc byte[CryptoConstants.Xchacha20Poly1305NonceLen];
 192
 0193        Span<byte> key = stackalloc byte[CryptoConstants.PrivkeyLen];
 0194        using var argon2Id = new Argon2Id();
 0195        argon2Id.DeriveKeyFromPasswordAndSalt(password, s_salt, key);
 196
 0197        Span<byte> extKeyBytes = stackalloc byte[encryptedExtKey.Length - CryptoConstants.Xchacha20Poly1305TagLen];
 0198        using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0199        xChaCha20Poly1305.Decrypt(key, nonce, ReadOnlySpan<byte>.Empty, encryptedExtKey, extKeyBytes);
 200
 0201        var extKeyStr = Encoding.UTF8.GetString(extKeyBytes);
 0202        var extKey = ExtKey.Parse(extKeyStr, network);
 203
 0204        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), expectedNetwork, filePath)
 0205        {
 0206            _lastUsedIndex = data.LastUsedIndex,
 0207            OutputDescriptor = data.Descriptor
 0208        };
 0209    }
 210
 211    /// <summary>
 212    /// Gets the path for the Key file
 213    /// </summary>
 214    public static string GetKeyFilePath(string network)
 215    {
 0216        var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 0217        var networkDir = Path.Combine(homeDir, ".nltg", network);
 0218        Directory.CreateDirectory(networkDir); // Ensure directory exists
 0219        return Path.Combine(networkDir, "nltg.key.json"); //DaemonConstants.KeyFile);
 220    }
 221
 222    private ExtKey GetMasterKey()
 223    {
 0224        return new ExtKey(new Key(GetPrivateKeyBytes()), _network.GenesisHash.ToBytes());
 225    }
 226
 227    private void ReleaseUnmanagedResources()
 228    {
 0229        if (_securePrivateKeyPtr == IntPtr.Zero)
 0230            return;
 231
 0232        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 233
 234        // Securely wipe the memory before freeing it
 0235        cryptoProvider.MemoryZero(_securePrivateKeyPtr, _privateKeyLength);
 236
 237        // Unlock the memory
 0238        cryptoProvider.MemoryUnlock(_securePrivateKeyPtr, _privateKeyLength);
 239
 240        // MemoryFree the memory
 0241        cryptoProvider.MemoryFree(_securePrivateKeyPtr);
 242
 0243        _privateKeyLength = 0;
 0244        _securePrivateKeyPtr = IntPtr.Zero;
 0245    }
 246
 247    /// <summary>
 248    /// Retrieves the private key stored in secure memory.
 249    /// </summary>
 250    /// <returns>The private key as a byte array.</returns>
 251    /// <exception cref="InvalidOperationException">Thrown if the key is not initialized.</exception>
 252    private byte[] GetPrivateKeyBytes()
 253    {
 0254        if (_securePrivateKeyPtr == IntPtr.Zero)
 0255            throw new InvalidOperationException("Secure key is not initialized.");
 256
 0257        var privateKey = new byte[_privateKeyLength];
 0258        Marshal.Copy(_securePrivateKeyPtr, privateKey, 0, (int)_privateKeyLength);
 259
 0260        return privateKey;
 261    }
 262
 263    public void Dispose()
 264    {
 0265        ReleaseUnmanagedResources();
 0266        GC.SuppressFinalize(this);
 0267    }
 268
 269    ~SecureKeyManager()
 270    {
 0271        ReleaseUnmanagedResources();
 0272    }
 273}