Shamir's Secret Sharing — Swift 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 Swift 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: Swift 5.9+ (Foundation 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 Foundation
// MARK: - Errors
enum SecretSharingError: Error, CustomStringConvertible {
case zeroInverse
case divisionByZero
case thresholdTooLow
case tooManyShares
case thresholdExceedsTotal(Int, Int)
case noCsprng
case malformedShare(String, String)
case zeroXCoordinate
case notEnoughShares
case collidingX(Int)
case mismatchedLengths
var description: String {
switch self {
case .zeroInverse: return "0 has no multiplicative inverse in GF(256)"
case .divisionByZero: return "Division by zero in GF(256)"
case .thresholdTooLow: return "Threshold must be at least 2 (a 1-of-N split is just the secret itself)"
case .tooManyShares: return "Total shares must be at most 255 (share x-coordinates live in 1..255)"
case .thresholdExceedsTotal(let t, let n): return "Threshold (\(t)) cannot exceed total shares (\(n))"
case .noCsprng: return "No CSPRNG available (SystemRandomNumberGenerator is the source)"
case .malformedShare(let s, let why): return "Malformed share \"\(s)\" - \(why)"
case .zeroXCoordinate: return "Share x-coordinate 00 is invalid - shares are numbered from 01"
case .notEnoughShares: return "Need at least 2 distinct shares to reconstruct"
case .collidingX(let x): return String(format: "Two different shares both claim x=%02x - they cannot come from the same split", x)
case .mismatchedLengths: return "All shares must be the same length - they do not come from the same split"
}
}
}
// MARK: - GF(256) arithmetic
/// Exponent table: gf256Exp[i] = 3^i in GF(256). Index 255 mirrors index 0.
var gf256Exp = [UInt8](repeating: 0, count: 256)
/// Discrete log table: gf256Log[3^i] = i (gf256Log[0] is unused).
var gf256Log = [UInt8](repeating: 0, count: 256)
/// Multiply by 2 (x) in GF(256), reducing by 0x11B - the AES "xtime".
func xtime(_ a: UInt8) -> UInt8 {
let doubled = UInt8((Int(a) << 1) ^ (a & 0x80 != 0 ? 0x11b : 0))
return doubled & 0xff
}
private var tablesInitialized = false
func initTables() {
guard !tablesInitialized else { return }
var x: UInt8 = 1
for i in 0..<255 {
gf256Exp[i] = x
gf256Log[Int(x)] = UInt8(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].
gf256Exp[255] = 1
tablesInitialized = true
}
/// Addition in GF(256) is bitwise XOR (also serves as subtraction).
func gfAdd(_ a: UInt8, _ b: UInt8) -> UInt8 {
a ^ b
}
/// Multiply two field elements via log/exp tables.
func gfMul(_ a: UInt8, _ b: UInt8) -> UInt8 {
initTables() // idempotent - the log/exp tables are built on first use
if a == 0 || b == 0 { return 0 }
return gf256Exp[(Int(gf256Log[Int(a)]) + Int(gf256Log[Int(b)])) % 255]
}
/// Multiplicative inverse of a non-zero element.
func gfInv(_ a: UInt8) throws -> UInt8 {
if a == 0 { throw SecretSharingError.zeroInverse }
initTables()
return gf256Exp[(255 - Int(gf256Log[Int(a)])) % 255]
}
/// Divide a by b in GF(256).
func gfDiv(_ a: UInt8, _ b: UInt8) throws -> UInt8 {
if b == 0 { throw SecretSharingError.divisionByZero }
if a == 0 { return 0 }
initTables()
return gf256Exp[(Int(gf256Log[Int(a)]) + 255 - Int(gf256Log[Int(b)])) % 255]
}
/// Evaluate a polynomial (coeffs[0] = constant term) at x, Horner style.
func evalPoly(_ coeffs: [UInt8], _ x: UInt8) -> UInt8 {
var y: UInt8 = 0
for i in stride(from: coeffs.count - 1, through: 0, by: -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.
*/
func interpolateAtZero(_ points: [(x: UInt8, y: UInt8)]) throws -> UInt8 {
var result: UInt8 = 0
for j in 0..<points.count {
let xj = points[j].x
let yj = points[j].y
var weight: UInt8 = 1
for m in 0..<points.count where m != j {
let xm = points[m].x
weight = gfMul(weight, try gfDiv(xm, xj ^ xm))
}
result = gfAdd(result, gfMul(yj, weight))
}
return result
}
// MARK: - Hex codec
private func toHex(_ bytes: [UInt8]) -> String {
bytes.map { String(format: "%02x", $0) }.joined()
}
private func fromHex(_ hex: String) throws -> [UInt8] {
var out = [UInt8]()
out.reserveCapacity(hex.count / 2)
var i = hex.startIndex
while i < hex.endIndex {
let next = hex.index(i, offsetBy: 2)
guard next <= hex.endIndex, let b = UInt8(hex[i..<next], radix: 16) else {
throw SecretSharingError.malformedShare(hex, "the payload must be an even-length hex string")
}
out.append(b)
i = next
}
return out
}
/// Default CSPRNG source. SystemRandomNumberGenerator is cryptographically
/// secure on Apple platforms; injectable for deterministic tests.
typealias RandomBytes = (Int) -> [UInt8]
private func defaultRandomBytes(_ n: Int) -> [UInt8] {
var rng = SystemRandomNumberGenerator()
return (0..<n).map { _ in UInt8.random(in: .min ... .max, using: &rng) }
}
// MARK: - Split
/**
* 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.
*/
func splitSecret(_ secret: String, totalShares: Int, threshold: Int,
getRandomBytes: RandomBytes? = nil) throws -> [String] {
initTables()
guard threshold >= 2 else { throw SecretSharingError.thresholdTooLow }
guard totalShares <= 255 else { throw SecretSharingError.tooManyShares }
guard threshold <= totalShares else { throw SecretSharingError.thresholdExceedsTotal(threshold, totalShares) }
let bytes = Array(secret.utf8)
let rand = getRandomBytes ?? defaultRandomBytes
var yParts = [[UInt8]](repeating: [UInt8](repeating: 0, count: bytes.count), count: totalShares)
var coeffs = [UInt8](repeating: 0, count: threshold)
for b in 0..<bytes.count {
coeffs[0] = bytes[b]
if threshold > 1 {
let tail = rand(threshold - 1)
coeffs.replaceSubrange(1..., with: tail)
}
for i in 1...totalShares {
yParts[i - 1][b] = evalPoly(coeffs, UInt8(i))
}
}
return yParts.enumerated().map { idx, ys in
String(format: "%02x", idx + 1) + "-" + toHex(ys)
}
}
// MARK: - Reconstruct
/// A parsed share: its x-coordinate and its per-byte polynomial evaluations.
struct ParsedShare {
let x: UInt8
let y: [UInt8]
}
/// Parse one "xx-hex" share string; throws on any malformed input.
func parseShare(_ share: String) throws -> ParsedShare {
let s = share.trimmingCharacters(in: .whitespacesAndNewlines)
let chars = Array(s)
guard chars.count > 3, chars[2] == "-",
let x = UInt8(String(chars[0..<2]), radix: 16) else {
throw SecretSharingError.malformedShare(s, "expected the format \"xx-hex\u{2026}\" (e.g. \"01-a3b2c1\")")
}
let yHex = String(chars[3...])
guard yHex.allSatisfy({ $0.isASCII && $0.isHexDigit }), yHex.count % 2 == 0 else {
throw SecretSharingError.malformedShare(s, "the payload must be an even-length hex string")
}
if x == 0 { throw SecretSharingError.zeroXCoordinate }
return ParsedShare(x: x, y: try 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.
*/
func reconstructSecret(_ shares: [String]) throws -> String {
initTables()
guard shares.count >= 2 else { throw SecretSharingError.notEnoughShares }
var byX: [UInt8: [UInt8]] = [:]
for share in shares {
let parsed = try parseShare(share)
// The same share pasted twice is harmless (dedupe); a colliding
// x-coordinate with a different payload cannot belong to one split.
if let existing = byX[parsed.x] {
if existing != parsed.y { throw SecretSharingError.collidingX(Int(parsed.x)) }
} else {
byX[parsed.x] = parsed.y
}
}
let points = byX.sorted { $0.key < $1.key }.map { (x: $0.key, y: $0.value) }
guard points.count >= 2 else { throw SecretSharingError.notEnoughShares }
let len = points[0].y.count
guard points.allSatisfy({ $0.y.count == len }) else { throw SecretSharingError.mismatchedLengths }
var out = [UInt8](repeating: 0, count: len)
var pair = points.map { (x: $0.x, y: UInt8(0)) }
for b in 0..<len {
for i in 0..<points.count { pair[i].y = points[i].y[b] }
out[b] = try interpolateAtZero(pair)
}
guard let secret = String(bytes: out, encoding: .utf8) else {
// Fewer than the true threshold yields non-UTF-8 garbage; surface it raw.
return String(decoding: out, as: UTF8.self)
}
return secret
}
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 →