Shamir's Secret Sharing — C# source
Split a secret into N shares where any K shares can reconstruct it — but fewer than K reveal nothing. Based on Shamir's threshold scheme over GF(256).
This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Shamir's Secret Sharing over GF(256) — the Galois field of 256 elements.
// C# 12 / .NET 8 — ported from src/lib/secret-sharing.ts (the canonical
// TypeScript implementation). Display source for CosmoDev's polyglot pages.
//
// Pure math, zero dependencies. Addition in GF(256) is XOR; multiplication
// uses discrete log/exp tables built from the generator 3 (0x03) under the
// same reduction polynomial as AES (x^8 + x^4 + x^3 + x + 1 = 0x11B).
//
// Split: for each byte of the secret, build a random polynomial of degree
// K-1 whose constant term is the secret byte, then evaluate it at x = 1..N.
// Reconstruct: with K or more shares, Lagrange interpolation at x = 0
// recovers each constant term. Fewer than K shares reveal nothing
// (information-theoretic security).
using System.Text;
/// <summary>A parsed share: its x-coordinate and its per-byte polynomial evaluations.</summary>
/// <param name="X">Share index (1-255) parsed from the two-digit hex prefix.</param>
/// <param name="Y">One polynomial evaluation per secret byte.</param>
public sealed record ParsedShare(int X, byte[] Y);
public static class SecretSharing
{
/// <summary>Exponent table: GF256Exp[i] = 3^i in GF(256). Index 255 mirrors index 0.</summary>
public static readonly byte[] Gf256Exp = new byte[256];
/// <summary>Discrete log table: GF256Log[3^i] = i (GF256Log[0] is unused).</summary>
public static readonly byte[] Gf256Log = new byte[256];
static SecretSharing()
{
var x = 1;
for (var i = 0; i < 255; i++)
{
Gf256Exp[i] = (byte)x;
Gf256Log[x] = (byte)i;
// step to the next power of the generator 3: x *= 3 (i.e. x ^ xtime(x))
x ^= Xtime(x);
}
// 3 has order 255, so EXP wraps: EXP[255] === EXP[0].
Gf256Exp[255] = 1;
}
/// <summary>Multiply by 2 (x) in GF(256), reducing by 0x11B — the AES "xtime".</summary>
public static byte Xtime(byte a) =>
(byte)((a << 1) ^ ((a & 0x80) != 0 ? 0x11b : 0));
/// <summary>Addition in GF(256) is bitwise XOR (also serves as subtraction).</summary>
public static byte GfAdd(byte a, byte b) => (byte)(a ^ b);
/// <summary>Multiply two field elements via log/exp tables.</summary>
public static byte GfMul(byte a, byte b)
{
if (a == 0 || b == 0) return 0;
return Gf256Exp[(Gf256Log[a] + Gf256Log[b]) % 255];
}
/// <summary>Multiplicative inverse of a non-zero element.</summary>
public static byte GfInv(byte a)
{
if (a == 0) throw new ArgumentException("0 has no multiplicative inverse in GF(256).");
return Gf256Exp[(255 - Gf256Log[a]) % 255];
}
/// <summary>Divide a by b in GF(256).</summary>
public static byte GfDiv(byte a, byte b)
{
if (b == 0) throw new DivideByZeroException("Division by zero in GF(256).");
if (a == 0) return 0;
return Gf256Exp[(Gf256Log[a] + 255 - Gf256Log[b]) % 255];
}
/// <summary>Evaluate a polynomial (coeffs[0] = constant term) at x, Horner style.</summary>
public static byte EvalPoly(ReadOnlySpan<byte> coeffs, byte x)
{
byte y = 0;
for (var i = coeffs.Length - 1; i >= 0; i--)
{
y = GfAdd(GfMul(y, x), coeffs[i]);
}
return y;
}
/// <summary>
/// Lagrange interpolation at x = 0 over distinct-x points — recovers the
/// polynomial's constant term. Subtraction is XOR, so (0 - xm) = xm and
/// (xj - xm) = xj ^ xm.
/// </summary>
public static byte InterpolateAtZero(ReadOnlySpan<(byte X, byte Y)> points)
{
byte result = 0;
for (var j = 0; j < points.Length; j++)
{
var (xj, yj) = points[j];
byte weight = 1;
for (var m = 0; m < points.Length; m++)
{
if (m == j) continue;
var xm = points[m].X;
weight = GfMul(weight, GfDiv(xm, (byte)(xj ^ xm)));
}
result = GfAdd(result, GfMul(yj, weight));
}
return result;
}
private static string ToHex(ReadOnlySpan<byte> bytes) => Convert.ToHexString(bytes).ToLowerInvariant();
private static byte[] FromHex(string hex) =>
Convert.FromHexString(hex);
private static void DefaultRandomBytes(byte[] buffer) =>
System.Security.Cryptography.RandomNumberGenerator.Fill(buffer);
/// <summary>Random source injectable for deterministic tests; defaults to the CSPRNG.</summary>
public delegate void RandomBytes(byte[] buffer);
/// <summary>
/// Split a secret into <paramref name="totalShares"/> shares (x = 1..N)
/// where any <paramref name="threshold"/> of them reconstruct it. Returns
/// hex share strings like "01-a3b2c1..." — the two-digit hex
/// x-coordinate, a dash, then one hex byte per secret byte.
/// </summary>
public static string[] SplitSecret(
string secret, int totalShares, int threshold, RandomBytes? getRandomBytes = null)
{
if (threshold < 2)
{
throw new ArgumentOutOfRangeException(
nameof(threshold), "Threshold must be at least 2 (a 1-of-N split is just the secret itself).");
}
if (totalShares > 255)
{
throw new ArgumentOutOfRangeException(
nameof(totalShares), "Total shares must be at most 255 (share x-coordinates live in 1..255).");
}
if (threshold > totalShares)
{
throw new ArgumentException(
$"Threshold ({threshold}) cannot exceed total shares ({totalShares}).");
}
var bytes = Encoding.UTF8.GetBytes(secret);
getRandomBytes ??= DefaultRandomBytes;
var yParts = new byte[totalShares][];
for (var i = 0; i < totalShares; i++) yParts[i] = new byte[bytes.Length];
var coeffs = new byte[threshold];
var randomTail = new byte[threshold - 1];
for (var b = 0; b < bytes.Length; b++)
{
coeffs[0] = bytes[b];
if (threshold > 1)
{
getRandomBytes(randomTail);
randomTail.CopyTo(coeffs, 1);
}
for (var i = 1; i <= totalShares; i++)
{
yParts[i - 1][b] = EvalPoly(coeffs, (byte)i);
}
}
return yParts.Select((ys, idx) => $"{(idx + 1):x2}-{ToHex(ys)}").ToArray();
}
/// <summary>Parse one "xx-hex" share string; throws on any malformed input.</summary>
public static ParsedShare ParseShare(string share)
{
var s = share.Trim();
if (s.IndexOf('-') != 2 || s.Length < 3 || !IsHex(s[..2]))
{
throw new FormatException(
$"Malformed share \"{s}\" - expected the format \"xx-hex…\" (e.g. \"01-a3b2c1\").");
}
var yHex = s[3..];
if (yHex.Length % 2 != 0 || !IsHex(yHex))
{
throw new FormatException(
$"Malformed share \"{s}\" - the payload must be an even-length hex string.");
}
var x = Convert.ToInt32(s[..2], 16);
if (x == 0)
{
throw new FormatException("Share x-coordinate 00 is invalid - shares are numbered from 01.");
}
return new ParsedShare(x, FromHex(yHex));
static bool IsHex(string value) =>
value.Length > 0 && value.All(Uri.IsHexDigit);
}
/// <summary>
/// Reconstruct the secret from an arbitrary collection of share strings.
/// Needs at least 2 distinct shares (the threshold of the original split);
/// anything less than the true threshold K yields garbage without warning —
/// that is the security property of the scheme, not a bug.
/// </summary>
public static string ReconstructSecret(IEnumerable<string> shares)
{
var shareList = shares.ToArray();
if (shareList.Length < 2)
{
throw new ArgumentException("Need at least 2 shares to reconstruct.");
}
var byX = new Dictionary<int, byte[]>();
foreach (var share in shareList)
{
var parsed = ParseShare(share);
// The same share pasted twice is harmless (dedupe); a colliding
// x-coordinate with a different payload cannot belong to one split.
if (!byX.TryGetValue(parsed.X, out var existing))
{
byX[parsed.X] = parsed.Y;
}
else if (!existing.SequenceEqual(parsed.Y))
{
throw new ArgumentException(
$"Two different shares both claim x={parsed.X:x2} - they cannot come from the same split.");
}
}
var points = byX.OrderBy(kv => kv.Key).ToArray();
if (points.Length < 2)
{
throw new ArgumentException("Need at least 2 distinct shares to reconstruct.");
}
var len = points[0].Value.Length;
if (points.Any(p => p.Value.Length != len))
{
throw new ArgumentException(
"All shares must be the same length - they do not come from the same split.");
}
var output = new byte[len];
var pair = points.Select(p => (X: (byte)p.Key, Y: (byte)0)).ToArray();
for (var b = 0; b < len; b++)
{
for (var i = 0; i < points.Length; i++)
{
pair[i].Y = points[i].Value[b];
}
output[b] = InterpolateAtZero(pair);
}
return Encoding.UTF8.GetString(output);
}
}
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →