Skip to content

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.

// secret-sharing — Shamir's Secret Sharing over GF(256), the Galois field of
// 256 elements.
//
// Language: C++ (C++17, standard library only)
// Ported from src/lib/secret-sharing.ts (the canonical TypeScript implementation).
// display source — part of CosmoDev's polyglot tool 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).
//
// The TS reference encodes the secret with TextEncoder (UTF-8); a C++
// std::string already carries UTF-8 bytes, so it is used verbatim.

#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <random>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

namespace secret_sharing {

using Bytes = std::vector<uint8_t>;

/** Exponent table: GF256_EXP[i] = 3^i in GF(256). Index 255 mirrors index 0. */
std::array<uint8_t, 256> GF256_EXP;
/** Discrete log table: GF256_LOG[3^i] = i (GF256_LOG[0] is unused). */
std::array<uint8_t, 256> GF256_LOG;

/** Multiply by 2 (x) in GF(256), reducing by 0x11B - the AES "xtime". */
uint8_t xtime(uint8_t a) {
  return uint8_t((uint16_t(a) << 1) ^ ((a & 0x80) != 0 ? 0x11b : 0));
}

bool tables_ready = ([] {  // build the log/exp tables once, at static init
  uint8_t x = 1;
  for (int i = 0; i < 255; i++) {
    GF256_EXP[i] = x;
    GF256_LOG[x] = uint8_t(i);
    // step to the next power of the generator 3: x *= 3 (i.e. x ^ xtime(x))
    x = uint8_t(x ^ xtime(x));
  }
  // 3 has order 255, so EXP wraps: EXP[255] === EXP[0].
  GF256_EXP[255] = 1;
  return true;
})();

/** Addition in GF(256) is bitwise XOR (also serves as subtraction). */
uint8_t gfAdd(uint8_t a, uint8_t b) { return uint8_t(a ^ b); }

/** Multiply two field elements via log/exp tables. */
uint8_t gfMul(uint8_t a, uint8_t b) {
  if (a == 0 || b == 0) return 0;
  return GF256_EXP[(uint16_t(GF256_LOG[a]) + GF256_LOG[b]) % 255];
}

/** Multiplicative inverse of a non-zero element. */
uint8_t gfInv(uint8_t a) {
  if (a == 0) throw std::invalid_argument("0 has no multiplicative inverse in GF(256)");
  return GF256_EXP[(255 - GF256_LOG[a]) % 255];
}

/** Divide a by b in GF(256). */
uint8_t gfDiv(uint8_t a, uint8_t b) {
  if (b == 0) throw std::invalid_argument("Division by zero in GF(256)");
  if (a == 0) return 0;
  return GF256_EXP[(uint16_t(GF256_LOG[a]) + 255 - GF256_LOG[b]) % 255];
}

/** Evaluate a polynomial (coeffs[0] = constant term) at x, Horner style. */
uint8_t evalPoly(const Bytes& coeffs, uint8_t x) {
  uint8_t y = 0;
  for (std::size_t i = coeffs.size(); i-- > 0;) {
    y = gfAdd(gfMul(y, x), coeffs[i]);
  }
  return y;
}

using Point = std::pair<uint8_t, uint8_t>;  // (x, y)

/**
 * 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.
 */
uint8_t interpolateAtZero(const std::vector<Point>& points) {
  uint8_t result = 0;
  for (std::size_t j = 0; j < points.size(); j++) {
    const uint8_t xj = points[j].first;
    const uint8_t yj = points[j].second;
    uint8_t weight = 1;
    for (std::size_t m = 0; m < points.size(); m++) {
      if (m == j) continue;
      const uint8_t xm = points[m].first;
      weight = gfMul(weight, gfDiv(xm, uint8_t(xj ^ xm)));
    }
    result = gfAdd(result, gfMul(yj, weight));
  }
  return result;
}

// --- hex + random helpers -----------------------------------------------------

static std::string toHex(const Bytes& bytes) {
  static const char* DIGITS = "0123456789abcdef";
  std::string out;
  out.reserve(bytes.size() * 2);
  for (uint8_t b : bytes) {
    out += DIGITS[b >> 4];
    out += DIGITS[b & 0x0f];
  }
  return out;
}

static int hexValue(char c) {
  if (c >= '0' && c <= '9') return c - '0';
  if (c >= 'a' && c <= 'f') return c - 'a' + 10;
  if (c >= 'A' && c <= 'F') return c - 'A' + 10;
  return -1;
}

static Bytes fromHex(const std::string& hex) {
  Bytes out(hex.size() / 2);
  for (std::size_t i = 0; i < out.size(); i++) {
    out[i] = uint8_t(hexValue(hex[i * 2]) << 4 | hexValue(hex[i * 2 + 1]));
  }
  return out;
}

static bool isHex(const std::string& s) {
  if (s.empty()) return true;
  for (char c : s) {
    if (hexValue(c) < 0) return false;
  }
  return true;
}

/** Default CSPRNG: one call fills the buffer from std::random_device. */
static Bytes defaultRandomBytes(std::size_t n) {
  Bytes out(n);
  std::random_device rd;
  for (std::size_t i = 0; i < n; i++) out[i] = uint8_t(rd());
  return out;
}

static void assertPositiveInt(const char* name, int v) {
  if (v <= 0) throw std::invalid_argument(std::string(name) + " must be an integer >= 1");
}

/**
 * Split a secret into totalShares shares (x = 1..N) where any 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.
 */
std::vector<std::string> splitSecret(const std::string& secret, int totalShares, int threshold) {
  assertPositiveInt("Total shares", totalShares);
  assertPositiveInt("Threshold", threshold);
  if (threshold < 2) {
    throw std::invalid_argument("Threshold must be at least 2 (a 1-of-N split is just the secret itself)");
  }
  if (totalShares > 255) {
    throw std::invalid_argument("Total shares must be at most 255 (share x-coordinates live in 1..255)");
  }
  if (threshold > totalShares) {
    throw std::invalid_argument("Threshold (" + std::to_string(threshold) +
                                ") cannot exceed total shares (" + std::to_string(totalShares) + ")");
  }

  const Bytes bytes(secret.begin(), secret.end());  // UTF-8 bytes, verbatim
  std::vector<Bytes> yParts(totalShares, Bytes(bytes.size()));
  Bytes coeffs(threshold);

  for (std::size_t b = 0; b < bytes.size(); b++) {
    coeffs[0] = bytes[b];
    if (threshold > 1) {
      const Bytes rand = defaultRandomBytes(threshold - 1);
      for (int i = 1; i < threshold; i++) coeffs[i] = rand[i - 1];
    }
    for (int i = 1; i <= totalShares; i++) {
      yParts[i - 1][b] = evalPoly(coeffs, uint8_t(i));
    }
  }

  std::vector<std::string> shares;
  shares.reserve(totalShares);
  for (int idx = 0; idx < totalShares; idx++) {
    char x[3];
    snprintf(x, sizeof(x), "%02x", idx + 1);
    shares.push_back(std::string(x) + "-" + toHex(yParts[idx]));
  }
  return shares;
}

/** A parsed share: its x-coordinate and its per-byte polynomial evaluations. */
struct ParsedShare {
  uint8_t x;
  Bytes y;
};

/** Parse one "xx-hex" share string; throws on any malformed input. */
ParsedShare parseShare(const std::string& share) {
  // Trim ASCII whitespace from both ends.
  std::size_t begin = 0, end = share.size();
  while (begin < end && std::isspace(static_cast<unsigned char>(share[begin]))) begin++;
  while (end > begin && std::isspace(static_cast<unsigned char>(share[end - 1]))) end--;
  const std::string s = share.substr(begin, end - begin);

  if (s.find('-') != 2 || s.size() < 3 || !isHex(s.substr(0, 2)) || s.substr(0, 2).size() != 2) {
    throw std::invalid_argument("Malformed share \"" + s + "\" - expected the format \"xx-hex…\" (e.g. \"01-a3b2c1\")");
  }
  const std::string yHex = s.substr(3);
  if (!isHex(yHex) || yHex.size() % 2 != 0) {
    throw std::invalid_argument("Malformed share \"" + s + "\" - the payload must be an even-length hex string");
  }
  const int x = hexValue(s[0]) << 4 | hexValue(s[1]);
  if (x == 0) {
    throw std::invalid_argument("Share x-coordinate 00 is invalid - shares are numbered from 01");
  }
  return {uint8_t(x), fromHex(yHex)};
}

/**
 * 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.
 */
std::string reconstructSecret(const std::vector<std::string>& shares) {
  if (shares.size() < 2) throw std::invalid_argument("Need at least 2 shares to reconstruct");
  std::unordered_map<int, Bytes> byX;
  for (const auto& share : shares) {
    const ParsedShare parsed = parseShare(share);
    // The same share pasted twice is harmless (dedupe); a colliding
    // x-coordinate with a different payload cannot belong to one split.
    auto existing = byX.find(parsed.x);
    if (existing == byX.end()) {
      byX.emplace(parsed.x, parsed.y);
    } else if (existing->second != parsed.y) {
      char x[3];
      snprintf(x, sizeof(x), "%02x", parsed.x);
      throw std::invalid_argument("Two different shares both claim x=" + std::string(x) +
                                  " - they cannot come from the same split");
    }
  }
  std::vector<std::pair<int, Bytes>> points(byX.begin(), byX.end());
  std::sort(points.begin(), points.end(),
            [](const auto& a, const auto& b) { return a.first < b.first; });
  if (points.size() < 2) {
    throw std::invalid_argument("Need at least 2 distinct shares to reconstruct");
  }
  const std::size_t len = points[0].second.size();
  for (const auto& [x, y] : points) {
    if (y.size() != len) {
      throw std::invalid_argument("All shares must be the same length - they do not come from the same split");
    }
  }

  Bytes out(len);
  std::vector<Point> pair;
  for (const auto& [x, y] : points) pair.emplace_back(uint8_t(x), 0);
  for (std::size_t b = 0; b < len; b++) {
    for (std::size_t i = 0; i < points.size(); i++) pair[i].second = points[i].second[b];
    out[b] = interpolateAtZero(pair);
  }
  return std::string(out.begin(), out.end());  // UTF-8 bytes back to a string
}

}  // namespace secret_sharing

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 →