Skip to content

Shamir's Secret Sharing — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! secret-sharing — Shamir's Secret Sharing over GF(256).
//!
//! Language: Zig 0.14 (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).

const std = @import("std");

/// Exponent table: GF256_EXP[i] = 3^i in GF(256). Index 255 mirrors index 0.
pub var gf256_exp: [256]u8 = undefined;
/// Discrete log table: GF256_LOG[3^i] = i (GF256_LOG[0] is unused).
pub var gf256_log: [256]u8 = undefined;

var tables_initialized = false;

/// Build the log/exp tables once (the TS reference does this in an IIFE).
fn initTables() void {
    if (tables_initialized) return;
    var x: u16 = 1;
    var i: usize = 0;
    while (i < 255) : (i += 1) {
        gf256_exp[i] = @intCast(x);
        gf256_log[x] = @intCast(i);
        // step to the next power of the generator 3: x *= 3 (i.e. x ^ xtime(x))
        x ^= xtime(@intCast(x));
    }
    // 3 has order 255, so EXP wraps: EXP[255] === EXP[0].
    gf256_exp[255] = 1;
    tables_initialized = true;
}

/// Multiply by 2 (x) in GF(256), reducing by 0x11B - the AES "xtime".
pub fn xtime(a: u8) u8 {
    const ext: u16 = @as(u16, a) << 1;
    const reduced = ext ^ (if (a & 0x80 != 0) @as(u16, 0x11b) else 0);
    return @intCast(reduced & 0xff);
}

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

/// Multiply two field elements via log/exp tables.
pub fn gfMul(a: u8, b: u8) u8 {
    initTables();
    if (a == 0 or b == 0) return 0;
    const sum = @as(usize, gf256_log[a]) + @as(usize, gf256_log[b]);
    return gf256_exp[sum % 255];
}

/// Multiplicative inverse of a non-zero element.
pub fn gfInv(a: u8) !u8 {
    initTables();
    if (a == 0) return error.ZeroHasNoInverse;
    const idx = (255 - @as(usize, gf256_log[a])) % 255;
    return gf256_exp[idx];
}

/// Divide a by b in GF(256).
pub fn gfDiv(a: u8, b: u8) !u8 {
    initTables();
    if (b == 0) return error.DivisionByZero;
    if (a == 0) return 0;
    const idx = (@as(usize, gf256_log[a]) + 255 - @as(usize, gf256_log[b])) % 255;
    return gf256_exp[idx];
}

/// One (x, y) point on a share polynomial.
pub const Point = struct { x: u8, y: u8 };

