///|
/// The standard JSON boundary: one request object in, one report object out.
/// Failures raise InputError with a plain message.
pub fn execute(request : Json) -> Json raise {
run(from_json(request)).to_json()
}
///|
/// The typed boundary over the same requests.
pub fn run(request : Value) -> Value raise InputError {
match get(request, "operation") {
String("split") => split_with(request)
String("combine") => combine_with(request)
String("diagnose") => diagnose_with(request)
String("verify") => verify_with(request)
String(other) => raise InputError("unsupported operation: " + other)
_ => raise InputError("missing field: operation")
}
}
///|
/// JSON shape of one share, both ways.
fn share_to_value(share : Share) -> Value {
record([
("index", Number(share.index.to_double())),
("threshold", Number(share.threshold.to_double())),
("digest", String(digest_hex(share.digest))),
("data", String(to_hex(share.data))),
])
}
///|
fn share_from_value(value : Value) -> Share raise InputError {
fields_only(value, ["index", "threshold", "digest", "data"])
let index = num(required(value, "index"))
if index.floor() != index || index < 1.0 || index > 255.0 {
raise InputError("share index must be a whole number 1..255")
}
let threshold = num(required(value, "threshold"))
if threshold.floor() != threshold || threshold < 2.0 || threshold > 255.0 {
raise InputError("share threshold must be a whole number 2..255")
}
let digest_bytes = parse_hex(str(required(value, "digest")))
if digest_bytes.length() != 4 {
raise InputError("share digest must be 8 hex digits")
}
let data = parse_hex(str(required(value, "data")))
if data.is_empty() {
raise InputError("share data must carry at least one byte")
}
{
index: index.to_int(),
threshold: threshold.to_int(),
digest: digest_of_bytes(digest_bytes),
data,
}
}
///|
/// Split a secret (hex) into n shares of which any k recover it. The seed
/// is the entropy of the split - the randomness source is injected by the
/// caller, never read from the environment, and the same (secret, seed)
/// pair reproduces the same shares everywhere.
fn split_with(request : Value) -> Value raise InputError {
fields_only(request, [
"operation", "secret_hex", "threshold", "shares", "seed",
])
let secret = parse_hex(str(required(request, "secret_hex")))
if secret.is_empty() {
raise InputError("secret_hex must carry at least one byte")
}
if secret.length() > 16384 {
raise InputError("secret_hex must stay within 16384 bytes")
}
let threshold = num(required(request, "threshold"))
if threshold.floor() != threshold || threshold < 2.0 || threshold > 255.0 {
raise InputError("threshold must be a whole number 2..255")
}
let shares = num(required(request, "shares"))
if shares.floor() != shares || shares < 2.0 || shares > 255.0 {
raise InputError("shares must be a whole number 2..255")
}
if shares < threshold {
raise InputError(
"shares must be at least the threshold: " + threshold.to_string(),
)
}
// Doubles carry 53 significant bits, so a seed above 2^53 - 1 cannot
// arrive intact through JSON; the bound is part of the contract.
let seed = match get(request, "seed") {
Null => 0.0
other => num(other)
}
if seed.floor() != seed || seed < 0.0 || seed > 9007199254740991.0 {
raise InputError("seed must be a whole number 0..9007199254740991")
}
let generated = shamir_split(
secret,
threshold.to_int(),
shares.to_int(),
seed.to_uint64(),
)
let encoded : Array[Value] = []
for share in generated {
encoded.push(share_to_value(share))
}
record([
("algorithm", String("shamir-gf256")),
("secret_bytes", Number(secret.length().to_double())),
("threshold", Number(threshold)),
("shares", Number(shares)),
("seed", Number(seed)),
("digest", String(digest_hex(crc32(secret)))),
("share_list", Array(encoded)),
])
}
///|
/// Recover the secret from k or more shares of one split. Below the
/// threshold the door stays closed - there is no wrong-secret output to
/// misuse. The recovered bytes are checked against the label every share
/// carried; verified is false when they disagree, which means a share was
/// corrupted or does not belong to this split.
fn combine_with(request : Value) -> Value raise InputError {
fields_only(request, ["operation", "shares"])
let raw = arr(required(request, "shares"))
if raw.length() < 2 || raw.length() > 255 {
raise InputError("shares must list 2..255 shares")
}
let parsed : Array[Share] = []
for entry in raw {
parsed.push(share_from_value(entry))
}
let (recovered, verified) = shamir_combine(parsed)
record([
("algorithm", String("shamir-gf256")),
("secret_hex", String(to_hex(recovered))),
("recovered_digest", String(digest_hex(crc32(recovered)))),
("verified", Bool(verified)),
])
}
///|
/// Diagnose a share set without asserting it is clean: which shares agree
/// with a quorum, which disagree with every quorum, and whether a
/// consistent secret exists at all.
fn diagnose_with(request : Value) -> Value raise InputError {
fields_only(request, ["operation", "shares"])
let raw = arr(required(request, "shares"))
if raw.length() < 2 || raw.length() > 12 {
raise InputError("shares must list 2..12 shares")
}
let parsed : Array[Share] = []
for entry in raw {
parsed.push(share_from_value(entry))
}
shamir_diagnose(parsed)
}
///|
/// Audit a share set without returning the secret: the same interpolation
/// combine performs, reported as a verdict only. When secret_hex is also
/// supplied, the verdict additionally requires the recovered bytes to
/// equal it - a holder can prove a share set belongs to a known secret
/// without the response carrying anything new.
fn verify_with(request : Value) -> Value raise InputError {
fields_only(request, ["operation", "shares", "secret_hex"])
let raw = arr(required(request, "shares"))
if raw.length() < 2 || raw.length() > 255 {
raise InputError("shares must list 2..255 shares")
}
let expected = match get(request, "secret_hex") {
Null => ""
other => {
// A claim about the secret is itself hex and length-checked here,
// so a malformed claim is a refusal, not a verdict.
let claimed = parse_hex(str(other))
to_hex(claimed)
}
}
let parsed : Array[Share] = []
for entry in raw {
parsed.push(share_from_value(entry))
}
let (recovered, verified) = shamir_combine(parsed)
let matches_secret = if expected == "" {
verified
} else {
verified && to_hex(recovered) == expected
}
record([
("algorithm", String("shamir-gf256")),
("shares", Number(parsed.length().to_double())),
("threshold", Number(threshold_of(parsed).to_double())),
("secret_length", Number(recovered.length().to_double())),
("verified", Bool(matches_secret)),
])
}
///|
fn threshold_of(shares : Array[Share]) -> Int32 {
shares[0].threshold
}