Shamir's secret sharing
Exploring a very simple and elegant cryptographic scheme with a step by step typescript implementation
Shamir’s secret sharing is a cryptographic scheme that allows you to break a secret into shares (sometimes also called “shards”) in such a way that a configurable amount of them is required to reconstruct the original.
It was created by Adi Shamir (the “S” in “RSA”!) and the beauty of it is that the high level concepts are understandable with high school level math and fairly easy to implement for demonstrative purposes.
It has numerous use cases: it is used as a key recovery mechanism for crypto wallets, and it can also function as a way to ensure cooperation between parties to access certain resources or to create asymmetric access structures (for example, using 4 shares with a treshold of 2 and giving 2 shares to a company’s CEO and one each to his assistants allows him to access the resource alone but requires his assistants to both agree.)
This post will explain the high level ideas behind the scheme with a follow along toy Typescript implementation.
Overview
Shamir’s scheme leverages the fact that points on a plane with different uniquely detetermine a polynomial of degree , while less than points allow an infinite number of polynomial to pass through them.
The uniquely determined polynomial intercepts the axis in one point, .
Let’t say that you want to split you secret into shares and you want of them to be able to reconstruct it.
You must:
- choose the point so that is your secret;
- generate random coefficients and build the polynomial
- generate random points : these are your shares!
By having at least points from the third steps, anyone is able to reconstruct the polynomial from the second step (for example with Lagrange interpolation) and thus determine the secret .
This is easier to show in code than with formulas, so let’s proceed with a step by step Typescript implementation for the following example:
The code to my safe is 6174 (Kaprekar’s constant) and I want to share it with 10 of my friends and family so that it takes at least 4 of them to open it.
Step 1: representing a point and a polynomial
Let’s start with the basic building block: a point on the plane. We’ll represent a polynomial simply as an array of its coefficients, where the index of the array is the power of that coefficient multiplies:
type Point = {
x: number;
y: number;
};
// coefficients[i] is the coefficient of x^i
// coefficients[0] is always the secret itself, since P(0) = coefficients[0]
type Polynomial = number[];
function evaluate(polynomial: Polynomial, x: number): number {
return polynomial.reduce((sum, coefficient, power) => {
return sum + coefficient * Math.pow(x, power);
}, 0);
}
Step 2: building the polynomial
To split our secret into shares requiring a threshold of , we need a polynomial of degree . Its constant term is our secret, and every other coefficient is chosen at random:
UPPER_BOUND = 9999 // in practice this could be unbounded, it's just to keep things readable
function randomCoefficient(): number {
// NOT cryptographically secure, this is a toy implementation
return Math.floor(Math.random() * UPPER_BOUND) + 1;
}
function buildPolynomial(secret: number, threshold: number): Polynomial {
const polynomial: Polynomial = [secret];
for (let power = 1; power < threshold; power++) {
polynomial.push(randomCoefficient());
}
return polynomial;
}
For our safe, threshold is 4, so buildPolynomial(6174, 4) gives us a degree-3 polynomial, something like:
Step 3: generating the shares
Now we just need to evaluate the polynomial at different, non-zero values of . We never hand out the point at , since that’s the secret itself:
function generateShares(secret: number, shareCount: number, threshold: number): Point[] {
const polynomial = buildPolynomial(secret, threshold);
const shares: Point[] = [];
for (let x = 1; x <= shareCount; x++) {
shares.push({ x, y: evaluate(polynomial, x) });
}
return shares;
}
const SECRET = 6174;
const N = 10; // number of shares
const K = 4; // threshold
const shares = generateShares(SECRET, N, K);
Each of my 10 friends and family members gets one Point. On their own, a single point (or even three of them)
tells you nothing about the secret: infinitely many degree-3 polynomials pass through any three points, and each
one has a different -intercept.
Step 4: reconstructing the secret
This is where Lagrange interpolation comes in. Given points, there’s exactly one polynomial of degree passing through all of them. We don’t actually need to rebuild the whole polynomial though: we only care about its value at , which simplifies the general Lagrange formula quite a bit:
In code:
function reconstructSecret(points: Point[]): number {
let secret = 0;
for (let i = 0; i < points.length; i++) {
const { x: xi, y: yi } = points[i];
let numerator = 1;
let denominator = 1;
for (let j = 0; j < points.length; j++) {
if (i === j) continue;
const { x: xj } = points[j];
numerator *= -xj;
denominator *= xi - xj;
}
secret += yi * (numerator / denominator);
}
return Math.round(secret);
}
Let’s try it with any 4 of the 10 shares:
const someShares = [shares[1], shares[3], shares[6], shares[9]];
console.log(reconstructSecret(someShares)); // 6174
It doesn’t matter which 4 shares I pick - any subset of 4 (or more) will reconstruct the exact same polynomial, and therefore the exact same secret. But if I only gather 3:
const tooFewShares = [shares[1], shares[3], shares[6]];
console.log(reconstructSecret(tooFewShares)); // some other number, NOT 6174
reconstructSecret happily returns a number, it just isn’t the right one. There are infinitely many degree-3
polynomials through those 3 points, and the formula above silently picks the one that assumes the missing point
doesn’t exist, landing on the wrong -intercept. This “confidently wrong” behavior is the seed of the first
drawback we’ll cover below.
Drawbacks
Shamir’s scheme is elegant, but it’s not a complete solution on its own. Two limitations show up almost immediately once you try to use it for anything beyond a toy example:
Share size
Every share is a full point on the polynomial, meaning its size is comparable to the secret itself (with some overhead for the coordinate and, in a finite field implementation, padding to the field size). Splitting a large secret such as a video file into 10 shares means storing roughly 10x the original data across all shareholders combined.
No authentication
As we saw above, the scheme cannot tell the difference between “not enough shares” and “wrong shares.” Any points, whether genuine or tampered with, produce some polynomial and some -intercept. There’s no built-in way to verify that a reconstructed secret is the real one, or that a given share hasn’t been corrupted or maliciously substituted. Extensions like Verifiable Secret Sharing address this, at the cost of extra complexity and additional data attached to each share.
Wrapping up
With about sixty lines of TypeScript, we can go from a hand-drawn intuition about lines and points to a working (albeit non-production) implementation of Shamir’s secret sharing: splitting a secret into shares, and reconstructing it from any large-enough subset. The core idea that points pin down a degree polynomial, while fewer than leave it wide open is the entire scheme. Everything else, from finite fields to authentication, is about closing the gaps between “mathematically elegant” and “practical and safe to use with a real secret.”
This is why YASSS encrypts the actual secret with AES-GCM (using the shares as key material, with additional authenticated data to bind each share to its context), and signs the shares with Ed25519 so that tampering or forged shares can be detected before they ever reach the reconstruction step. Shamir’s scheme handles the “how many parties need to agree” part, the rest of the implementation does logistics and the “can we trust what they’re giving us” part.
Thank you for reading and see you next time!