SecretSharingDotNet 1.1.0

dotnet add package SecretSharingDotNet --version 1.1.0
                    
NuGet\Install-Package SecretSharingDotNet -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SecretSharingDotNet" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SecretSharingDotNet" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="SecretSharingDotNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SecretSharingDotNet --version 1.1.0
                    
#r "nuget: SecretSharingDotNet, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SecretSharingDotNet@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SecretSharingDotNet&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=SecretSharingDotNet&version=1.1.0
                    
Install as a Cake Tool

Setup

Install SecretSharingDotNet package ๐Ÿ“ฅ

  1. Open a console and switch to the directory containing your project file.

  2. Use the following command to install version 1.1.0 of the SecretSharingDotNet package:

    dotnet add package SecretSharingDotNet -v 1.1.0 -f <FRAMEWORK>
    
  3. After the completion of the command, look at the project file to make sure that the package is successfully installed.

    You can open the .csproj file to see the added package reference:

    <ItemGroup>
      <PackageReference Include="SecretSharingDotNet" Version="1.1.0" />
    </ItemGroup>
    

Remove SecretSharingDotNet package ๐Ÿ“ค

  1. Open a console and switch to the directory containing your project file.

  2. Use the following command to remove the SecretSharingDotNet package:

    dotnet remove package SecretSharingDotNet
    
  3. After the completion of the command, look at the project file to make sure that the package is successfully removed.

    You can open the .csproj file to check the deleted package reference.

Usage ๐Ÿ”ง

Breaking changes in v1.1.0.

  • Shares<TNumber> is no longer marked [Serializable]. With a formatter's defaults the type could not be serialized before either, so the change is invisible to consumers who never registered a surrogate. It bites in one case: if you registered an ISerializationSurrogate for Share<TNumber>, a populated Shares<TNumber> used to round-trip through BinaryFormatter and the formatter now rejects the root type. Register a surrogate for Shares<TNumber> as well, or persist through ToCharArray(bool, bool, ShareFormat) โ€” the format this library defines and validates on the way back in.

Breaking changes in v1.0.1 (major version bump from v0.14.0). Highlights for migrating consumers โ€” see CHANGELOG.md for the full list.

  • Text I/O is pinned-buffer-only. The string-based entry points are gone. Wrap a string in .ToPinnedSecure() and use the pinned factories: Secret<TNumber>.FromText(PinnedPoolArray<char>), new Share<TNumber>(PinnedPoolArray<char>), Shares<TNumber>.FromText(...), Shares<TNumber>.FromTextLines(...). Read back via the matching ToCharArray() methods on Secret, Share, and Shares.
  • Reconstruction(string) and Reconstruction(string[]) removed. Build a Shares<TNumber> through one of the pinned factories above and pass that.
  • Secret<TNumber>.ToBase64() โ†’ ToBase64String() (string, redaction-gated) and ToBase64CharArray() (pinned char buffer, not redacted).
  • Share<TNumber>.X / .Y properties โ†’ .Index / .Value.
  • MakeShares parameter types. numberOfMinimumShares and numberOfShares are now int instead of TNumber.
  • Calculator.ToInt32() / BigIntCalculator.ToInt32() removed.
  • SecretReconstructor<TNumber>.SecurityLevel is now read-only โ€” it is derived from the supplied shares on every Reconstruction call (see Basics).
  • Calculator<TNumber>.Clone is now abstract โ€” custom backends must override it.
  • ExtendedEuclideanAlgorithm<TNumber> is now sealed โ€” derive from IExtendedGcdAlgorithm<TNumber> or compose around it instead.
  • SecretSplitter<TNumber> is now sealed โ€” compose around IMakeSharesUseCase<TNumber> instead of deriving.
  • SecureBigInteger.Pow(int) throws ArgumentOutOfRangeException (was ArgumentException) for negative exponents, consistent with the other length / exponent validations on the type. ArgumentOutOfRangeException derives from ArgumentException, so wide catches still work.
  • Shares<TNumber>.OriginalSecret and Shares<TNumber>.OriginalSecretExists removed (deprecated since v0.14.0).
  • Platform support. The minimum supported .NET Framework is now 4.7.2; net4.7 and net4.7.1 are removed.
  • FinitePoint<TNumber> is now internal (was public).

The text-secret encoding is UTF-8 (introduced in v0.14.0).

A runnable end-to-end example lives in samples/SecretSharingDotNet.Demo.Console. It wires the SecureBigInteger backend via FixedIterationSecretReconstructor (fixed-iteration MersenneSafeGcdAlgorithm inverse) through Microsoft.Extensions.DependencyInjection, reads the secret via ConsolePasswordReader (no string materialisation), splits it, and reconstructs it from a user-selected K-of-N subset of the generated shares.

Basics

Use the function MakeShares to generate the shares, based on a random or pre-defined secret. Afterwards, use the function Reconstruction to reconstruct the original secret.

The library is generic in the numeric backend: BigInteger (used in the examples below) or SecureBigInteger (pinned-memory storage with constant-time arithmetic โ€” see the Security & Threat Model section). Both IMakeSharesUseCase<TNumber> and IReconstructionUseCase<TNumber> implement IDisposable; wrap them in using so pinned, security-sensitive buffers are released deterministically.

Choosing a backend and reconstructor

Backend Reconstructor + GCD strategy Pinned memory Constant-time arithmetic โ€  Fixed-iteration inverse โ€  Choose when
BigInteger SecretReconstructor + ExtendedEuclideanAlgorithm no no no performance matters and timing side channels are out of scope
SecureBigInteger FixedIterationSecretReconstructor (wires MersenneSafeGcdAlgorithm) yes partial โ€  yes secrets where passive timing analysis is in scope

