Skip to content

Shamir's Secret Sharing — Java 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 Java 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.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/secret-sharing.ts
// 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).

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class SecretSharing {

    /** Exponent table: GF256_EXP[i] = 3^i in GF(256). Index 255 mirrors index 0. */
    public static final int[] GF256_EXP = new int[256];
    /** Discrete log table: GF256_LOG[3^i] = i (GF256_LOG[0] is unused). */
    public static final int[] GF256_LOG = new int[256];

    static {
        int x = 1;
        for (int i = 0; i < 255; i++) {
            GF256_EXP[i] = x;
            GF256_LOG[x] = 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].
        GF256_EXP[255] = 1;
    }

    private static final SecureRandom RANDOM = new SecureRandom();

    private SecretSharing() {
    }

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

    /** Addition in GF(256) is bitwise XOR (also serves as subtraction). */
    public static int gfAdd(int a, int b) {
        return (a ^ b) & 0xff;
    }

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

    /** Multiplicative inverse of a non-zero element. */
    public static int gfInv(int a) {
        if (a == 0) {
            throw new ArithmeticException("0 has no multiplicative inverse in GF(256)");
        }
        return GF256_EXP[(255 - GF256_LOG[a]) % 255];
    }

    /** Divide a by b in GF(256). */
    public static int gfDiv(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero in GF(256)");
        }
        if (a == 0) {
            return 0;
        }
        return GF256_EXP[(GF256_LOG[a] + 255 - GF256_LOG[b]) % 255];
    }

    /** Evaluate a polynomial (coeffs[0] = constant term) at x, Horner style. */
    public static int evalPoly(byte[] coeffs, int x) {
        int y = 0;
        for (int i = coeffs.length - 1; i >= 0; i--) {
            y = gfAdd(gfMul(y, x), coeffs[i] & 0xff);
        }
        return y;
    }

    /** One (x, y) point for Lagrange interpolation. */
    public record Point(int x, int 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.
     */
    public static int interpolateAtZero(List<Point> points) {
        int result = 0;
        for (int j = 0; j < points.size(); j++) {
            int xj = points.get(j).x();
            int weight = 1;
            for (int m = 0; m < points.size(); m++) {
                if (m == j) {
                    continue;
                }
                int xm = points.get(m).x();
                weight = gfMul(weight, gfDiv(xm, xj ^ xm));
            }
            result = gfAdd(result, gfMul(points.get(j).y(), weight));
        }
        return result;
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    private static byte[] fromHex(String hex) {
        byte[] out = new byte[hex.length() / 2];
        for (int i = 0; i < out.length; i++) {
            out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
        }
        return out;
    }

    /** A parsed share: its x-coordinate and its per-byte polynomial evaluations. */
    public record ParsedShare(int x, byte[] y) {
    }

    /** Parse one "xx-hex" share string; throws on any malformed input. */
    public static ParsedShare parseShare(String share) {
        String s = share.trim();
        if (s.indexOf('-') != 2 || !s.substring(0, 2).matches("[0-9a-fA-F]{2}")) {
            throw new IllegalArgumentException(
                    "Malformed share \"" + s + "\" - expected the format \"xx-hex…\" (e.g. \"01-a3b2c1\")");
        }
        String yHex = s.substring(3);
        if (!yHex.matches("[0-9a-fA-F]*") || yHex.length() % 2 != 0) {
            throw new IllegalArgumentException(
                    "Malformed share \"" + s + "\" - the payload must be an even-length hex string");
        }
        int x = Integer.parseInt(s.substring(0, 2), 16);
        if (x == 0) {
            throw new IllegalArgumentException("Share x-coordinate 00 is invalid - shares are numbered from 01");
        }
        return new ParsedShare(x, fromHex(yHex));
    }

    /**
     * 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.
     */
    public static List<String> splitSecret(String secret, int totalShares, int threshold) {
        if (threshold < 2) {
            throw new IllegalArgumentException(
                    "Threshold must be at least 2 (a 1-of-N split is just the secret itself)");
        }
        if (totalShares > 255) {
            throw new IllegalArgumentException(
                    "Total shares must be at most 255 (share x-coordinates live in 1..255)");
        }
        if (threshold > totalShares) {
            throw new IllegalArgumentException(
                    "Threshold (" + threshold + ") cannot exceed total shares (" + totalShares + ")");
        }

        byte[] bytes = secret.getBytes(java.nio.charset.StandardCharsets.UTF_8);
        byte[][] yParts = new byte[totalShares][bytes.length];
        byte[] coeffs = new byte[threshold];

        // Per secret byte: constant term = the byte, fresh random higher
        // coefficients, then evaluate the polynomial at every share's x.
        for (int b = 0; b < bytes.length; b++) {
            coeffs[0] = bytes[b];
            if (threshold > 1) {
                byte[] rnd = new byte[threshold - 1];
                RANDOM.nextBytes(rnd);
                System.arraycopy(rnd, 0, coeffs, 1, threshold - 1);
            }
            for (int i = 1; i <= totalShares; i++) {
                yParts[i - 1][b] = (byte) evalPoly(coeffs, i);
            }
        }

        List<String> shares = new ArrayList<>(totalShares);
        for (int idx = 0; idx < totalShares; idx++) {
            shares.add(String.format("%02x-%s", idx + 1, toHex(yParts[idx])));
        }
        return shares;
    }

    /**
     * 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.
     */
    public static String reconstructSecret(List<String> shares) {
        if (shares.size() < 2) {
            throw new IllegalArgumentException("Need at least 2 shares to reconstruct");
        }
        Map<Integer, byte[]> byX = new LinkedHashMap<>();
        for (String share : shares) {
            ParsedShare parsed = parseShare(share);
            byte[] existing = byX.get(parsed.x());
            // The same share pasted twice is harmless (dedupe); a colliding
            // x-coordinate with a different payload cannot belong to one split.
            if (existing == null) {
                byX.put(parsed.x(), parsed.y());
            } else if (!java.util.Arrays.equals(existing, parsed.y())) {
                throw new IllegalArgumentException("Two different shares both claim x="
                        + String.format("%02x", parsed.x()) + " - they cannot come from the same split");
            }
        }
        List<Map.Entry<Integer, byte[]>> points = new ArrayList<>(byX.entrySet());
        points.sort(Map.Entry.comparingByKey());
        if (points.size() < 2) {
            throw new IllegalArgumentException("Need at least 2 distinct shares to reconstruct");
        }
        int len = points.get(0).getValue().length;
        for (Map.Entry<Integer, byte[]> e : points) {
            if (e.getValue().length != len) {
                throw new IllegalArgumentException(
                        "All shares must be the same length - they do not come from the same split");
            }
        }

        byte[] out = new byte[len];
        List<Point> pair = new ArrayList<>();
        for (Map.Entry<Integer, byte[]> point : points) {
            pair.add(new Point(point.getKey(), 0));
        }
        for (int b = 0; b < len; b++) {
            for (int i = 0; i < points.size(); i++) {
                pair.set(i, new Point(points.get(i).getKey(), points.get(i).getValue()[b] & 0xff));
            }
            out[b] = (byte) interpolateAtZero(pair);
        }
        return new String(out, java.nio.charset.StandardCharsets.UTF_8);
    }
}

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 →