< 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: 39_18410846617
Line coverage
0%
Covered lines: 0
Uncovered lines: 127
Coverable lines: 127
Total lines: 278
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%
get_HeightOfBirth()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
 045    public uint HeightOfBirth { get; init; }
 46
 47    /// <summary>
 48    /// Manages secure key operations for generating and managing cryptographic keys.
 49    /// Provides functionality to safely store, load, and derive secure keys protected in memory.
 50    /// </summary>
 51    /// <param name="privateKey">The private key to be managed.</param>
 52    /// <param name="network">The network associated with the private key.</param>
 53    /// <param name="filePath">The file path for storing the key data.</param>
 54    /// <param name="heightOfBirth">Block Height when the wallet was created</param>
 055    public SecureKeyManager(byte[] privateKey, BitcoinNetwork network, string filePath, uint heightOfBirth)
 56    {
 057        _privateKeyLength = (ulong)privateKey.Length;
 58
 059        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 60
 61        // Allocate secure memory
 062        _securePrivateKeyPtr = cryptoProvider.MemoryAlloc(_privateKeyLength);
 63
 64        // Lock the memory to prevent swapping
 065        if (cryptoProvider.MemoryLock(_securePrivateKeyPtr, _privateKeyLength) == -1)
 066            throw new InvalidOperationException("Failed to lock memory.");
 67
 68        // Copy the private key to secure memory
 069        Marshal.Copy(privateKey, 0, _securePrivateKeyPtr, (int)_privateKeyLength);
 70
 71        // Get Output Descriptor
 072        _network = Network.GetNetwork(network)
 073                ?? throw new ArgumentException("Invalid network specified.", nameof(network));
 074        var extKey = new ExtKey(new Key(privateKey), network.ChainHash);
 075        var xpub = extKey.Neuter().ToString(_network);
 076        var fingerprint = extKey.GetPublicKey().GetHDFingerPrint();
 77
 078        OutputDescriptor = $"wpkh([{fingerprint}/{KeyPath}/*]{xpub}/0/*)";
 79
 80        // Securely wipe the original key from regular memory
 081        cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(privateKey, 0), _privateKeyLength);
 82
 083        _filePath = filePath;
 084        HeightOfBirth = heightOfBirth;
 085    }
 86
 87    public ExtPrivKey GetNextKey(out uint index)
 88    {
 089        lock (_lastUsedIndexLock)
 90        {
 091            _lastUsedIndex++;
 092            index = _lastUsedIndex;
 093        }
 94
 95        // Derive the key at m/6425'/0'/0'/0/index
 096        var masterKey = GetMasterKey();
 097        var derivedKey = masterKey.Derive(_keyPath.Derive(index));
 98
 099        _ = UpdateLastUsedIndexOnFile().ContinueWith(task =>
 0100        {
 0101            if (task.IsFaulted)
 0102                Console.Error.WriteLine($"Failed to update last used index on file: {task.Exception.Message}");
 0103        }, TaskContinuationOptions.OnlyOnFaulted);
 104
 0105        return derivedKey.ToBytes();
 106    }
 107
 108    public ExtPrivKey GetKeyAtIndex(uint index)
 109    {
 0110        var masterKey = GetMasterKey();
 0111        return masterKey.Derive(_keyPath.Derive(index)).ToBytes();
 112    }
 113
 114    public CryptoKeyPair GetNodeKeyPair()
 115    {
 0116        var masterKey = GetMasterKey();
 0117        return new CryptoKeyPair(masterKey.PrivateKey.ToBytes(), masterKey.PrivateKey.PubKey.ToBytes());
 118    }
 119
 120    public CompactPubKey GetNodePubKey()
 121    {
 0122        var masterKey = GetMasterKey();
 0123        return masterKey.PrivateKey.PubKey.ToBytes();
 124    }
 125
 126    public async Task UpdateLastUsedIndexOnFile()
 127    {
 0128        var jsonString = await File.ReadAllTextAsync(_filePath);
 0129        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0130                ?? throw new SerializationException("Invalid key file");
 131
 0132        lock (_lastUsedIndexLock)
 133        {
 0134            data.LastUsedIndex = _lastUsedIndex;
 0135        }
 136
 0137        jsonString = JsonSerializer.Serialize(data);
 138
 0139        await File.WriteAllTextAsync(_filePath, jsonString);
 0140    }
 141
 142    public void SaveToFile(string password)
 143    {
 0144        lock (_lastUsedIndexLock)
 145        {
 0146            var extKey = GetMasterKey();
 0147            var extKeyBytes = Encoding.UTF8.GetBytes(extKey.ToString(_network));
 148
 0149            Span<byte> key = stackalloc byte[CryptoConstants.PrivkeyLen];
 0150            Span<byte> nonce = stackalloc byte[CryptoConstants.Xchacha20Poly1305NonceLen];
 0151            Span<byte> cipherText = stackalloc byte[extKeyBytes.Length + CryptoConstants.Xchacha20Poly1305TagLen];
 152
 0153            using var argon2Id = new Argon2Id();
 0154            argon2Id.DeriveKeyFromPasswordAndSalt(password, s_salt, key);
 155
 0156            using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0157            xChaCha20Poly1305.Encrypt(key, nonce, ReadOnlySpan<byte>.Empty, extKeyBytes, cipherText);
 158
 0159            var data = new KeyFileData
 0160            {
 0161                Network = _network.ToString(),
 0162                LastUsedIndex = _lastUsedIndex,
 0163                Descriptor = OutputDescriptor,
 0164                EncryptedExtKey = Convert.ToBase64String(cipherText),
 0165                HeightOfBirth = HeightOfBirth
 0166            };
 0167            var json = JsonSerializer.Serialize(data);
 0168            File.WriteAllText(_filePath, json);
 169        }
 0170    }
 171
 172    public static SecureKeyManager FromMnemonic(string mnemonic, string passphrase, BitcoinNetwork network,
 173                                                string? filePath = null, uint currentHeight = 0)
 174    {
 0175        if (string.IsNullOrWhiteSpace(filePath))
 0176            filePath = GetKeyFilePath(network);
 177
 0178        var mnemonicObj = new Mnemonic(mnemonic, Wordlist.English);
 0179        var extKey = mnemonicObj.DeriveExtKey(passphrase);
 0180        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), network, filePath, currentHeight);
 181    }
 182
 183    public static SecureKeyManager FromFilePath(string filePath, BitcoinNetwork expectedNetwork, string password)
 184    {
 0185        var jsonString = File.ReadAllText(filePath);
 0186        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0187                ?? throw new SerializationException("Invalid key file");
 188
 0189        if (expectedNetwork != data.Network.ToLowerInvariant())
 0190            throw new Exception($"Invalid network. Expected {expectedNetwork}, but got {data.Network}");
 191
 0192        var network = Network.GetNetwork(expectedNetwork)
 0193                   ?? throw new ArgumentException("Invalid network specified.", nameof(expectedNetwork));
 194
 0195        var encryptedExtKey = Convert.FromBase64String(data.EncryptedExtKey);
 0196        Span<byte> nonce = stackalloc byte[CryptoConstants.Xchacha20Poly1305NonceLen];
 197
 0198        Span<byte> key = stackalloc byte[CryptoConstants.PrivkeyLen];
 0199        using var argon2Id = new Argon2Id();
 0200        argon2Id.DeriveKeyFromPasswordAndSalt(password, s_salt, key);
 201
 0202        Span<byte> extKeyBytes = stackalloc byte[encryptedExtKey.Length - CryptoConstants.Xchacha20Poly1305TagLen];
 0203        using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0204        xChaCha20Poly1305.Decrypt(key, nonce, ReadOnlySpan<byte>.Empty, encryptedExtKey, extKeyBytes);
 205
 0206        var extKeyStr = Encoding.UTF8.GetString(extKeyBytes);
 0207        var extKey = ExtKey.Parse(extKeyStr, network);
 208
 0209        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), expectedNetwork, filePath, data.HeightOfBirth)
 0210        {
 0211            _lastUsedIndex = data.LastUsedIndex,
 0212            OutputDescriptor = data.Descriptor
 0213        };
 0214    }
 215
 216    /// <summary>
 217    /// Gets the path for the Key file
 218    /// </summary>
 219    public static string GetKeyFilePath(string network)
 220    {
 0221        var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 0222        var networkDir = Path.Combine(homeDir, ".nltg", network);
 0223        Directory.CreateDirectory(networkDir); // Ensure directory exists
 0224        return Path.Combine(networkDir, "nltg.key.json"); //DaemonConstants.KeyFile);
 225    }
 226
 227    private ExtKey GetMasterKey()
 228    {
 0229        return new ExtKey(new Key(GetPrivateKeyBytes()), _network.GenesisHash.ToBytes());
 230    }
 231
 232    private void ReleaseUnmanagedResources()
 233    {
 0234        if (_securePrivateKeyPtr == IntPtr.Zero)
 0235            return;
 236
 0237        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 238
 239        // Securely wipe the memory before freeing it
 0240        cryptoProvider.MemoryZero(_securePrivateKeyPtr, _privateKeyLength);
 241
 242        // Unlock the memory
 0243        cryptoProvider.MemoryUnlock(_securePrivateKeyPtr, _privateKeyLength);
 244
 245        // MemoryFree the memory
 0246        cryptoProvider.MemoryFree(_securePrivateKeyPtr);
 247
 0248        _privateKeyLength = 0;
 0249        _securePrivateKeyPtr = IntPtr.Zero;
 0250    }
 251
 252    /// <summary>
 253    /// Retrieves the private key stored in secure memory.
 254    /// </summary>
 255    /// <returns>The private key as a byte array.</returns>
 256    /// <exception cref="InvalidOperationException">Thrown if the key is not initialized.</exception>
 257    private byte[] GetPrivateKeyBytes()
 258    {
 0259        if (_securePrivateKeyPtr == IntPtr.Zero)
 0260            throw new InvalidOperationException("Secure key is not initialized.");
 261
 0262        var privateKey = new byte[_privateKeyLength];
 0263        Marshal.Copy(_securePrivateKeyPtr, privateKey, 0, (int)_privateKeyLength);
 264
 0265        return privateKey;
 266    }
 267
 268    public void Dispose()
 269    {
 0270        ReleaseUnmanagedResources();
 0271        GC.SuppressFinalize(this);
 0272    }
 273
 274    ~SecureKeyManager()
 275    {
 0276        ReleaseUnmanagedResources();
 0277    }
 278}