< Summary - Combined Code Coverage

Information
Class: NLightning.Application.NLTG.Managers.SecureKeyManager
Assembly: NLightning.Application.NLTG
File(s): /home/runner/work/nlightning/nlightning/src/NLightning.Application.NLTG/Managers/SecureKeyManager.cs
Tag: 30_15166811759
Line coverage
0%
Covered lines: 0
Uncovered lines: 120
Coverable lines: 120
Total lines: 262
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 18
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)0%620%
get_OutputDescriptor()100%210%
GetNextKey(...)0%620%
GetNodeKey()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.Application.NLTG/Managers/SecureKeyManager.cs

#LineLine coverage
 1using System.Runtime.InteropServices;
 2using System.Runtime.Serialization;
 3using System.Text;
 4using System.Text.Json;
 5using NBitcoin;
 6using NLightning.Domain.Crypto.Constants;
 7using NLightning.Domain.Protocol.Managers;
 8using Serilog;
 9
 10namespace NLightning.Application.NLTG.Managers;
 11
 12using Constants;
 13using Infrastructure.Crypto.Ciphers;
 14using Infrastructure.Crypto.Factories;
 15using Infrastructure.Crypto.Hashes;
 16using Models;
 17
 18/// <summary>
 19/// Manages a securely stored private key using protected memory allocation.
 20/// This class ensures that the private key remains inaccessible from regular memory
 21/// and is securely wiped when no longer needed.
 22/// </summary>
 23public class SecureKeyManager : ISecureKeyManager, IDisposable
 24{
 25    private readonly string _filePath;
 026    private readonly object _lastUsedIndexLock = new();
 027    private readonly Network _network = Network.Main;
 28
 29    private uint _lastUsedIndex;
 30    private ulong _privateKeyLength;
 31    private IntPtr _securePrivateKeyPtr;
 32
 33    public const string PATH = "m/6425'/0'/0'/0/{0}";
 34
 035    public string OutputDescriptor { get; init; }
 36
 37    /// <summary>
 38    /// Manages secure key operations for generating and managing cryptographic keys.
 39    /// Provides functionality to safely store, load, and derive secure keys protected in memory.
 40    /// </summary>
 41    /// <param name="privateKey">The private key to be managed.</param>
 42    /// <param name="network">The network associated with the private key.</param>
 43    /// <param name="filePath">The file path for storing the key data.</param>
 044    public SecureKeyManager(byte[] privateKey, Network network, string filePath)
 45    {
 046        _privateKeyLength = (ulong)privateKey.Length;
 47
 048        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 49
 50        // Allocate secure memory
 051        _securePrivateKeyPtr = cryptoProvider.MemoryAlloc(_privateKeyLength);
 52
 53        // Lock the memory to prevent swapping
 054        if (cryptoProvider.MemoryLock(_securePrivateKeyPtr, _privateKeyLength) == -1)
 055            throw new InvalidOperationException("Failed to lock memory.");
 56
 57        // Copy the private key to secure memory
 058        Marshal.Copy(privateKey, 0, _securePrivateKeyPtr, (int)_privateKeyLength);
 59
 60        // Get Output Descriptor
 061        var extKey = new ExtKey(new Key(privateKey), network.GenesisHash.ToBytes());
 062        var xpub = extKey.Neuter().ToString(_network);
 063        var fingerprint = extKey.GetPublicKey().GetHDFingerPrint();
 64
 065        OutputDescriptor = $"wpkh([{fingerprint}/{string.Format(PATH, "*")}]{xpub}/0/*)";
 66
 67        // Securely wipe the original key from regular memory
 068        cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(privateKey, 0), _privateKeyLength);
 69
 070        _filePath = filePath;
 071        _network = network;
 072    }
 73
 74    public ExtKey GetNextKey(out uint index)
 75    {
 076        lock (_lastUsedIndexLock)
 77        {
 078            _lastUsedIndex++;
 079            index = _lastUsedIndex;
 080        }
 81
 82        // Derive the key at m/6425'/0'/0'/0/index
 083        var masterKey = GetMasterKey();
 084        var derivedKey = masterKey.Derive(new KeyPath(string.Format(PATH, index)));
 85
 086        _ = UpdateLastUsedIndexOnFile().ContinueWith(task =>
 087        {
 088            if (task.IsFaulted)
 089            {
 090                Log.Error(task.Exception, "Failed to update last used index on file");
 091            }
 092        }, TaskContinuationOptions.OnlyOnFaulted);
 93
 094        return derivedKey;
 95    }
 96
 97    public Key GetNodeKey()
 98    {
 099        var masterKey = GetMasterKey();
 0100        return masterKey.PrivateKey;
 101    }
 102
 103    public PubKey GetNodePubKey()
 104    {
 0105        var masterKey = GetMasterKey();
 0106        return masterKey.PrivateKey.PubKey;
 107    }
 108
 109    public async Task UpdateLastUsedIndexOnFile()
 110    {
 0111        var jsonString = File.ReadAllText(_filePath);
 0112        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0113                   ?? throw new SerializationException("Invalid key file");
 114
 0115        lock (_lastUsedIndexLock)
 116        {
 0117            data.LastUsedIndex = _lastUsedIndex;
 0118        }
 119
 0120        jsonString = JsonSerializer.Serialize(data);
 121
 0122        await File.WriteAllTextAsync(_filePath, jsonString);
 0123    }
 124
 125    public void SaveToFile(string password)
 126    {
 0127        lock (_lastUsedIndexLock)
 128        {
 0129            var extKey = GetMasterKey();
 0130            var extKeyBytes = Encoding.UTF8.GetBytes(extKey.ToString(_network));
 131
 0132            Span<byte> salt = stackalloc byte[CryptoConstants.XCHACHA20_POLY1305_TAG_LEN];
 0133            Span<byte> key = stackalloc byte[CryptoConstants.PRIVKEY_LEN];
 0134            Span<byte> nonce = stackalloc byte[CryptoConstants.XCHACHA20_POLY1305_NONCE_LEN];
 0135            Span<byte> cipherText = stackalloc byte[extKeyBytes.Length + CryptoConstants.XCHACHA20_POLY1305_TAG_LEN];
 136
 0137            using var argon2Id = new Argon2Id();
 0138            argon2Id.DeriveKeyFromPasswordAndSalt(password, salt, key);
 139
 0140            using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0141            xChaCha20Poly1305.Encrypt(key, nonce, ReadOnlySpan<byte>.Empty, extKeyBytes, cipherText);
 142
 0143            var data = new KeyFileData
 0144            {
 0145                Network = _network.ToString(),
 0146                LastUsedIndex = _lastUsedIndex,
 0147                Descriptor = OutputDescriptor,
 0148                EncryptedExtKey = Convert.ToBase64String(cipherText),
 0149                Nonce = Convert.ToBase64String(nonce),
 0150                Salt = Convert.ToBase64String(salt)
 0151            };
 0152            var json = JsonSerializer.Serialize(data);
 0153            File.WriteAllText(_filePath, json);
 154        }
 0155    }
 156
 157    public static SecureKeyManager FromMnemonic(string mnemonic, string passphrase, Network network,
 158                                                string? filePath = null)
 159    {
 0160        if (string.IsNullOrWhiteSpace(filePath))
 0161            filePath = GetKeyFilePath(network.ToString());
 162
 0163        var mnemonicObj = new Mnemonic(mnemonic, Wordlist.English);
 0164        var extKey = mnemonicObj.DeriveExtKey(passphrase);
 0165        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), network, filePath);
 166    }
 167
 168    public static SecureKeyManager FromFilePath(string filePath, Network expectedNetwork, string password)
 169    {
 0170        var jsonString = File.ReadAllText(filePath);
 0171        var data = JsonSerializer.Deserialize<KeyFileData>(jsonString)
 0172                   ?? throw new SerializationException("Invalid key file");
 173
 0174        var network = Network.GetNetwork(data.Network) ?? throw new Exception("Invalid network");
 0175        if (expectedNetwork != network)
 0176            throw new Exception($"Invalid network. Expected {expectedNetwork}, but got {network}");
 177
 0178        var encryptedExtKey = Convert.FromBase64String(data.EncryptedExtKey);
 0179        var nonce = Convert.FromBase64String(data.Nonce);
 0180        var salt = Convert.FromBase64String(data.Salt);
 181
 0182        Span<byte> key = stackalloc byte[CryptoConstants.PRIVKEY_LEN];
 0183        using var argon2Id = new Argon2Id();
 0184        argon2Id.DeriveKeyFromPasswordAndSalt(password, salt, key);
 185
 0186        Span<byte> extKeyBytes = stackalloc byte[encryptedExtKey.Length - CryptoConstants.XCHACHA20_POLY1305_TAG_LEN];
 0187        using var xChaCha20Poly1305 = new XChaCha20Poly1305();
 0188        xChaCha20Poly1305.Decrypt(key, nonce, ReadOnlySpan<byte>.Empty, encryptedExtKey, extKeyBytes);
 189
 0190        var extKeyStr = Encoding.UTF8.GetString(extKeyBytes);
 0191        var extKey = ExtKey.Parse(extKeyStr, network);
 192
 0193        return new SecureKeyManager(extKey.PrivateKey.ToBytes(), network, filePath)
 0194        {
 0195            _lastUsedIndex = data.LastUsedIndex,
 0196            OutputDescriptor = data.Descriptor
 0197        };
 0198    }
 199
 200    /// <summary>
 201    /// Gets the path for the Key file
 202    /// </summary>
 203    public static string GetKeyFilePath(string network)
 204    {
 0205        var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 0206        var networkDir = Path.Combine(homeDir, ".nltg", network);
 0207        Directory.CreateDirectory(networkDir); // Ensure directory exists
 0208        return Path.Combine(networkDir, DaemonConstants.KEY_FILE);
 209    }
 210
 211    private ExtKey GetMasterKey()
 212    {
 0213        return new ExtKey(new Key(GetPrivateKeyBytes()), _network.GenesisHash.ToBytes());
 214    }
 215
 216    private void ReleaseUnmanagedResources()
 217    {
 0218        if (_securePrivateKeyPtr == IntPtr.Zero)
 0219            return;
 220
 0221        using var cryptoProvider = CryptoFactory.GetCryptoProvider();
 222
 223        // Securely wipe the memory before freeing it
 0224        cryptoProvider.MemoryZero(_securePrivateKeyPtr, _privateKeyLength);
 225
 226        // Unlock the memory
 0227        cryptoProvider.MemoryUnlock(_securePrivateKeyPtr, _privateKeyLength);
 228
 229        // MemoryFree the memory
 0230        cryptoProvider.MemoryFree(_securePrivateKeyPtr);
 231
 0232        _privateKeyLength = 0;
 0233        _securePrivateKeyPtr = IntPtr.Zero;
 0234    }
 235
 236    /// <summary>
 237    /// Retrieves the private key stored in secure memory.
 238    /// </summary>
 239    /// <returns>The private key as a byte array.</returns>
 240    /// <exception cref="InvalidOperationException">Thrown if the key is not initialized.</exception>
 241    private byte[] GetPrivateKeyBytes()
 242    {
 0243        if (_securePrivateKeyPtr == IntPtr.Zero)
 0244            throw new InvalidOperationException("Secure key is not initialized.");
 245
 0246        var privateKey = new byte[_privateKeyLength];
 0247        Marshal.Copy(_securePrivateKeyPtr, privateKey, 0, (int)_privateKeyLength);
 248
 0249        return privateKey;
 250    }
 251
 252    public void Dispose()
 253    {
 0254        ReleaseUnmanagedResources();
 0255        GC.SuppressFinalize(this);
 0256    }
 257
 258    ~SecureKeyManager()
 259    {
 0260        ReleaseUnmanagedResources();
 0261    }
 262}