โ€  Both constant-time properties are best-effort against passive timing analysis โ€” managed .NET cannot guarantee constant time (see the Security & Threat Model section). Constant-time arithmetic is partial: the per-limb work of the core primitives is fixed by the operand limb counts and branchless โ€” by max(l, r) for subtraction, max(l, r) + 1 for addition, left ร— right for multiplication and squaring, and dividend ร— 64 outer iterations for division and remainder โ€” but every result is normalised by a leading-zero trim whose iteration count depends on the result's magnitude, and that trimmed length then sizes the next operation โ€” so the limb count of an intermediate value is itself secret-derived rather than public. It covers the core primitives (add / subtract / multiply / square / divide / remainder) on the public bit length; it does not cover Pow (variable-time on its exponent), ordering (CompareTo), or the hex / Base64 decoders. Fixed-iteration inverse means that at a fixed security level the GCD iteration count depends only on that (public) level, not on the secret operand values โ€” removing the iteration-count side channel of a plain extended-Euclidean GCD. Reconstruction runs at the level the shares record or the caller names, and derives one from the shares' maximum value only for shares recording neither, so the iteration count still reflects that level โ€” which is public either way, recorded in the share or revealed by its size; it is not per-operation constant-time (MersenneSafeGcdAlgorithm's per-iteration timing is not uniform). Together these two properties reduce but do not eliminate reconstruction timing side channels โ€” they do not make the whole reconstruction constant-time.

Do not pair SecureBigInteger with ExtendedEuclideanAlgorithm: you keep the pinned memory but reintroduce an operand-value-dependent iteration count โ€” the side channel FixedIterationSecretReconstructor removes. FixedIterationSecretReconstructor<SecureBigInteger> accepts only fixed-iteration GCD strategies, so this mispairing is a compile-time error rather than a silent side channel. See the Security & Threat Model section for the exact scope.

The length of the shares is based on the security level. On the splitter side, you can set it three ways: pass it as the securityLevel parameter of the MakeShares overload that takes one, assign the SecretSplitter<TNumber>.SecurityLevel property, or inject a pre-configured ISecurityLevelManager<TNumber> through the constructor. The configured level is overridden when the secret size exceeds the Mersenne prime derived from it โ€” the library auto-adjusts upward. It is not necessary to define a security level for a reconstruction: SecretReconstructor<TNumber>.SecurityLevel is read-only and is derived from the supplied shares on every Reconstruction call.

The ToString() overrides on Secret<TNumber>, Share<TNumber>, and Shares<TNumber> are build-mode-sensitive: DEBUG builds return the actual content, while Release builds return the redaction sentinel "*** Secured Value ***" so secrets cannot accidentally leak through logs, exception messages, or other diagnostic output.

Using the SecretSharingDotNet library with DI in a .NET project

This guide demonstrates two parallel DI wirings: the BigInteger backend with ExtendedEuclideanAlgorithm (BCL-native, variable-time arithmetic) and the SecureBigInteger backend via FixedIterationSecretReconstructor, which wires the constant-time MersenneSafeGcdAlgorithm (pinned-memory storage, constant-time arithmetic on public bit-length โ€” see the Security & Threat Model section for the trade-offs). Both variants share the same import block:

using Microsoft.Extensions.DependencyInjection;
using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;
using SecretSharingDotNet.Math.Numerics;
using System;
using System.Linq;
using System.Numerics;

The output sites below use the pinned ToCharArray() accessors on Shares<TNumber> and Secret<TNumber> and write them directly via Console.Out.Write(char[], int, int). These accessors are not redaction-gated โ€” they return the actual content in both DEBUG and Release builds โ€” and avoid materialising a managed string between the pinned buffer and the writer.

The Splitter and Reconstructor hold a mutable ISecurityLevelManager that each MakeShares / Reconstruction call reads-then-writes; concurrent invocations on the same instance race on the security-level field and may return wrong results. The registrations below therefore use AddTransient for the use-case interfaces (each call resolves its own instance) and AddSingleton only for the stateless GCD algorithm.

Reconstruction(shares, securityLevel) โ€” the overload that names the finite field instead of taking it from the shares โ€” lives on IReconstructionWithSecurityLevelUseCase<TNumber>, which inherits IReconstructionUseCase<TNumber>. Interface inheritance does not create a second registration: registering only IReconstructionUseCase<TNumber> leaves the derived interface unresolvable. Consumers who need to name the field register it as well, alongside the existing line and pointing at the same implementation:

serviceCollection.AddTransient<IReconstructionWithSecurityLevelUseCase<BigInteger>, SecretReconstructor<BigInteger>>();

You need this only for shares that carry no record of their field โ€” anything split before the level became part of the share, or built from raw coordinates. Shares that do carry a level are reconstructed correctly through the existing interface without any change.

Variant 1 โ€” BigInteger + ExtendedEuclideanAlgorithm

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IExtendedGcdAlgorithm<BigInteger>, ExtendedEuclideanAlgorithm<BigInteger>>();
serviceCollection.AddTransient<IMakeSharesUseCase<BigInteger>, SecretSplitter<BigInteger>>();
serviceCollection.AddTransient<IReconstructionUseCase<BigInteger>, SecretReconstructor<BigInteger>>();
using var serviceProvider = serviceCollection.BuildServiceProvider();

var makeSharesUseCase = serviceProvider.GetRequiredService<IMakeSharesUseCase<BigInteger>>();
var reconstructionUseCase = serviceProvider.GetRequiredService<IReconstructionUseCase<BigInteger>>();

// Build the secret from a pinned UTF-8 char buffer; the plaintext never lives
// in an unpinned managed string past the call site.
using var pinned = "Hello!".ToPinnedSecure();
using var secret = Secret<BigInteger>.FromText(pinned);
using var shares = makeSharesUseCase.MakeShares(3, 7, secret);

using var sharesChars = shares.ToCharArray();
Console.Out.Write(sharesChars.PoolArray, 0, sharesChars.Length);

using var reconstruction = reconstructionUseCase.Reconstruction(shares.Where(p => p.IsIndexEven).ToArray());
using var reconstructionChars = reconstruction.ToCharArray();
Console.Out.Write(reconstructionChars.PoolArray, 0, reconstructionChars.Length);
Console.WriteLine();

Variant 2 โ€” SecureBigInteger + FixedIterationSecretReconstructor

FixedIterationSecretReconstructor<SecureBigInteger> is a drop-in IReconstructionUseCase<SecureBigInteger> that supplies the fixed-iteration MersenneSafeGcdAlgorithm itself, so no separate GCD registration is needed. To use a different fixed-iteration strategy, additionally register IFixedIterationExtendedGcdAlgorithm<SecureBigInteger> and the reconstructor picks it up (see the Security & Threat Model section).

var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IMakeSharesUseCase<SecureBigInteger>, SecretSplitter<SecureBigInteger>>();
serviceCollection.AddTransient<IReconstructionUseCase<SecureBigInteger>, FixedIterationSecretReconstructor<SecureBigInteger>>();
using var serviceProvider = serviceCollection.BuildServiceProvider();

var makeSharesUseCase = serviceProvider.GetRequiredService<IMakeSharesUseCase<SecureBigInteger>>();
var reconstructionUseCase = serviceProvider.GetRequiredService<IReconstructionUseCase<SecureBigInteger>>();

using var pinned = "Hello!".ToPinnedSecure();
using var secret = Secret<SecureBigInteger>.FromText(pinned);
using var shares = makeSharesUseCase.MakeShares(3, 7, secret);

using var sharesChars = shares.ToCharArray();
Console.Out.Write(sharesChars.PoolArray, 0, sharesChars.Length);

using var reconstruction = reconstructionUseCase.Reconstruction(shares.Where(p => p.IsIndexEven).ToArray());
using var reconstructionChars = reconstruction.ToCharArray();
Console.Out.Write(reconstructionChars.PoolArray, 0, reconstructionChars.Length);
Console.WriteLine();

Both use-case interfaces extend IDisposable and โ€” under the SecureBigInteger backend โ€” own pinned, security-sensitive buffers. Note that the use-case instances are not wrapped in using after GetRequiredService: the container owns the lifetime of services it resolves and disposes every tracked IDisposable transient when the ServiceProvider itself is disposed (here via using var). Disposing them explicitly at the call site would double-dispose against the container's cascade. In long-running hosts (ASP.NET, worker services) the root provider lives for the application's lifetime, so tracked transients accumulate until process exit โ€” wrap each unit of work in a using var scope = serviceProvider.CreateScope(); block (or register the use-cases as AddScoped) so the container disposes the resolved instances at scope end. The results of the calls (secret, shares, reconstruction, and the ToCharArray outputs) are caller-owned and remain wrapped in using.

Random secret ๐ŸŽฒ

Create a random secret in conjunction with the generation of shares. The length of the generated shares and of the secret are based on the security level. Here is an example with a pre-defined security level of 127 using the BigInteger backend; for the SecureBigInteger + MersenneSafeGcdAlgorithm wiring, see the Security & Threat Model section.

using System;
using System.Linq;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;

namespace Example1;

public class Program
{
  public static void Main(string[] args)
  {
    //// Create Shamir's Secret Sharing instance with BigInteger
    using var splitter = new SecretSplitter<BigInteger>();

    //// Minimum number of shared secrets for reconstruction: 3
    //// Maximum number of shared secrets: 7
    //// Security level: 127 (Mersenne prime exponent)
    using var shares = splitter.MakeShares(3, 7, 127, out var secret);
    using (secret)
    {
      //// Secret as big integer number
      Console.WriteLine((BigInteger)secret);

      //// Secret as base64 (pinned char buffer, not redaction-gated)
      using var secretBase64 = secret.ToBase64CharArray();
      Console.Out.Write(secretBase64.PoolArray, 0, secretBase64.Length);
      Console.WriteLine();

      var gcd = new ExtendedEuclideanAlgorithm<BigInteger>();
      //// The 'shares' instance contains the shared secrets
      using var combiner = new SecretReconstructor<BigInteger>(gcd);
      using var recoveredSecret1 = combiner.Reconstruction(shares.Where(p => p.IsIndexEven).ToArray());
      using var recoveredSecret2 = combiner.Reconstruction(shares.Where(p => p.IsIndexOdd).ToArray());

      //// All shares serialised as hex lines (pinned char buffer, not redaction-gated)
      using var sharesChars = shares.ToCharArray();
      Console.Out.Write(sharesChars.PoolArray, 0, sharesChars.Length);

      //// 1st recovered secret as big integer number
      Console.WriteLine((BigInteger)recoveredSecret1);

      //// 2nd recovered secret as big integer number
      Console.WriteLine((BigInteger)recoveredSecret2);

      //// 1st recovered secret as base64 (pinned char buffer, not redaction-gated)
      using var recovered1Base64 = recoveredSecret1.ToBase64CharArray();
      Console.Out.Write(recovered1Base64.PoolArray, 0, recovered1Base64.Length);
      Console.WriteLine();

      //// 2nd recovered secret as base64 (pinned char buffer, not redaction-gated)
      using var recovered2Base64 = recoveredSecret2.ToBase64CharArray();
      Console.Out.Write(recovered2Base64.PoolArray, 0, recovered2Base64.Length);
      Console.WriteLine();
    }
  }
}

Pre-defined secret: text ๐Ÿ“„

Use a text as secret, which can be divided into shares. The length of the generated shares is based on the security level. Here is an example with auto-detected security level using the BigInteger backend; for the SecureBigInteger + MersenneSafeGcdAlgorithm wiring, see the Security & Threat Model section.

using System;
using System.Linq;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;

namespace Example2;

public class Program
{
  public static void Main(string[] args)
  {
    //// Create Shamir's Secret Sharing instance with BigInteger
    using var splitter = new SecretSplitter<BigInteger>();

    string password = "Hello World!!";
    //// Minimum number of shared secrets for reconstruction: 3
    //// Maximum number of shared secrets: 7
    //// Attention: The password length can change the security level set by passing
    //// it to MakeShares or assigning the SecurityLevel property.
    using var pinned = password.ToPinnedSecure();
    using var secret = Secret<BigInteger>.FromText(pinned);
    using var shares = splitter.MakeShares(3, 7, secret);

    var gcd = new ExtendedEuclideanAlgorithm<BigInteger>();
    //// The 'shares' instance contains the shared secrets
    using var combiner = new SecretReconstructor<BigInteger>(gcd);
    using var recoveredSecret1 = combiner.Reconstruction(shares.Where(p => p.IsIndexEven).ToArray());
    using var recoveredSecret2 = combiner.Reconstruction(shares.Where(p => p.IsIndexOdd).ToArray());

    //// All shares serialised as hex lines (pinned char buffer, not redaction-gated)
    using var sharesChars = shares.ToCharArray();
    Console.Out.Write(sharesChars.PoolArray, 0, sharesChars.Length);

    //// 1st recovered secret as UTF-8 text (pinned char buffer, not redaction-gated)
    using var recovered1Chars = recoveredSecret1.ToCharArray();
    Console.Out.Write(recovered1Chars.PoolArray, 0, recovered1Chars.Length);
    Console.WriteLine();

    //// 2nd recovered secret as UTF-8 text (pinned char buffer, not redaction-gated)
    using var recovered2Chars = recoveredSecret2.ToCharArray();
    Console.Out.Write(recovered2Chars.PoolArray, 0, recovered2Chars.Length);
    Console.WriteLine();
  }
}

Pre-defined secret: number ๐Ÿ”ข

Use an integer number as secret, which can be divided into shares. The length of the generated shares is based on the security level. Here is an example with a pre-defined security level of 521 using the BigInteger backend; for the SecureBigInteger + MersenneSafeGcdAlgorithm wiring, see the Security & Threat Model section.

using System;
using System.Linq;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;

namespace Example3;

public class Program
{
  public static void Main(string[] args)
  {
    //// Create Shamir's Secret Sharing instance with BigInteger
    using var splitter = new SecretSplitter<BigInteger>();

    BigInteger number = 20000;
    //// Minimum number of shared secrets for reconstruction: 3
    //// Maximum number of shared secrets: 7
    //// Security level: 521 (Mersenne prime exponent)
    //// Attention: The size of the number can change the security level set by passing
    //// it to MakeShares or assigning the SecurityLevel property.
    using var secret = (Secret<BigInteger>)number;
    using var shares = splitter.MakeShares(3, 7, secret, 521);

    var gcd = new ExtendedEuclideanAlgorithm<BigInteger>();
    //// The 'shares' instance contains the shared secrets
    using var combiner = new SecretReconstructor<BigInteger>(gcd);
    using var recoveredSecret1 = combiner.Reconstruction(shares.Where(p => p.IsIndexEven).ToArray());
    using var recoveredSecret2 = combiner.Reconstruction(shares.Where(p => p.IsIndexOdd).ToArray());

    //// All shares serialised as hex lines (pinned char buffer, not redaction-gated)
    using var sharesChars = shares.ToCharArray();
    Console.Out.Write(sharesChars.PoolArray, 0, sharesChars.Length);

    //// 1st recovered secret as big integer number
    Console.WriteLine((BigInteger)recoveredSecret1);

    //// 2nd recovered secret as big integer number
    Console.WriteLine((BigInteger)recoveredSecret2);
  }
}

Pre-defined secret: byte array โ–ฆ

Use a byte array as secret, which can be divided into shares. The length of the generated shares is based on the security level. Here is an example with auto-detected security level using the BigInteger backend; for the SecureBigInteger + MersenneSafeGcdAlgorithm wiring, see the Security & Threat Model section.

using System;
using System.Linq;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;

namespace Example4;

public class Program
{
  public static void Main(string[] args)
  {
    //// Create Shamir's Secret Sharing instance with BigInteger
    using var splitter = new SecretSplitter<BigInteger>();

    byte[] bytes = { 0x1D, 0x2E, 0x3F };
    //// Minimum number of shared secrets for reconstruction: 4
    //// Maximum number of shared secrets: 10
    //// Attention: The byte array size can change the security level set by passing
    //// it to MakeShares or assigning the SecurityLevel property.
    using var secret = (Secret<BigInteger>)bytes;
    using var shares = splitter.MakeShares(4, 10, secret);

    var gcd = new ExtendedEuclideanAlgorithm<BigInteger>();
    //// The 'shares' instance contains the shared secrets
    using var combiner = new SecretReconstructor<BigInteger>(gcd);
    using var recovered = combiner.Reconstruction(shares.Where(p => p.IsIndexEven).ToArray());
    using var recoveredBytes = recovered.ToByteArray();

    //// All shares serialised as hex lines (pinned char buffer, not redaction-gated)
    using var sharesChars = shares.ToCharArray();
    Console.Out.Write(sharesChars.PoolArray, 0, sharesChars.Length);

    //// The secret bytes
    Console.WriteLine($"{recoveredBytes[0]:X2}, {recoveredBytes[1]:X2}, {recoveredBytes[2]:X2}");
  }
}

Secret ๐Ÿ”

The Secret<TNumber> type is the unified envelope for the secrets that flow through SecretSplitter and SecretReconstructor. The four sub-examples below cover every public construction, inspection, and serialisation path on the BigInteger backend, plus a fixed-iteration round-trip on the SecureBigInteger backend. Sub-examples 5aโ€“5c share the same payload โ€” UTF-8 text "Hello World!!" โ€” so each path can be traced end-to-end with the same expected output.

Secret<T> โ€” construct

using System;
using System.Numerics;
using System.Text;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.SecureMemory;
using SecretSharingDotNet.Cryptography.SecureInput;

namespace Example5a;

public class Program
{
  public static void Main(string[] args)
  {
    //// The shared payload across 5aโ€“5c.
    byte[] payload = Encoding.UTF8.GetBytes("Hello World!!");

    //// (a) From pinned UTF-8 characters via FromText.
    using var pinnedText = "Hello World!!".ToPinnedSecure();
    using var secretFromText = Secret<BigInteger>.FromText(pinnedText);

    //// (b) From pinned standard-Base64 characters via FromBase64 (RFC 4648 ยง4).
    using var pinnedBase64 = "SGVsbG8gV29ybGQhIQ==".ToPinnedSecure();
    using var secretFromBase64 = Secret<BigInteger>.FromBase64(pinnedBase64);

    //// (c) From byte sources โ€” three equivalent paths.
    using var secretFromBytesCtor = new Secret<BigInteger>(payload);
    using var secretFromBytesImplicit = (Secret<BigInteger>)payload;
    using var pinnedBytes = new PinnedPoolArray<byte>(payload.Length);
    Array.Copy(payload, pinnedBytes.PoolArray, payload.Length);
    using var secretFromPinnedBytes = (Secret<BigInteger>)pinnedBytes;

    //// (d) From a numeric TNumber via implicit operator. The BigInteger is
    //// built from the same bytes in little-endian order โ€” the representation
    //// that round-trips through Secret<BigInteger>.
    BigInteger asNumber = new BigInteger(payload);
    using var secretFromNumber = (Secret<BigInteger>)asNumber;

    //// All five secrets carry the same payload. Equality is constant-time.
    Console.WriteLine($"text  == bytes  : {secretFromText == secretFromBytesCtor}");
    Console.WriteLine($"text  == base64 : {secretFromText == secretFromBase64}");
    Console.WriteLine($"text  == pinned : {secretFromText == secretFromPinnedBytes}");
    Console.WriteLine($"text  == number : {secretFromText == secretFromNumber}");
  }
}

Secret<T> โ€” inspect, compare

using System;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;

namespace Example5b;

public class Program
{
  public static void Main(string[] args)
  {
    using var pinnedA = "Hello World!!".ToPinnedSecure();
    using var pinnedB = "Hello World!!".ToPinnedSecure();
    using var pinnedC = "GoodBye World".ToPinnedSecure();
    using var a = Secret<BigInteger>.FromText(pinnedA);
    using var b = Secret<BigInteger>.FromText(pinnedB);
    using var c = Secret<BigInteger>.FromText(pinnedC);

    //// Equality is a fixed-time byte comparison โ€” no early exit on the
    //// first mismatch.
    Console.WriteLine($"a == b       : {a == b}");
    Console.WriteLine($"a != c       : {a != c}");
    Console.WriteLine($"a.Equals(b)  : {a.Equals(b)}");

    //// Ordering operators are defined for equal-length secrets; CompareTo
    //// returns the same three-way result as on any IComparable<T>.
    Console.WriteLine($"c < a              : {c < a}");
    Console.WriteLine($"a.CompareTo(b) = 0 : {a.CompareTo(b) == 0}");

    //// Implicit cast back to the numeric type. With BigInteger the bytes
    //// are interpreted little-endian โ€” the same convention used on the
    //// wire and in Share<T>.
    BigInteger asNumber = a;
    Console.WriteLine($"a as BigInteger : {asNumber}");

    //// ToString() is build-mode-sensitive: DEBUG builds return the UTF-8
    //// decoded text, Release builds return the redaction sentinel
    //// "*** Secured Value ***" so secrets cannot leak through logs,
    //// exception messages, or debugger displays. Use ToCharArray() or
    //// ToByteArray() to read the actual content โ€” neither is gated by
    //// the redaction sentinel.
    Console.WriteLine($"a.ToString()    : {a}");
  }
}

Secret<T> โ€” serialise

using System;
using System.Numerics;
using System.Text;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;

namespace Example5c;

public class Program
{
  public static void Main(string[] args)
  {
    using var pinnedText = "Hello World!!".ToPinnedSecure();
    using var secret = Secret<BigInteger>.FromText(pinnedText);

    //// (a) Raw bytes โ€” pinned, not redaction-gated.
    using var bytes = secret.ToByteArray();
    Console.WriteLine($"bytes length        : {bytes.Length}");

    //// (b) UTF-8 characters โ€” pinned, not redaction-gated.
    using var chars = secret.ToCharArray();
    Console.Out.Write(chars.PoolArray, 0, chars.Length);
    Console.WriteLine();

    //// (c) Custom encoding โ€” same payload, different bytes-on-the-wire.
    using var utf16Chars = secret.ToCharArray(Encoding.Unicode);
    Console.WriteLine($"utf-16 chars length : {utf16Chars.Length}");

    //// (d) Base64 as a pinned char buffer โ€” pinned, not redaction-gated.
    using var base64Chars = secret.ToBase64CharArray();
    Console.Out.Write(base64Chars.PoolArray, 0, base64Chars.Length);
    Console.WriteLine();

    //// (e) Base64 as a string โ€” convenient for interop APIs that take
    //// string. Note: the returned string lives on the managed heap and is
    //// NOT pinned or securely cleared; treat it as ephemeral and minimise
    //// its scope.
    string base64 = secret.ToBase64String();
    Console.WriteLine($"base64 string       : {base64}");
  }
}

Secret<SecureBigInteger> โ€” text, number, bytes + fixed-iteration round-trip

using System;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;
using SecretSharingDotNet.Math.Numerics;

namespace Example5d;

public class Program
{
  public static void Main(string[] args)
  {
    //// Three Secret<SecureBigInteger> instances โ€” one per input shape.
    //// Each one is split through SecretSplitter<SecureBigInteger> and
    //// reconstructed via the fixed-iteration MersenneSafeGcdAlgorithm path.

    //// (a) Text โ€” pinned UTF-8 chars in, Secret<SecureBigInteger> out.
    using var pinnedText = "Hello World!!".ToPinnedSecure();
    using var textSecret = Secret<SecureBigInteger>.FromText(pinnedText);

    //// (b) Number โ€” implicit cast from SecureBigInteger.
    using var numberSecret = (Secret<SecureBigInteger>)(SecureBigInteger)20000;

    //// (c) Bytes โ€” implicit cast from byte[].
    byte[] payload = { 0x1D, 0x2E, 0x3F };
    using var bytesSecret = (Secret<SecureBigInteger>)payload;

    //// SecretSplitter and SecretReconstructor auto-adjust their security
    //// level to the secret being processed, so one pair of instances
    //// serves all three shapes. FixedIterationSecretReconstructor wires the
    //// fixed-iteration MersenneSafeGcdAlgorithm modular inverse used during
    //// Lagrange interpolation.
    using var splitter = new SecretSplitter<SecureBigInteger>();
    using var combiner = new FixedIterationSecretReconstructor<SecureBigInteger>();

    //// (a) Text round-trip.
    using var textShares = splitter.MakeShares(3, 7, textSecret);
    using var recoveredText = combiner.Reconstruction(textShares);
    using var recoveredChars = recoveredText.ToCharArray();
    Console.Out.Write(recoveredChars.PoolArray, 0, recoveredChars.Length);
    Console.WriteLine();

    //// (b) Number round-trip โ€” explicit security level of 521.
    using var numberShares = splitter.MakeShares(3, 7, numberSecret, 521);
    using var recoveredNumber = combiner.Reconstruction(numberShares);
    //// Both Secret<SecureBigInteger>.ToString() and SecureBigInteger.ToString()
    //// return the redaction sentinel "*** Secured Value ***" in Release builds.
    //// Cast through an unprotected primitive โ€” here, explicit (int) โ€” to read
    //// the actual value. Use this only inside an audited scope where revealing
    //// the value is intentional.
    Console.WriteLine((int)(SecureBigInteger)recoveredNumber);

    //// (c) Bytes round-trip.
    using var bytesShares = splitter.MakeShares(4, 10, bytesSecret);
    using var recoveredBytes = combiner.Reconstruction(bytesShares);
    using var recoveredByteArray = recoveredBytes.ToByteArray();
    Console.WriteLine($"{recoveredByteArray[0]:X2} {recoveredByteArray[1]:X2} {recoveredByteArray[2]:X2}");
  }
}

Shares ๐Ÿ”‘

The library exposes wire-format I/O on two surfaces: individual Share<TNumber> points and Shares<TNumber> collections. The three sub-examples below cover every public construction and serialisation path for both surfaces, plus a round-trip through SecretReconstructor. The example values reconstruct to 52199147989510990914370102003412153.

Single Share<T> โ€” construct, inspect, serialize

using System;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Math;

namespace Example6a;

public class Program
{
  public static void Main(string[] args)
  {
    //// (a) Parse from "INDEX-VALUE" hex text (pinned char buffer). The "0x"
    //// prefix on either coordinate is accepted; the index must be > 0.
    using var pinnedText = "05-CDECB88126DBC04D753E0C2D83D7B55D".ToPinnedSecure();
    using var shareFromText = new Share<BigInteger>(pinnedText);

    //// (b) From raw little-endian byte pairs (index bytes + value bytes).
    byte[] indexBytes = { 0x05 };
    byte[] valueBytes = { 0xCD, 0xEC, 0xB8, 0x81, 0x26, 0xDB, 0xC0, 0x4D,
                          0x75, 0x3E, 0x0C, 0x2D, 0x83, 0xD7, 0xB5, 0x5D };
    using var shareFromBytes = new Share<BigInteger>(indexBytes, valueBytes);

    //// (c) Programmatic โ€” pass Calculator<BigInteger> instances. The Share
    //// takes ownership of both calculators and disposes them in cascade;
    //// do not wrap the calculator locals in 'using' (would double-dispose).
    Calculator<BigInteger> idx = new BigInteger(5);
    Calculator<BigInteger> val = new BigInteger(42);
    using var shareFromCalc = new Share<BigInteger>(idx, val);

    //// Inspect index, value and parity.
    Console.WriteLine($"Index: {(BigInteger)shareFromText.Index}");
    Console.WriteLine($"Value: {(BigInteger)shareFromText.Value}");
    Console.WriteLine($"IsIndexEven: {shareFromText.IsIndexEven}");
    Console.WriteLine($"IsIndexOdd: {shareFromText.IsIndexOdd}");

    //// Serialise back to hex (pinned char buffer, not redaction-gated).
    //// Default: uppercase, no "0x" prefix.
    using var hexDefault = shareFromText.ToCharArray();
    Console.Out.Write(hexDefault.PoolArray, 0, hexDefault.Length);
    Console.WriteLine();

    //// Lowercase with "0x" prefix on each coordinate.
    using var hexLowerPrefixed = shareFromText.ToCharArray(uppercase: false, withPrefix: true);
    Console.Out.Write(hexLowerPrefixed.PoolArray, 0, hexLowerPrefixed.Length);
    Console.WriteLine();
  }
}

Shares<T> collection โ€” construct, serialize

using System;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.SecureMemory;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Math;

namespace Example6b;

public class Program
{
  public static void Main(string[] args)
  {
    //// === Three deserialisation paths ===
    //// (A fourth path is SecretSplitter.MakeShares(...) โ€” see the earlier
    //// examples. The methods below cover deserialisation from wire formats.)

    //// (a) From a Share<T>[] via implicit operator. The Shares<T> adopts the
    //// array and disposes each contained Share in cascade.
    using var p1 = "02-665C74ED38FDFF095B2FC9319A272A75".ToPinnedSecure();
    using var p2 = "05-CDECB88126DBC04D753E0C2D83D7B55D".ToPinnedSecure();
    using var p3 = "07-54A83E34AB0310A7F5D80F2A68FD4F33".ToPinnedSecure();
    using var sharesFromArray = (Shares<BigInteger>)new[]
    {
        new Share<BigInteger>(p1),
        new Share<BigInteger>(p2),
        new Share<BigInteger>(p3),
    };

    //// (b) From a single multi-line text blob via FromText. The pinned buffer
    //// is the caller's; FromText returns a separately-owned Shares<T>.
    using var pinnedBlob = ("02-665C74ED38FDFF095B2FC9319A272A75" + Environment.NewLine +
                           "05-CDECB88126DBC04D753E0C2D83D7B55D" + Environment.NewLine +
                           "07-54A83E34AB0310A7F5D80F2A68FD4F33").ToPinnedSecure();
    using var sharesFromBlob = Shares<BigInteger>.FromText(pinnedBlob);

    //// (c) From per-line pinned buffers via FromTextLines โ€” useful when share
    //// lines arrive separately (e.g., from a UI form).
    using var line1 = "02-665C74ED38FDFF095B2FC9319A272A75".ToPinnedSecure();
    using var line2 = "05-CDECB88126DBC04D753E0C2D83D7B55D".ToPinnedSecure();
    using var line3 = "07-54A83E34AB0310A7F5D80F2A68FD4F33".ToPinnedSecure();
    using var sharesFromLines = Shares<BigInteger>.FromTextLines(new[] { line1, line2, line3 });

    //// === Serialise a Shares<T> back to multi-line text (pinned, not redacted) ===
    //// Default: uppercase, no "0x" prefix.
    using var sharesUpperNoPrefix = sharesFromBlob.ToCharArray();
    Console.Out.Write(sharesUpperNoPrefix.PoolArray, 0, sharesUpperNoPrefix.Length);

    //// Lowercase with "0x" prefix on each coordinate.
    using var sharesLowerPrefixed = sharesFromBlob.ToCharArray(uppercase: false, withPrefix: true);
    Console.Out.Write(sharesLowerPrefixed.PoolArray, 0, sharesLowerPrefixed.Length);

    //// === Implicit conversions on Shares<T> ===
    //// To PinnedPoolArray<char>: allocates a fresh pinned buffer with the
    //// same content as ToCharArray() default; the caller owns the result.
    using PinnedPoolArray<char> asPinnedChars = sharesFromBlob;

    //// To Share<T>[]: a snapshot array whose entries alias the shares owned
    //// by sharesFromBlob โ€” do not dispose the elements individually.
    Share<BigInteger>[] asArray = sharesFromBlob;
    Console.WriteLine($"Share count: {asArray.Length}");
  }
}

Round-trip โ€” reconstruction

using System;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;

namespace Example6c;

public class Program
{
  public static void Main(string[] args)
  {
    //// Build a Shares<BigInteger> from three lines of wire-format text:
    using var pinnedBlob = ("02-665C74ED38FDFF095B2FC9319A272A75" + Environment.NewLine +
                           "05-CDECB88126DBC04D753E0C2D83D7B55D" + Environment.NewLine +
                           "07-54A83E34AB0310A7F5D80F2A68FD4F33").ToPinnedSecure();
    using var shares = Shares<BigInteger>.FromText(pinnedBlob);

    //// Reconstruct via the variable-time ExtendedEuclideanAlgorithm; for the
    //// fixed-iteration path with SecureBigInteger + MersenneSafeGcdAlgorithm,
    //// see the Security & Threat Model section.
    var gcd = new ExtendedEuclideanAlgorithm<BigInteger>();
    using var combiner = new SecretReconstructor<BigInteger>(gcd);
    using var recovered = combiner.Reconstruction(shares);

    //// Output should be 52199147989510990914370102003412153
    Console.WriteLine((BigInteger)recovered);
  }
}

The field a share was created in ๐Ÿงญ

A share is a point on a polynomial over a finite field, and until now it carried no record of which field. Reconstruction therefore derived one from the share values โ€” and when every value in the supplied subset happened to fall below a smaller Mersenne prime, it interpolated in the wrong field and returned a different secret, with no error at all (issue #403).

A share created by MakeShares now records its field in Share<TNumber>.SecurityLevel, and reconstruction uses it. For shares held in memory, nothing needs to change โ€” the round trip is simply correct now.

Stored shares are a different matter. ToCharArray() still writes the two-segment INDEX-VALUE form by default, which has no room for the level, so a share written out and read back is in exactly the position it was before. Opting into the three-segment form is what closes that, and it becomes the default in the next breaking-change cycle.

Writing shares that keep their field

//// Three segments: INDEX-VALUE-LEVEL, the exponent hexadecimal.
using var text = shares.ToCharArray(uppercase: true, withPrefix: false, ShareFormat.Extended);

//// Reads either form; a third segment is adopted when present.
using var reparsed = Shares<BigInteger>.FromText(text);

An older version of this library reading the three-segment form fails loudly rather than misreading it: its parser takes the first separator and treats the remainder as the value, where the second separator is not a hexadecimal digit and the decode is refused.

Migrating shares you already stored

//// Re-issue with the exponent the split actually used. Coordinates are cloned,
//// so the originals stay usable.
using var migrated = legacyShares.ReissueWithSecurityLevel(17);
using var text = migrated.ToCharArray(uppercase: true, withPrefix: false, ShareFormat.Extended);

You must supply the exponent the split actually used, after any auto-raise. MakeShares raises the level to fit the secret including its mark byte, so a one-byte secret split without an explicit level lands on 17, which is rarely the number a caller remembers asking for. A supported exponent that is large enough but simply wrong is accepted without complaint and can silently reconstruct a different secret: the coordinates do not say which field produced them, which is the absence this whole change works around. What is refused is an unsupported exponent, a field too small for the coordinates, and one contradicting a level the share already records.

"Unsupported" means the library's built-in table of Mersenne exponents. If your SecurityLevelManager is built on your own IMersennePrimeProvider, migrate against that provider, so migration and reconstruction answer to the same table:

//// The provider decides exactly: an exponent it supports is accepted even where the
//// built-in table lacks it, and one it rejects is refused even where the table has it.
using var migrated = legacyShares.ReissueWithSecurityLevel(17, mersennePrimeProvider);

Checking the exponent against your provider first and then calling the one-argument overload does not achieve the same: that overload refuses anything the built-in table lacks, whatever your provider says.

Naming the field per call instead

If you would rather not rewrite stored shares, name the field at reconstruction time:

serviceCollection.AddTransient<IReconstructionWithSecurityLevelUseCase<BigInteger>, SecretReconstructor<BigInteger>>();
// ...
using var recovered = useCase.Reconstruction(legacyShares, 17);

The explicit level outranks what the shares record but is checked against it โ€” a contradiction is refused rather than silently preferred. Without such an argument, shares that disagree among themselves, or where some record a level and others do not, are refused: there is nothing to choose between them, and choosing would be the guesswork this change removes. With one there is nothing left to choose, because the caller has chosen โ€” so a set mixing re-issued and untouched shares reconstructs, as long as every level actually recorded matches the argument.

That is also what makes the migration above resumable: a half-migrated set is usable through Reconstruction(shares, 17) rather than being stuck between two states.

Shares that record nothing go through the value-derived selection only where no level is named at all. Where that now collapses to an undecodable secret, the failure is a ReconstructionException naming the exponent the interpolation ran under, rather than an ArgumentException from two layers down. It still cannot name a cause: a tampered share produces exactly the same picture.

Secure console input โŒจ๏ธ

ConsolePasswordReader reads keyboard input one keystroke at a time directly into a pinned PinnedPoolArray<char> โ€” no string, no StringBuilder, no intermediate heap copy. The pinned buffer is the same shape that Secret<TNumber>.FromText(...), Share<TNumber>(...), and Shares<TNumber>.FromText(...) / Shares<TNumber>.FromTextLines(...) accept directly, so secrets and shares can flow end-to-end without ever materialising as a string. The two sub-examples below cover both directions: reading a secret to split, and reading shares to reconstruct.

Editing behaviour of ConsolePasswordReader.ReadPassword(int maxLength, char? echoChar = null):

  • Pressing Enter ends the input and returns the pinned buffer; the buffer's Length reflects the number of characters actually entered.
  • Pressing Backspace deletes the most recently accepted character and erases the echo on the console if echoChar is set.
  • Once maxLength characters have been entered, additional printable keystrokes are silently ignored until the user presses Enter or Backspace.
  • Non-printable control keys other than Enter and Backspace are ignored.
  • Ctrl+C is not intercepted โ€” the default console behaviour (process termination) applies.
  • ReadPassword throws InvalidOperationException when standard input is redirected (Console.IsInputRedirected == true); the API is interactive-only and cannot read from a pipe or a redirected file.

Read a secret, then split

using System;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;

namespace Example7a;

public class Program
{
  public static void Main(string[] args)
  {
    //// Read up to 64 characters from the console, echoing '*' per keystroke.
    //// The returned buffer is pinned and securely cleared on dispose.
    Console.Write("Secret: ");
    using var pinnedSecret = ConsolePasswordReader.ReadPassword(maxLength: 64, echoChar: '*');
    Console.WriteLine();
    using var secret = Secret<BigInteger>.FromText(pinnedSecret);

    //// Split the secret into 7 shares; any 3 are enough to reconstruct.
    using var splitter = new SecretSplitter<BigInteger>();
    using var shares = splitter.MakeShares(3, 7, secret);

    //// Emit all shares as hex lines (pinned char buffer, not redaction-gated).
    using var sharesChars = shares.ToCharArray();
    Console.Out.Write(sharesChars.PoolArray, 0, sharesChars.Length);
    Console.WriteLine();
  }
}

Read shares, then reconstruct

using System;
using System.Numerics;

using SecretSharingDotNet.Cryptography;
using SecretSharingDotNet.SecureMemory;
using SecretSharingDotNet.Cryptography.SecureInput;
using SecretSharingDotNet.Cryptography.ShamirsSecretSharing;
using SecretSharingDotNet.Math;

namespace Example7b;

public class Program
{
  public static void Main(string[] args)
  {
    //// Read three share lines from the console โ€” silent input, since
    //// echoChar defaults to null. Each line lands in its own pinned buffer.
    const int shareCount = 3;
    var entries = new PinnedPoolArray<char>[shareCount];
    for (int i = 0; i < shareCount; i++)
    {
      Console.Write($"Share {i + 1}: ");
      entries[i] = ConsolePasswordReader.ReadPassword(maxLength: 256);
      Console.WriteLine();
    }

    //// Wrap the per-line buffers in a PinnedPoolArrayList โ€” disposing it
    //// cascades to every contained PinnedPoolArray<char>. Shares.FromTextLines
    //// reads from the lines without taking ownership; the caller (this scope)
    //// retains and disposes them.
    using var pinnedLines = new PinnedPoolArrayList<char>(entries);
    using var shares = Shares<BigInteger>.FromTextLines(pinnedLines);

    //// Reconstruct via the variable-time ExtendedEuclideanAlgorithm; for the
    //// fixed-iteration path with SecureBigInteger and MersenneSafeGcdAlgorithm,
    //// see the Security & Threat Model section.
    var gcd = new ExtendedEuclideanAlgorithm<BigInteger>();
    using var combiner = new SecretReconstructor<BigInteger>(gcd);
    using var recovered = combiner.Reconstruction(shares);

    //// Emit the recovered secret as UTF-8 text (pinned char buffer, not
    //// redaction-gated).
    using var recoveredChars = recovered.ToCharArray();
    Console.Out.Write(recoveredChars.PoolArray, 0, recoveredChars.Length);
    Console.WriteLine();
  }
}

Security & Threat Model ๐Ÿ›ก๏ธ

The library targets two backends through the Calculator<TNumber> strategy pattern: the .NET-native BigInteger and a custom SecureBigInteger. The latter is positioned as the security-conscious option, but its protection is scoped โ€” it is not a drop-in replacement for hardened native crypto stacks.

SecureBigInteger protects against:

  • Passive memory disclosure. The internal byte buffer is GC-pinned and overwritten with a 3-pass scramble plus CryptographicOperations.ZeroMemory on dispose. Heap snapshots, swap files, and reuse-after-free of the same physical pages cannot recover plaintext.

  • No string materialisation in Base64 encoding. Secret<TNumber>.ToBase64CharArray() encodes directly into the pinned destination buffer on every supported TFM โ€” net8/9/10 and netstandard2.1 via Convert.TryToBase64Chars, legacy TFMs (netstandard2.0 / net4.7.2 / net4.8 / net4.8.1) via an inline 24-bit-window encoder mirroring the existing pinned Base64 decoder. No intermediate, GC-relocatable, possibly-interned managed string is created on any TFM.

  • Length-vs-content equality leaks. SecureBigInteger.Equals pre-pads to max(left, right) and uses fixed-time comparison; equality timing does not leak which prefix bytes match. The comparison runs uniformly across all targeted TFMs via a private XOR-OR-fold helper, with no #if-conditional code path.

  • Insecure RNG. Every random draw goes through System.Security.Cryptography.RandomNumberGenerator via the internal Cryptography.SecureRandom helper. No System.Random, no PRNG seeds.

  • Digit-value timing leaks in the per-limb arithmetic. Add, Subtract, Multiply, Square, Divide, and Remainder iterate a number of limbs fixed by the operand limb counts and use branchless carry/borrow propagation, so the arithmetic proper does not branch on the digit values. The bound differs per operation and is not the same one for all six: Subtract iterates max(left.LimbCount, right.LimbCount), Add that plus one for the carry limb, Multiply and Square run nested loops of left.LimbCount ร— right.LimbCount, and Divide and Remainder run dividend.LimbCount ร— 64 outer iterations whose inner work spans the remainder and divisor limbs. Each bound is derived from the limb counts alone, not from the digit values.

    MersenneModulo folds branchlessly and finishes with a mask-driven conditional subtract; its iteration count is derived from the public Mersenne exponent and the operand limb count โ€” which, per the normalisation note below, is itself secret-derived for intermediate values. This is a guarantee about the per-limb loop only โ€” see result normalisation and the sign of Add / Subtract operands below, both of which remain value-dependent.

  • No variable-time number-theoretic surface. The legacy Gcd, ModPow, Log, Log10, and Log2 static methods โ€” whose implementations branched on operand values or invoked Math.Log on operand-derived doubles โ€” were removed from SecureBigInteger. The surface is narrowed to operations whose timing is bounded by the public bit-length, so there is no longer a way to call a variable-time-on-secret- operand method by accident.

  • Timing leaks in modular inversion (when the consumer opts in). MersenneSafeGcdAlgorithm<TNumber> implements the Bernsteinโ€“Yang "safegcd" / divstep recurrence and provides a fixed-iteration modular inverse for use inside SecretReconstructor.DivMod. The outer iteration count is fixed at the public Mersenne exponent, independent of operand values; that is the only timing guarantee the algorithm gives out of the box. Per-iteration wall-clock time is not uniform on the SecureBigInteger backend โ€” the three divstep branches dispatch a different number of fresh Calculator allocations โ€” seven for each of the two odd-g branches (six arithmetic results plus one Clone) against six for the even-g branch (three arithmetic results plus three Clones) โ€” and funnel g (or g โˆ’ f) through SecureBigInteger.Divide, whose bit-loop count tracks the limb count of the shrinking intermediate working values. The branch selector reads the parity of g and the sign of delta, both derived from the denominator handed to Compute; in reconstruction that denominator is built from the public share indices, so at a fixed field the branch sequence is public. It is secret-dependent only for a caller passing a secret operand to Compute directly. See the class XDoc for the full breakdown. Strict branchless mask-select (all three branches run; result chosen via constant-time mask) is future work to lift this from "outer-iteration-count constant-time" to "per-iteration uniform". The exponent is derived from the modulus passed to Compute at call time โ€” the algorithm holds no ISecurityLevelManager reference, so it cannot drift from the SecretReconstructor consuming it. The simplest way to opt in is FixedIterationSecretReconstructor<TNumber> โ€” it accepts only fixed-iteration GCD strategies and defaults to MersenneSafeGcdAlgorithm<TNumber>, so the strategy cannot be mispaired:

    using var combiner = new FixedIterationSecretReconstructor<SecureBigInteger>();
    using var recovered = combiner.Reconstruction(shares);
    

    To pin a specific fixed-iteration strategy, pass it to the same type: new FixedIterationSecretReconstructor<SecureBigInteger>(new MersenneSafeGcdAlgorithm<SecureBigInteger>()).

    Scope: the fixed iteration count is relative to the security level. Reconstruction auto-selects that level from the shares (via AdjustSecurityLevel on the maximum share value), so the iteration count reflects the selected level โ€” which the share sizes already reveal. What this removes versus ExtendedEuclideanAlgorithm is the operand-value dependence within a level, not the level-selection dependence (observable only to a pure-timing attacker with no share access).

Public-input dependence (treated as public, not secret):

  • Pow(int exponent) is variable-time on the exponent value (iteration count is O(logโ‚‚(exponent)), with additional early-return shortcuts for exponent โˆˆ {0, 1} that distinguish those two values from all others). The exponent is not treated as secret; the per-iteration arithmetic on the secret base goes through the constant-time-on-bit-length Multiply. Callers must not pass secret-derived exponents through this method.

  • The sign of Add / Subtract operands, and the magnitude ordering of Subtract. Both branch on whether the operand signs match, and the routing differs between them: Add runs AddUnsigned on equal signs and CompareUnsigned followed by SubtractUnsigned on mixed ones, Subtract the other way round. For Add the two paths cost one operation against two, so wall-clock time distinguishes a same-sign addition from a mixed-sign one. For Subtract with equal signs โ€” the ordinary case for Shamir values, which the mark byte keeps positive โ€” the compare path is the one taken, and comparison >= 0 selects which operand becomes the minuend. Both selections call the same SubtractUnsigned over the same limb count, so magnitude ordering steers the branch without changing the amount of work; treat the ordering of two secret operands as observable rather than assuming the positive-value invariant removes the branch. Within this library the mixed-sign path is not reached on the Shamir values themselves โ€” the mark byte keeps secrets, coefficients and share values positive and MersenneModulo reduces into [0, M_p) โ€” so Subtract on them always takes the equal-sign compare path above, ordering branch included. The mixed-sign path is taken inside the modular inverse. MersenneSafeGcdAlgorithm.ApplyExtendedDivstep operates on the signed Bรฉzout coefficients: newUG = uG - uF turns negative at the first branch-1 iteration, which is the first divstep when the normalised denominator is odd and a later one when it is even, since the even-g branch carries uG through unchanged. MersenneModulo carries its own sign branch, whose negative path runs three additional limb loops, a SubtractInPlace and one more pinned allocation that the positive path skips entirely. MersenneSafeGcdAlgorithm.Compute reduces two signed intermediates per call โ€” beta * inv2n and alpha * inv2n, both sign-corrected Bรฉzout coefficients โ€” and the negative path runs for whichever of them is negative. It is not reached from SecretReconstructor.DivMod: Compute returns the coefficient already reduced into [0, M_p), so the reduction that follows there sees a non-negative value. IsOne likewise returns early on a negative sign, ahead of its fold.

    What this does and does not say about reconstruction. These branches are real, but inside this library they do not run on the secret. Compute has exactly one call site, SecretReconstructor.DivMod, which passes the normalised denominator and the field prime. Both DivMod call sites in LagrangeInterpolate build that denominator purely from differences of share indices, x_i - x_j; the secret share values enter the numerator, which never reaches Compute. At a fixed security level the divstep history, and therefore every sign it produces, is a function of the public share indices. What remains share-dependent is the level selection itself, already documented above as revealed by the share sizes. The warning binds callers who use SecureBigInteger or MersenneSafeGcdAlgorithm directly on signed secret operands: they must treat the sign as public, or normalise to magnitudes before the operation.

SecureBigInteger does not protect against:

  • ByteCount leaking the value's byte length. The property returns early when the high limb is zero and otherwise calls BytesInLimb, which counts significant bytes by shifting until the limb reaches zero โ€” one to eight iterations depending on the value. This is finer than the limb granularity of the normalisation note below, and it sits on the secret path: SecurityLevelManager.AdjustSecurityLevel evaluates maximumY.ByteCount on a share value, and Secret<TNumber> uses it on the secret and on polynomial coefficients.

  • Ordering and serialisation of SecureBigInteger. CompareTo returns early when the operand signs differ, before the fixed-count CompareUnsigned runs. ToByteArray and its IsExactByteBoundaryPowerOfTwo helper take several value-dependent early returns. Both are treated as boundary operations, like the hex and Base64 decoders below; only equality is constant-time.

  • Post-arithmetic zero branches. Add, Subtract, Multiply, Divide, and Remainder branch on IsZeroInternal() after the limb work, before assigning the result sign. The guarded work is a single field write, so the timing difference is minimal, but the predicate is the secret-derived zero status of the result. In Add and Subtract that branch also undoes a leak the code closes deliberately a few lines earlier: Subtract folds the comparison == 0 case into the >= 0 branch so timing cannot separate |minuend| == |subtrahend| from |minuend| โ‰  |subtrahend|, and the zero normalisation then branches on exactly that case. None of the five takes a zero short-circuit before the arithmetic โ€” Multiply carries a source comment explaining that branching there would leak โ€” and the remaining zero check is the divide-by-zero contract, evaluated on the divisor.

  • Result normalisation leaking the result's magnitude. Every unsigned helper returns through new SecureBigInteger(limbs, count, isNegative), whose constructor calls TrimLeadingZerosInPlace. That trim scans the limbs downward and stops at the first non-zero one, so its iteration count depends on how many leading zero limbs the result has โ€” a value-dependent quantity, even when both operands had the same limb count. The trimmed length is then stored and sizes the next operation, so the limb count of an intermediate value is itself secret-derived rather than public. The absolute cost is small โ€” at most one ulong comparison per limb of the result, which is wider than the operands: with two-limb operands at security level 127 a sum is three limbs, a product four, and a division remainder three โ€” but it is a data-dependent branch present in every core operation, so the arithmetic as a whole is not constant-time on the result magnitude. Removing this would require a fixed-width representation that never trims.

  • Variable-time modular inverse when a variable-time GCD is chosen. The base SecretReconstructor<TNumber> requires an explicit GCD strategy; pairing it with ExtendedEuclideanAlgorithm<TNumber> yields an iteration count that is variable on the operand values. This is a deliberate opt-out, not a default โ€” use FixedIterationSecretReconstructor<TNumber> (which accepts only fixed-iteration strategies and defaults to MersenneSafeGcdAlgorithm<TNumber>) for reconstruction whose timing does not depend on secret operands. The variable-time pairing remains available for the BigInteger backend and other cases where constant time is not a goal.

  • Hex- and Base64-decoder input-character timing. Share<TNumber>'s hex-deserialisation pipeline (DecodeHexToCalculator โ†’ GetHexValue) and Secret<TNumber>'s Base64 decode (DecodeBase64Char) are not constant-time on input characters โ€” both compile to branchy range-pattern switches whose wall-clock time varies by which character class the input belongs to. Both decoders are treated as boundary parsers: only the resulting Calculator<TNumber> value / decoded secret bytes flow through the CT primitives.

  • Ordering of Secret<TNumber>. Secret<TNumber>.CompareTo and the relational operators (<, >, <=, >=) ultimately use PinnedPoolArray<byte>.IStructuralComparable.CompareTo, which short-circuits on the first differing byte. Wall-clock time leaks the length of the common prefix between two secrets. Sorting or comparing secret material where ordering timing matters is out of scope โ€” only equality (Secret<TNumber>.Equals over the fixed-time path) is CT.

  • Mathematically-small Mersenne security levels. ISecurityLevelManager.SecurityLevel accepts every Mersenne prime exponent from the library's table, starting at MinMersennePrimeExponent = 13 (prime 2^13 โˆ’ 1 = 8191). A 13-bit modulus is mathematically tiny and is included for round-trip / parameter-exploration scenarios, not as a recommended cipher-strength floor. The auto-upgrade in SecretSplitter<TNumber>.MakeShares raises the level whenever the secret's bit-length exceeds the current prime; reconstruction runs at the level the shares record or the caller names, and only for shares recording neither fits the level to the maximum share value via SecurityLevelManager.AdjustSecurityLevel, which may raise or lower the level. Either way, consumers who explicitly construct shares at low exponents on a small secret receive what they ask for. Treat MinMersennePrimeExponent as a representational floor, not a security floor; production secrets should use a security level of 127 or higher, in line with the README examples.

  • Resource use of a supported security level. Validating an exponent keeps unsupported values out; it does not bound what a supported one costs. The built-in table reaches 43,112,609, and reconstruction computes the Mersenne prime for the level it runs under โ€” at least twice, once to check the coordinates against the field and once when the security level manager takes the level. That computation grows roughly quadratically with the exponent: seconds in the low millions, extrapolating to minutes at the top of the table, and longer on SecureBigInteger. Because shares can record their level, the cost no longer scales with the size of the input โ€” a three-segment share string of a dozen characters can name the largest exponent. Applications that reconstruct shares from untrusted sources should build their SecurityLevelManager on an IMersennePrimeProvider that supports only the exponents they use; reconstruction then refuses any other before computing a prime. Pass the same provider to ReissueWithSecurityLevel when migrating. The library sets no ceiling of its own, and such a provider is an allowlist, not a general denial-of-service defence.

  • Silent reconstruction on tampered shares. Shamir's scheme carries no integrity check on individual shares: if a single share's Index or Value is mutated in transit, in storage, or by a malicious participant, SecretReconstructor.Reconstruction produces a wrong secret with no exception, no warning, and no signal that anything is off. The library implements plain Shamir; it does not implement verifiable secret sharing (VSS โ€” e.g. Feldman or Pedersen) or per-share MACs. Consumers whose threat model includes share manipulation must layer an integrity scheme (signed shares, HMAC-keyed envelopes, VSS) on top. The recorded security level is not authenticated either. Reconstruction refuses shares whose levels disagree, but that detects inconsistency, not manipulation: a level changed alike on every share passes the check and can lead to a wrong secret just as silently. An integrity scheme has to cover the level together with the coordinates โ€” for the three-segment form, the whole string.

Not classified. The three groups above list what was examined. Absence from all of them is not a guarantee โ€” it means the surface was not walked against the code. That currently covers, among others, ToPinnedCharArray and the decimal conversion behind it, ToHexadecimal and FromHexadecimal, Abs and Negate, the conversion operators, and the limb/byte marshalling helpers. Several are reveal paths by design, where constant time is not a goal; the point of saying so is that you can tell an unexamined surface from an examined one.

Constant-time big-integer arithmetic in pure managed .NET is non-trivial; canonical implementations (libsodium, BoringSSL) rely on fixed-width representation, branchless conditional moves, and hand-tuned cache-pattern audits. The constant-time primitives exposed here approximate that approach within the constraints of managed .NET โ€” they are best-effort against passive timing analysis and have not been audited against active co-located attackers (cross-VM cache attacks, browser high-resolution timers, network-RTT measurements). Consumers whose threat model includes such attackers should layer the operation through a constant-time crypto stack (e.g. libsodium-net, hardware-backed enclaves) rather than rely on the SecureBigInteger naming alone.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 is compatible.  net48 is compatible.  net481 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETFramework 4.7.2

  • .NETFramework 4.8

  • .NETFramework 4.8.1

  • .NETStandard 2.0

  • .NETStandard 2.1

    • No dependencies.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on SecretSharingDotNet:

Package Downloads
Aero.Core

Aero core library with useful utility tools for any project

Leosac.KeyManager.Library.KeyGen

Key Generation Library for Leosac Key Manager development

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 1,320 9/26/2026
1.0.1 1,887 8/24/2026
1.0.1-rc02 138 7/12/2026
1.0.1-rc01 138 5/29/2026
0.14.0 10,301 12/25/2025
0.13.0 3,447 11/16/2025
0.12.0 6,940 11/17/2024
0.11.0 15,160 12/30/2023
0.10.2 2,776 9/16/2023
0.10.1 2,258 5/8/2023
0.10.0 9,574 12/24/2022
0.9.0 1,316 10/9/2022
0.8.0 4,379 7/5/2022
0.7.0 5,441 2/8/2022
0.6.0 10,490 11/25/2021
0.5.0 707 10/7/2021
0.4.2 3,112 12/18/2020
0.3.0 899 4/19/2020
0.2.0 771 4/12/2020
0.1.1 845 4/10/2020
Loading failed