Secret Sharing

Shamir's Secret Sharing (SSS) splits a secret into multiple shares. Any threshold of shares can reconstruct the secret, but fewer shares reveal nothing about it.

Mathematical Foundation

Text
┌─────────────────────────────────────────────────────────────────┐
│              Shamir's Secret Sharing                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Secret: S                                                      │
│  Threshold: t (minimum shares to reconstruct)                   │
│  Shares: n (total shares)                                       │
│                                                                 │
│  Polynomial: f(x) = S + a₁x + a₂x² + ... + aₜ₋₁xᵗ⁻¹ (mod p)     │
│                                                                 │
│  Where:                                                         │
│  • S is the constant term (the secret)                          │
│  • a₁, a₂, ..., aₜ₋₁ are random coefficients                    │
│  • p is a large prime                                           │
│                                                                 │
│  Shares: (1, f(1)), (2, f(2)), ..., (n, f(n))                   │
│                                                                 │
│  Reconstruction: Lagrange interpolation at x=0                  │
│                                                                 │
│             n                                                   │
│  S = f(0) = Σ  yᵢ × Lᵢ(0)                                       │
│            i=1                                                  │
│                                                                 │
│  Where Lᵢ(x) = ∏(j≠i) (x - xⱼ)/(xᵢ - xⱼ)                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

TypeScript Implementation

TypeScript
import { randomBytes } from "crypto";

// Finite field arithmetic over a prime
const PRIME = BigInt(
  "52435875175126190479447740508185965837690552500527637822603658699938581184513",
); // Baby JubJub prime

function mod(a: bigint, p: bigint = PRIME): bigint {
  return ((a % p) + p) % p;
}

function modInverse(a: bigint, p: bigint = PRIME): bigint {
  let [old_r, r] = [p, mod(a, p)];
  let [old_t, t] = [0n, 1n];

  while (r !== 0n) {
    const quotient = old_r / r;
    [old_r, r] = [r, old_r - quotient * r];
    [old_t, t] = [t, old_t - quotient * t];
  }

  return mod(old_t, p);
}

interface Share {
  x: bigint;
  y: bigint;
}

export class ShamirSecretSharing {
  private prime: bigint;

  constructor(prime: bigint = PRIME) {
    this.prime = prime;
  }

  // Generate random coefficients for polynomial
  private generateCoefficients(secret: bigint, threshold: number): bigint[] {
    const coefficients: bigint[] = [secret];

    for (let i = 1; i < threshold; i++) {
      const randomValue = BigInt("0x" + randomBytes(32).toString("hex"));
      coefficients.push(mod(randomValue, this.prime));
    }

    return coefficients;
  }

  // Evaluate polynomial at x
  private evaluatePolynomial(coefficients: bigint[], x: bigint): bigint {
    let result = 0n;
    let power = 1n;

    for (const coef of coefficients) {
      result = mod(result + mod(coef * power, this.prime), this.prime);
      power = mod(power * x, this.prime);
    }

    return result;
  }

  // Split secret into shares
  split(secret: bigint, threshold: number, numShares: number): Share[] {
    if (threshold > numShares) {
      throw new Error("Threshold cannot exceed number of shares");
    }

    if (threshold < 2) {
      throw new Error("Threshold must be at least 2");
    }

    const coefficients = this.generateCoefficients(secret, threshold);
    const shares: Share[] = [];

    for (let i = 1; i <= numShares; i++) {
      const x = BigInt(i);
      const y = this.evaluatePolynomial(coefficients, x);
      shares.push({ x, y });
    }

    return shares;
  }

  // Lagrange basis polynomial evaluated at 0
  private lagrangeBasis(shares: Share[], i: number): bigint {
    let numerator = 1n;
    let denominator = 1n;
    const xi = shares[i].x;

    for (let j = 0; j < shares.length; j++) {
      if (i !== j) {
        const xj = shares[j].x;
        numerator = mod(numerator * mod(-xj, this.prime), this.prime);
        denominator = mod(denominator * mod(xi - xj, this.prime), this.prime);
      }
    }

    return mod(numerator * modInverse(denominator, this.prime), this.prime);
  }

  // Reconstruct secret from shares
  reconstruct(shares: Share[]): bigint {
    if (shares.length < 2) {
      throw new Error("Need at least 2 shares");
    }

    let secret = 0n;

    for (let i = 0; i < shares.length; i++) {
      const basis = this.lagrangeBasis(shares, i);
      secret = mod(secret + mod(shares[i].y * basis, this.prime), this.prime);
    }

    return secret;
  }

  // Refresh shares without revealing secret
  refreshShares(shares: Share[], threshold: number): Share[] {
    // Generate a polynomial with zero constant term
    const zeroSecret = 0n;
    const refreshPoly = this.generateCoefficients(zeroSecret, threshold);

    // Add refresh values to existing shares
    return shares.map((share) => ({
      x: share.x,
      y: mod(
        share.y + this.evaluatePolynomial(refreshPoly, share.x),
        this.prime,
      ),
    }));
  }
}

