SecretSharingDotNet 1.1.0
dotnet add package SecretSharingDotNet --version 1.1.0
NuGet\Install-Package SecretSharingDotNet -Version 1.1.0
<PackageReference Include="SecretSharingDotNet" Version="1.1.0" />
<PackageVersion Include="SecretSharingDotNet" Version="1.1.0" />
<PackageReference Include="SecretSharingDotNet" />
paket add SecretSharingDotNet --version 1.1.0
#r "nuget: SecretSharingDotNet, 1.1.0"
#:package SecretSharingDotNet@1.1.0
#addin nuget:?package=SecretSharingDotNet&version=1.1.0
#tool nuget:?package=SecretSharingDotNet&version=1.1.0
Setup
Install SecretSharingDotNet package ๐ฅ
Open a console and switch to the directory containing your project file.
Use the following command to install version 1.1.0 of the SecretSharingDotNet package:
dotnet add package SecretSharingDotNet -v 1.1.0 -f <FRAMEWORK>After the completion of the command, look at the project file to make sure that the package is successfully installed.
You can open the
.csprojfile to see the added package reference:<ItemGroup> <PackageReference Include="SecretSharingDotNet" Version="1.1.0" /> </ItemGroup>
Remove SecretSharingDotNet package ๐ค
Open a console and switch to the directory containing your project file.
Use the following command to remove the SecretSharingDotNet package:
dotnet remove package SecretSharingDotNetAfter the completion of the command, look at the project file to make sure that the package is successfully removed.
You can open the
.csprojfile 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 anISerializationSurrogateforShare<TNumber>, a populatedShares<TNumber>used to round-trip throughBinaryFormatterand the formatter now rejects the root type. Register a surrogate forShares<TNumber>as well, or persist throughToCharArray(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 astringin.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 matchingToCharArray()methods onSecret,Share, andShares. Reconstruction(string)andReconstruction(string[])removed. Build aShares<TNumber>through one of the pinned factories above and pass that.Secret<TNumber>.ToBase64()โToBase64String()(string, redaction-gated) andToBase64CharArray()(pinned char buffer, not redacted).Share<TNumber>.X/.Yproperties โ.Index/.Value.MakeSharesparameter types.numberOfMinimumSharesandnumberOfSharesare nowintinstead ofTNumber.Calculator.ToInt32()/BigIntCalculator.ToInt32()removed.SecretReconstructor<TNumber>.SecurityLevelis now read-only โ it is derived from the supplied shares on everyReconstructioncall (see Basics).Calculator<TNumber>.Cloneis nowabstractโ custom backends must override it.ExtendedEuclideanAlgorithm<TNumber>is nowsealedโ derive fromIExtendedGcdAlgorithm<TNumber>or compose around it instead.SecretSplitter<TNumber>is nowsealedโ compose aroundIMakeSharesUseCase<TNumber>instead of deriving.SecureBigInteger.Pow(int)throwsArgumentOutOfRangeException(wasArgumentException) for negative exponents, consistent with the other length / exponent validations on the type.ArgumentOutOfRangeExceptionderives fromArgumentException, so wide catches still work.Shares<TNumber>.OriginalSecretandShares<TNumber>.OriginalSecretExistsremoved (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 nowinternal(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
Lengthreflects the number of characters actually entered. - Pressing Backspace deletes the most recently accepted character and erases the echo on the console if
echoCharis set. - Once
maxLengthcharacters 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.
ReadPasswordthrowsInvalidOperationExceptionwhen 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.ZeroMemoryon 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 viaConvert.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 managedstringis created on any TFM.Length-vs-content equality leaks.
SecureBigInteger.Equalspre-pads tomax(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.RandomNumberGeneratorvia the internalCryptography.SecureRandomhelper. NoSystem.Random, no PRNG seeds.Digit-value timing leaks in the per-limb arithmetic.
Add,Subtract,Multiply,Square,Divide, andRemainderiterate 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:Subtractiteratesmax(left.LimbCount, right.LimbCount),Addthat plus one for the carry limb,MultiplyandSquarerun nested loops ofleft.LimbCount ร right.LimbCount, andDivideandRemainderrundividend.LimbCount ร 64outer iterations whose inner work spans the remainder and divisor limbs. Each bound is derived from the limb counts alone, not from the digit values.MersenneModulofolds 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 ofAdd/Subtractoperands below, both of which remain value-dependent.No variable-time number-theoretic surface. The legacy
Gcd,ModPow,Log,Log10, andLog2static methods โ whose implementations branched on operand values or invokedMath.Logon operand-derived doubles โ were removed fromSecureBigInteger. 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 insideSecretReconstructor.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 freshCalculatorallocations โ seven for each of the two odd-gbranches (six arithmetic results plus oneClone) against six for the even-gbranch (three arithmetic results plus threeClones) โ and funnelg(org โ f) throughSecureBigInteger.Divide, whose bit-loop count tracks the limb count of the shrinking intermediate working values. The branch selector reads the parity ofgand the sign ofdelta, both derived from the denominator handed toCompute; 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 toComputedirectly. 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 toComputeat call time โ the algorithm holds noISecurityLevelManagerreference, so it cannot drift from theSecretReconstructorconsuming it. The simplest way to opt in isFixedIterationSecretReconstructor<TNumber>โ it accepts only fixed-iteration GCD strategies and defaults toMersenneSafeGcdAlgorithm<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.
Reconstructionauto-selects that level from the shares (viaAdjustSecurityLevelon the maximum share value), so the iteration count reflects the selected level โ which the share sizes already reveal. What this removes versusExtendedEuclideanAlgorithmis 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 isO(logโ(exponent)), with additional early-return shortcuts forexponent โ {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-lengthMultiply. Callers must not pass secret-derived exponents through this method.The sign of
Add/Subtractoperands, and the magnitude ordering ofSubtract. Both branch on whether the operand signs match, and the routing differs between them:AddrunsAddUnsignedon equal signs andCompareUnsignedfollowed bySubtractUnsignedon mixed ones,Subtractthe other way round. ForAddthe two paths cost one operation against two, so wall-clock time distinguishes a same-sign addition from a mixed-sign one. ForSubtractwith equal signs โ the ordinary case for Shamir values, which the mark byte keeps positive โ the compare path is the one taken, andcomparison >= 0selects which operand becomes the minuend. Both selections call the sameSubtractUnsignedover 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 andMersenneModuloreduces into[0, M_p)โ soSubtracton them always takes the equal-sign compare path above, ordering branch included. The mixed-sign path is taken inside the modular inverse.MersenneSafeGcdAlgorithm.ApplyExtendedDivstepoperates on the signed Bรฉzout coefficients:newUG = uG - uFturns 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-gbranch carriesuGthrough unchanged.MersenneModulocarries its own sign branch, whose negative path runs three additional limb loops, aSubtractInPlaceand one more pinned allocation that the positive path skips entirely.MersenneSafeGcdAlgorithm.Computereduces two signed intermediates per call โbeta * inv2nandalpha * inv2n, both sign-corrected Bรฉzout coefficients โ and the negative path runs for whichever of them is negative. It is not reached fromSecretReconstructor.DivMod:Computereturns the coefficient already reduced into[0, M_p), so the reduction that follows there sees a non-negative value.IsOnelikewise 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.
Computehas exactly one call site,SecretReconstructor.DivMod, which passes the normalised denominator and the field prime. BothDivModcall sites inLagrangeInterpolatebuild that denominator purely from differences of share indices,x_i - x_j; the secret share values enter the numerator, which never reachesCompute. 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 useSecureBigIntegerorMersenneSafeGcdAlgorithmdirectly on signed secret operands: they must treat the sign as public, or normalise to magnitudes before the operation.
SecureBigInteger does not protect against:
ByteCountleaking the value's byte length. The property returns early when the high limb is zero and otherwise callsBytesInLimb, 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.AdjustSecurityLevelevaluatesmaximumY.ByteCounton a share value, andSecret<TNumber>uses it on the secret and on polynomial coefficients.Ordering and serialisation of
SecureBigInteger.CompareToreturns early when the operand signs differ, before the fixed-countCompareUnsignedruns.ToByteArrayand itsIsExactByteBoundaryPowerOfTwohelper 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, andRemainderbranch onIsZeroInternal()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. InAddandSubtractthat branch also undoes a leak the code closes deliberately a few lines earlier:Subtractfolds thecomparison == 0case into the>= 0branch 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 โMultiplycarries 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 callsTrimLeadingZerosInPlace. 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 oneulongcomparison 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 withExtendedEuclideanAlgorithm<TNumber>yields an iteration count that is variable on the operand values. This is a deliberate opt-out, not a default โ useFixedIterationSecretReconstructor<TNumber>(which accepts only fixed-iteration strategies and defaults toMersenneSafeGcdAlgorithm<TNumber>) for reconstruction whose timing does not depend on secret operands. The variable-time pairing remains available for theBigIntegerbackend 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) andSecret<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 resultingCalculator<TNumber>value / decoded secret bytes flow through the CT primitives.Ordering of
Secret<TNumber>.Secret<TNumber>.CompareToand the relational operators (<,>,<=,>=) ultimately usePinnedPoolArray<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>.Equalsover the fixed-time path) is CT.Mathematically-small Mersenne security levels.
ISecurityLevelManager.SecurityLevelaccepts every Mersenne prime exponent from the library's table, starting atMinMersennePrimeExponent = 13(prime2^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 inSecretSplitter<TNumber>.MakeSharesraises 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 viaSecurityLevelManager.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. TreatMinMersennePrimeExponentas a representational floor, not a security floor; production secrets should use a security level of127or 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 onSecureBigInteger. 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 theirSecurityLevelManageron anIMersennePrimeProviderthat supports only the exponents they use; reconstruction then refuses any other before computing a prime. Pass the same provider toReissueWithSecurityLevelwhen 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
IndexorValueis mutated in transit, in storage, or by a malicious participant,SecretReconstructor.Reconstructionproduces 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 | Versions 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. |
-
.NETFramework 4.7.2
- System.Buffers (>= 4.6.1)
-
.NETFramework 4.8
- System.Buffers (>= 4.6.1)
-
.NETFramework 4.8.1
- System.Buffers (>= 4.6.1)
-
.NETStandard 2.0
- System.Buffers (>= 4.6.1)
-
.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 |