/// Evaluate a polynomial (coeffs[0] = constant term) at x, Horner style.
pub fn evalPoly(coeffs: []const u8, x: u8) u8 {
    var y: u8 = 0;
    var i: usize = coeffs.len;
    while (i > 0) {
        i -= 1;
        y = gfAdd(gfMul(y, x), coeffs[i]);
    }
    return 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.
pub fn interpolateAtZero(points: []const Point) u8 {
    var result: u8 = 0;
    for (points, 0..) |pj, j| {
        var weight: u8 = 1;
        for (points, 0..) |pm, m| {
            if (m == j) continue;
            weight = gfMul(weight, gfDiv(pm.x, pj.x ^ pm.x) catch 0);
        }
        result = gfAdd(result, gfMul(pj.y, weight));
    }
    return result;
}

pub const Error = error{
    ThresholdTooSmall,
    TooManyShares,
    ThresholdExceedsShares,
    MalformedShare,
    InvalidShareX,
    ConflictingShares,
    LengthMismatch,
    NeedMoreShares,
    OutOfMemory,
};

/// Split a secret into total_shares 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.
/// Caller owns each returned string (free with `allocator`).
pub fn splitSecret(
    allocator: std.mem.Allocator,
    secret: []const u8,
    total_shares: usize,
    threshold: usize,
) Error![][]u8 {
    if (threshold < 2) return Error.ThresholdTooSmall;
    if (total_shares > 255) return Error.TooManyShares;
    if (threshold > total_shares) return Error.ThresholdExceedsShares;

    const y_parts = try allocator.alloc([]u8, total_shares);
    var allocated: usize = 0;
    errdefer {
        for (y_parts[0..allocated]) |p| allocator.free(p);
        allocator.free(y_parts);
    }

    var coeffs = try allocator.alloc(u8, threshold);
    defer allocator.free(coeffs);

    for (0..total_shares) |i| {
        y_parts[i] = try allocator.alloc(u8, secret.len);
        allocated = i + 1;
    }

    for (secret, 0..) |secret_byte, b| {
        coeffs[0] = secret_byte;
        // coefficients 1..K-1 are fresh random bytes per secret byte
        std.crypto.random.bytes(coeffs[1..threshold]);
        for (1..total_shares + 1) |i| {
            y_parts[i - 1][b] = evalPoly(coeffs, @intCast(i));
        }
    }

    // Format each share as "xx-hex…" (xx = 2-digit hex x-coordinate).
    const out = try allocator.alloc([]u8, total_shares);
    errdefer allocator.free(out);
    for (y_parts, 0..) |ys, idx| {
        const share = try allocator.alloc(u8, 3 + ys.len * 2);
        out[idx] = share;
        _ = std.fmt.bufPrint(share[0..3], "{x:0>2}-", .{@as(u8, @intCast(idx + 1))}) catch
            unreachable;
        var p: usize = 3;
        for (ys) |byte| {
            _ = std.fmt.bufPrint(share[p .. p + 2], "{x:0>2}", .{byte}) catch unreachable;
            p += 2;
        }
        allocator.free(ys);
    }
    allocator.free(y_parts);
    return out;
}

/// A parsed share: its x-coordinate and its per-byte polynomial evaluations.
pub const ParsedShare = struct {
    x: u8,
    /// Caller-owned when allocated by parseShare.
    y: []u8,
};

/// Parse one "xx-hex" share string; fails on any malformed input.
/// The returned `y` slice is owned by the caller.
pub fn parseShare(allocator: std.mem.Allocator, share: []const u8) Error!ParsedShare {
    const s = std.mem.trim(u8, share, " \t\r\n");
    if (s.len < 3 or s[2] != '-') return Error.MalformedShare;
    const x = std.fmt.parseInt(u8, s[0..2], 16) catch return Error.MalformedShare;
    const y_hex = s[3..];
    if (y_hex.len % 2 != 0) return Error.MalformedShare;
    const y = try allocator.alloc(u8, y_hex.len / 2);
    errdefer allocator.free(y);
    for (y, 0..) |*byte, i| {
        byte.* = std.fmt.parseInt(u8, y_hex[i * 2 .. i * 2 + 2], 16) catch
            return Error.MalformedShare;
    }
    if (x == 0) return Error.InvalidShareX;
    return .{ .x = x, .y = y };
}

/// 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.
/// Caller owns the returned string.
pub fn reconstructSecret(
    allocator: std.mem.Allocator,
    shares: []const []const u8,
) Error![]u8 {
    if (shares.len < 2) return Error.NeedMoreShares;

    // Collect distinct x-coordinates (a share pasted twice is harmless; a
    // colliding x with a different payload cannot belong to one split).
    var parsed = try allocator.alloc(ParsedShare, shares.len);
    defer {
        for (parsed) |p| allocator.free(p.y);
        allocator.free(parsed);
    }
    for (shares, 0..) |share, i| {
        parsed[i] = try parseShare(allocator, share);
        for (parsed[0..i]) |prev| {
            if (prev.x == parsed[i].x) {
                if (!std.mem.eql(u8, prev.y, parsed[i].y)) return Error.ConflictingShares;
            }
        }
    }

    // Sort by x (insertion sort - share counts are tiny).
    std.mem.sort(ParsedShare, parsed, {}, struct {
        fn lt(_: void, a: ParsedShare, b: ParsedShare) bool {
            return a.x < b.x;
        }
    }.lt);

    // Dedupe equal shares.
    var distinct: usize = 0;
    for (parsed, 0..) |p, i| {
        if (i == 0 or p.x != parsed[i - 1].x) {
            parsed[distinct] = p;
            distinct += 1;
        }
    }
    const points_raw = parsed[0..distinct];
    if (points_raw.len < 2) return Error.NeedMoreShares;

    const len = points_raw[0].y.len;
    for (points_raw) |p| {
        if (p.y.len != len) return Error.LengthMismatch;
    }

    const out = try allocator.alloc(u8, len);
    errdefer allocator.free(out);
    var pair = try allocator.alloc(Point, points_raw.len);
    defer allocator.free(pair);
    for (0..len) |b| {
        for (points_raw, 0..) |p, i| pair[i] = .{ .x = p.x, .y = p.y[b] };
        out[b] = interpolateAtZero(pair);
    }
    return out;
}

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 →