// Serialize/deserialize shares
export function serializeShare(share: Share): string {
  return JSON.stringify({
    x: share.x.toString(),
    y: share.y.toString(),
  });
}

export function deserializeShare(data: string): Share {
  const parsed = JSON.parse(data);
  return {
    x: BigInt(parsed.x),
    y: BigInt(parsed.y),
  };
}

Solana Keypair Splitting

TypeScript
import { Keypair, PublicKey } from "@solana/web3.js";
import {
  ShamirSecretSharing,
  Share,
  serializeShare,
  deserializeShare,
} from "./sss";

export class KeySplitter {
  private sss: ShamirSecretSharing;

  constructor() {
    this.sss = new ShamirSecretSharing();
  }

  // Split a Solana keypair
  splitKeypair(
    keypair: Keypair,
    threshold: number,
    numShares: number,
  ): {
    publicKey: PublicKey;
    shares: string[];
  } {
    // Convert secret key to bigint
    const secretKeyBytes = keypair.secretKey.slice(0, 32); // First 32 bytes
    const secretBigInt = BigInt(
      "0x" + Buffer.from(secretKeyBytes).toString("hex"),
    );

    // Split the secret
    const shares = this.sss.split(secretBigInt, threshold, numShares);

    return {
      publicKey: keypair.publicKey,
      shares: shares.map(serializeShare),
    };
  }

  // Reconstruct keypair from shares
  reconstructKeypair(shares: string[]): Keypair {
    const parsedShares = shares.map(deserializeShare);

    // Reconstruct the secret
    const secretBigInt = this.sss.reconstruct(parsedShares);

    // Convert back to bytes
    const secretHex = secretBigInt.toString(16).padStart(64, "0");
    const secretKeyBytes = Buffer.from(secretHex, "hex");

    // Recreate keypair (Solana uses 64-byte secret key)
    return Keypair.fromSeed(secretKeyBytes);
  }

  // Verify shares without reconstruction
  verifyShareConsistency(shares: string[], threshold: number): boolean {
    if (shares.length < threshold) {
      return false;
    }

    const parsedShares = shares.map(deserializeShare);

    // Take threshold number of different subsets and verify they reconstruct the same value
    const firstSubset = parsedShares.slice(0, threshold);
    const firstValue = this.sss.reconstruct(firstSubset);

    // Check with a different subset
    for (let i = threshold; i < parsedShares.length; i++) {
      const subset = [...parsedShares.slice(0, threshold - 1), parsedShares[i]];
      const value = this.sss.reconstruct(subset);

      if (value !== firstValue) {
        return false;
      }
    }

    return true;
  }
}

Verifiable Secret Sharing

VSS adds verification to ensure shares are valid:

TypeScript
import { createHash, randomBytes } from "crypto";

interface VSSShare {
  x: bigint;
  y: bigint;
}

interface VSSCommitment {
  commitments: bigint[];
}

// Pedersen commitments for VSS
const G = BigInt(
  "5299619240641551281634865583518297030282874472190772894086521144482721001553",
);
const H = BigInt(
  "16950150798460657717958625567821834550301663161624707787222815936182638968203",
);
const ORDER = BigInt(
  "21888242871839275222246405745257275088548364400416034343698204186575808495617",
);

function modPow(base: bigint, exp: bigint, mod: bigint): bigint {
  let result = 1n;
  base = base % mod;

  while (exp > 0n) {
    if (exp % 2n === 1n) {
      result = (result * base) % mod;
    }
    exp = exp / 2n;
    base = (base * base) % mod;
  }

  return result;
}

export class VerifiableSecretSharing {
  // Feldman VSS: Dealer creates commitments to polynomial coefficients
  static createSharesWithCommitments(
    secret: bigint,
    threshold: number,
    numShares: number,
  ): {
    shares: VSSShare[];
    commitments: bigint[];
  } {
    // Generate random coefficients
    const coefficients: bigint[] = [secret];
    for (let i = 1; i < threshold; i++) {
      const randomValue = BigInt("0x" + randomBytes(32).toString("hex"));
      coefficients.push(randomValue % ORDER);
    }

    // Create commitments: C_i = g^{a_i}
    const commitments = coefficients.map((coef) => modPow(G, coef, ORDER));

    // Create shares
    const shares: VSSShare[] = [];
    for (let i = 1; i <= numShares; i++) {
      const x = BigInt(i);
      let y = 0n;
      let power = 1n;

      for (const coef of coefficients) {
        y = (y + coef * power) % ORDER;
        power = (power * x) % ORDER;
      }

      shares.push({ x, y });
    }

    return { shares, commitments };
  }

