< Summary - Combined Code Coverage

Information
Class: NLightning.Infrastructure.Protocol.Services.SecretStorageService
Assembly: NLightning.Infrastructure
File(s): /home/runner/work/nlightning/nlightning/src/NLightning.Infrastructure/Protocol/Services/SecretStorageService.cs
Tag: 30_15166811759
Line coverage
92%
Covered lines: 61
Uncovered lines: 5
Coverable lines: 66
Total lines: 197
Line coverage: 92.4%
Branch coverage
90%
Covered branches: 29
Total branches: 32
Branch coverage: 90.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
InsertSecret(...)90%10.011095.45%
DeriveOldSecret(...)100%66100%
GetBucketIndex(...)75%4.25475%
DeriveSecret(...)100%44100%
FreeSecret(...)50%2.02283.33%
ReleaseUnmanagedResources()100%44100%
Dispose(...)100%22100%
Dispose()100%11100%
Finalize()100%210%

File(s)

/home/runner/work/nlightning/nlightning/src/NLightning.Infrastructure/Protocol/Services/SecretStorageService.cs

#LineLine coverage
 1using System.Runtime.InteropServices;
 2using System.Security.Cryptography;
 3
 4namespace NLightning.Infrastructure.Protocol.Services;
 5
 6using Crypto.Factories;
 7using Crypto.Hashes;
 8using Crypto.Interfaces;
 9using Domain.Protocol.Services;
 10using Models;
 11
 12/// <summary>
 13/// Provides efficient storage of per-commitment secrets
 14/// </summary>
 15public class SecretStorageService : ISecretStorageService
 16{
 17    public const int SECRET_SIZE = 32;
 18
 4819    private readonly StoredSecret?[] _knownSecrets = new StoredSecret?[49];
 4820    private readonly ICryptoProvider _cryptoProvider = CryptoFactory.GetCryptoProvider();
 21
 22    /// <summary>
 23    /// Inserts a new secret and verifies it against existing secrets
 24    /// </summary>
 25    public bool InsertSecret(ReadOnlySpan<byte> secret, ulong index)
 26    {
 43227        if (secret is not { Length: SECRET_SIZE })
 028            throw new ArgumentException($"Secret must be {SECRET_SIZE} bytes", nameof(secret));
 29
 30        // Find bucket for this secret
 43231        var bucket = GetBucketIndex(index);
 32
 43233        var storedSecret = new byte[SECRET_SIZE];
 43234        var derivedSecret = new byte[SECRET_SIZE];
 35        // Verify this secret can derive all previously known secrets
 1016036        for (var b = 0; b < bucket; b++)
 37        {
 468038            if (_knownSecrets[b] == null)
 39                continue;
 40
 448841            DeriveSecret(secret, bucket, _knownSecrets[b]!.Index, derivedSecret);
 42
 43            // Compare with stored secret (copied from secure memory)
 448844            Marshal.Copy(_knownSecrets[b]!.SecretPtr, storedSecret, 0, SECRET_SIZE);
 45
 448846            if (!CryptographicOperations.FixedTimeEquals(derivedSecret, storedSecret))
 47            {
 48                // Securely wipe the temporary copy
 3249                _cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(storedSecret, 0), SECRET_SIZE);
 3250                _cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(derivedSecret, 0), SECRET_SIZE);
 3251                return false; // Secret verification failed
 52            }
 53
 54            // Securely wipe the temporary copies
 445655            _cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(storedSecret, 0), SECRET_SIZE);
 445656            _cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(derivedSecret, 0), SECRET_SIZE);
 57        }
 58
 40059        if (_knownSecrets[bucket] != null)
 60        {
 61            // Free previous secret in this bucket if it exists
 10462            FreeSecret(_knownSecrets[bucket]!.SecretPtr);
 63        }
 64
 65        // Allocate secure memory for the new secret
 40066        var securePtr = _cryptoProvider.MemoryAlloc(SECRET_SIZE);
 67
 68        // Lock memory to prevent swapping
 40069        _cryptoProvider.MemoryLock(securePtr, SECRET_SIZE);
 70
 71        // Copy secret to secure memory
 40072        Marshal.Copy(secret.ToArray(), 0, securePtr, SECRET_SIZE);
 73
 74        // Store in the appropriate bucket
 40075        _knownSecrets[bucket] = new StoredSecret(index, securePtr);
 76
 40077        return true;
 78    }
 79
 80    /// <summary>
 81    /// Derives an old secret from a known higher-level secret
 82    /// </summary>
 83    public void DeriveOldSecret(ulong index, Span<byte> derivedSecret)
 84    {
 85        // Try to find a base secret that can derive this one
 112086        for (var b = 0; b < _knownSecrets.Length; b++)
 87        {
 55688            if (_knownSecrets[b] == null)
 89                continue;
 90
 91            // Check if this secret can derive the requested index
 28892            var mask = ~((1UL << b) - 1);
 28893            if ((index & mask) != (_knownSecrets[b]!.Index & mask))
 94            {
 95                continue;
 96            }
 97
 98            // Found a base secret that can derive the requested one
 8099            var baseSecret = new byte[SECRET_SIZE];
 80100            Marshal.Copy(_knownSecrets[b]!.SecretPtr, baseSecret, 0, SECRET_SIZE);
 101
 80102            DeriveSecret(baseSecret, b, index, derivedSecret);
 103
 104            // Securely wipe the temporary base secret
 80105            _cryptoProvider.MemoryZero(Marshal.UnsafeAddrOfPinnedArrayElement(baseSecret, 0), SECRET_SIZE);
 106
 80107            return; // Success
 108        }
 109
 4110        throw new InvalidOperationException($"Cannot derive secret for index {index}");
 111    }
 112
 113    private static int GetBucketIndex(ulong index)
 114    {
 10272115        for (var b = 0; b < 48; b++)
 116        {
 5136117            if (((index >> b) & 1) == 1)
 118            {
 432119                return b;
 120            }
 121        }
 0122        return 48; // For index 0 (seed)
 123    }
 124
 125    private static void DeriveSecret(ReadOnlySpan<byte> baseSecret, int bits, ulong index, Span<byte> derivedSecret)
 126    {
 4568127        using var sha256 = new Sha256();
 128
 4568129        baseSecret.CopyTo(derivedSecret);
 130
 287040131        for (var b = bits - 1; b >= 0; b--)
 132        {
 138952133            if (((index >> b) & 1) == 0)
 134            {
 135                continue;
 136            }
 137
 69468138            derivedSecret[b / 8] ^= (byte)(1 << (b % 8));
 139
 69468140            sha256.AppendData(derivedSecret);
 69468141            sha256.GetHashAndReset(derivedSecret);
 142        }
 9136143    }
 144
 145    /// <summary>
 146    /// Securely frees a secret from memory
 147    /// </summary>
 148    private void FreeSecret(IntPtr secretPtr)
 149    {
 400150        if (secretPtr == IntPtr.Zero)
 0151            return;
 152
 153        // Wipe memory before freeing
 400154        _cryptoProvider.MemoryZero(secretPtr, SECRET_SIZE);
 155
 156        // Unlock memory
 400157        _cryptoProvider.MemoryUnlock(secretPtr, SECRET_SIZE);
 158
 159        // Free memory
 400160        _cryptoProvider.MemoryFree(secretPtr);
 400161    }
 162
 163    private void ReleaseUnmanagedResources()
 164    {
 165        // Free all secrets
 4800166        for (var i = 0; i < _knownSecrets.Length; i++)
 167        {
 2352168            if (_knownSecrets[i] == null)
 169            {
 170                continue;
 171            }
 172
 296173            FreeSecret(_knownSecrets[i]!.SecretPtr);
 296174            _knownSecrets[i] = null;
 175        }
 48176    }
 177
 178    private void Dispose(bool disposing)
 179    {
 48180        ReleaseUnmanagedResources();
 48181        if (disposing)
 182        {
 48183            _cryptoProvider.Dispose();
 184        }
 48185    }
 186
 187    public void Dispose()
 188    {
 48189        Dispose(true);
 48190        GC.SuppressFinalize(this);
 48191    }
 192
 193    ~SecretStorageService()
 194    {
 0195        Dispose(false);
 0196    }
 197}