  // Verify a share against commitments
  static verifyShare(share: VSSShare, commitments: bigint[]): boolean {
    // Compute g^{share.y}
    const lhs = modPow(G, share.y, ORDER);

    // Compute ∏ C_j^{x^j}
    let rhs = 1n;
    let power = 1n;

    for (const commitment of commitments) {
      rhs = (rhs * modPow(commitment, power, ORDER)) % ORDER;
      power = (power * share.x) % ORDER;
    }

    return lhs === rhs;
  }

  // Reconstruct with verification
  static reconstructWithVerification(
    shares: VSSShare[],
    commitments: bigint[],
  ): bigint | null {
    // Verify all shares first
    for (const share of shares) {
      if (!this.verifyShare(share, commitments)) {
        console.error(`Share ${share.x} failed verification`);
        return null;
      }
    }

    // Standard Lagrange reconstruction
    let secret = 0n;

    for (let i = 0; i < shares.length; i++) {
      let basis = 1n;

      for (let j = 0; j < shares.length; j++) {
        if (i !== j) {
          const num = (ORDER - shares[j].x) % ORDER;
          const den = (shares[i].x - shares[j].x + ORDER) % ORDER;
          const denInv = modPow(den, ORDER - 2n, ORDER); // Fermat's little theorem
          basis = (basis * num * denInv) % ORDER;
        }
      }

      secret = (secret + shares[i].y * basis) % ORDER;
    }

    return secret;
  }
}

Distributed Key Generation

DKG allows parties to jointly generate shares without a trusted dealer:

TypeScript
interface DKGParticipant {
  id: number;
  secretPoly: bigint[];
  commitments: bigint[];
  receivedShares: Map<number, bigint>;
}

export class DKGProtocol {
  private participants: Map<number, DKGParticipant>;
  private threshold: number;
  private numParticipants: number;

  constructor(threshold: number, numParticipants: number) {
    this.threshold = threshold;
    this.numParticipants = numParticipants;
    this.participants = new Map();
  }

  // Round 1: Each participant generates polynomial and broadcasts commitments
  round1(participantId: number): {
    commitments: bigint[];
    sharesToSend: Map<number, bigint>;
  } {
    // Generate random polynomial
    const secretPoly: bigint[] = [];
    for (let i = 0; i < this.threshold; i++) {
      const randomValue = BigInt("0x" + randomBytes(32).toString("hex"));
      secretPoly.push(randomValue % ORDER);
    }

    // Create commitments
    const commitments = secretPoly.map((coef) => modPow(G, coef, ORDER));

    // Calculate shares for each participant
    const sharesToSend = new Map<number, bigint>();
    for (let j = 1; j <= this.numParticipants; j++) {
      let share = 0n;
      let power = 1n;
      const x = BigInt(j);

      for (const coef of secretPoly) {
        share = (share + coef * power) % ORDER;
        power = (power * x) % ORDER;
      }

      sharesToSend.set(j, share);
    }

    // Store participant state
    this.participants.set(participantId, {
      id: participantId,
      secretPoly,
      commitments,
      receivedShares: new Map(),
    });

    return { commitments, sharesToSend };
  }

  // Round 2: Participants receive and verify shares
  round2(
    participantId: number,
    fromId: number,
    share: bigint,
    commitments: bigint[],
  ): boolean {
    const participant = this.participants.get(participantId);
    if (!participant) throw new Error("Participant not initialized");

    // Verify share
    const x = BigInt(participantId);
    const lhs = modPow(G, share, ORDER);

    let rhs = 1n;
    let power = 1n;
    for (const commitment of commitments) {
      rhs = (rhs * modPow(commitment, power, ORDER)) % ORDER;
      power = (power * x) % ORDER;
    }

    if (lhs !== rhs) {
      return false;
    }

    participant.receivedShares.set(fromId, share);
    return true;
  }

  // Round 3: Combine shares to get final key share
  round3(participantId: number): {
    finalShare: bigint;
    publicKey: bigint;
  } {
    const participant = this.participants.get(participantId);
    if (!participant) throw new Error("Participant not initialized");

    // Sum all received shares including own
    let finalShare = participant.secretPoly[0]; // Own secret
    for (const [, share] of participant.receivedShares) {
      finalShare = (finalShare + share) % ORDER;
    }

    // Public key is the combined first commitment from all participants
    // (In practice, this would be computed from all participants' commitments)
    const publicKey = modPow(G, finalShare, ORDER);

    return { finalShare, publicKey };
  }
}

Usage Example

TypeScript
// Split a Solana keypair
const splitter = new KeySplitter();
const keypair = Keypair.generate();

const { publicKey, shares } = splitter.splitKeypair(keypair, 3, 5);

console.log("Public Key:", publicKey.toBase58());
console.log("Shares:", shares);

// Distribute shares to 5 parties...

// Reconstruct with any 3 shares
const reconstructedKeypair = splitter.reconstructKeypair([
  shares[0],
  shares[2],
  shares[4],
]);

console.log("Reconstructed:", reconstructedKeypair.publicKey.equals(publicKey)); // true

Next: Threshold Wallets - Building MPC-secured wallets.