use crate::field::PrimeField;
use crate::bigint::BigUint;
use crate::csprng::Csprng;
use crate::secure::ct_eq_biguint;
#[derive(Clone, Eq, PartialEq)]
pub struct Share {
pub a: Vec<BigUint>,
pub b: BigUint,
}
impl core::fmt::Debug for Share {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("Share(<elided>)")
}
}
#[must_use]
pub fn split<R: Csprng>(
field: &PrimeField,
rng: &mut R,
secret: &BigUint,
k: usize,
n: usize,
) -> Vec<Share> {
assert!(k >= 2, "k must be at least 2 (k = 1 would leak the secret)");
assert!(n >= k, "n must be at least k");
let mut point: Vec<BigUint> = Vec::with_capacity(k);
point.push(field.reduce(secret));
for _ in 1..k {
point.push(field.random(rng));
}
const MAX_RESAMPLE: usize = 64;
let mut shares: Vec<Share> = Vec::with_capacity(n);
for share_idx in 0..n {
let mut attempt = 0usize;
loop {
let mut a: Vec<BigUint> = Vec::with_capacity(k - 1);
for _ in 0..(k - 1) {
a.push(field.random(rng));
}
let mut b = point[k - 1].clone();
for j in 0..(k - 1) {
let term = field.mul(&a[j], &point[j]);
b = field.add(&b, &term);
}
let candidate = Share { a, b };
if share_idx < k {
shares.push(candidate);
if first_k_full_rank(field, &shares) {
break;
}
shares.pop();
attempt += 1;
assert!(
attempt < MAX_RESAMPLE,
"Blakley split could not find a non-singular share \
within {MAX_RESAMPLE} resamples — field is too small \
for k = {k}",
);
} else {
shares.push(candidate);
break;
}
}
}
shares
}
#[allow(clippy::needless_range_loop)]
fn first_k_full_rank(field: &PrimeField, shares: &[Share]) -> bool {
let k = shares.len();
if k == 0 {
return true;
}
let coeff_len = shares[0].a.len() + 1; let mut mat: Vec<Vec<BigUint>> = Vec::with_capacity(k);
for s in shares {
let mut row = Vec::with_capacity(coeff_len);
for c in &s.a {
row.push(field.reduce(c));
}
row.push(BigUint::one());
mat.push(row);
}
for col in 0..k {
let mut pivot_row = None;
for r in col..k {
if !mat[r][col].is_zero() {
pivot_row = Some(r);
break;
}
}
let Some(pr) = pivot_row else {
return false;
};
if pr != col {
mat.swap(pr, col);
}
let inv = match field.inv(&mat[col][col]) {
Some(v) => v,
None => return false,
};
for c in col..k {
mat[col][c] = field.mul(&mat[col][c], &inv);
}
for r in 0..k {
if r == col || mat[r][col].is_zero() {
continue;
}
let factor = mat[r][col].clone();
for c in col..k {
let term = field.mul(&factor, &mat[col][c]);
mat[r][c] = field.sub(&mat[r][c], &term);
}
}
}
true
}
#[must_use]
#[allow(clippy::needless_range_loop)] pub fn reconstruct(field: &PrimeField, shares: &[Share], k: usize) -> Option<BigUint> {
if k < 2 || shares.len() < k {
return None;
}
for s in shares {
if s.a.len() != k - 1 {
return None;
}
}
let mut mat: Vec<Vec<BigUint>> = Vec::with_capacity(k);
for s in shares.iter().take(k) {
let mut row: Vec<BigUint> = Vec::with_capacity(k + 1);
for c in &s.a {
row.push(field.reduce(c));
}
row.push(BigUint::one());
row.push(field.reduce(&s.b));
mat.push(row);
}
for col in 0..k {
let mut pivot_row = None;
for r in col..k {
if !mat[r][col].is_zero() {
pivot_row = Some(r);
break;
}
}
let pr = pivot_row?;
if pr != col {
mat.swap(pr, col);
}
let inv = field.inv(&mat[col][col])?;
for c in col..=k {
mat[col][c] = field.mul(&mat[col][c], &inv);
}
for r in 0..k {
if r == col || mat[r][col].is_zero() {
continue;
}
let factor = mat[r][col].clone();
for c in col..=k {
let term = field.mul(&factor, &mat[col][c]);
mat[r][c] = field.sub(&mat[r][c], &term);
}
}
}
let point: Vec<BigUint> = (0..k).map(|i| mat[i][k].clone()).collect();
for s in &shares[k..] {
let mut lhs = point[k - 1].clone();
for j in 0..(k - 1) {
let term = field.mul(&s.a[j], &point[j]);
lhs = field.add(&lhs, &term);
}
let rhs = field.reduce(&s.b);
if !ct_eq_biguint(&lhs, &rhs) {
return None;
}
}
Some(point[0].clone())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::csprng::ChaCha20Rng;
fn rng() -> ChaCha20Rng {
ChaCha20Rng::from_seed(&[19u8; 32])
}
fn small() -> PrimeField {
PrimeField::new(BigUint::from_u64((1u64 << 61) - 1))
}
#[test]
fn round_trip() {
let f = small();
let mut r = rng();
let secret = BigUint::from_u64(0xC0FFEE);
for &(k, n) in &[(2usize, 3usize), (3, 5), (5, 9), (4, 7)] {
let shares = split(&f, &mut r, &secret, k, n);
assert_eq!(shares.len(), n);
assert_eq!(reconstruct(&f, &shares[..k], k), Some(secret.clone()));
assert_eq!(reconstruct(&f, &shares[1..1 + k], k), Some(secret.clone()));
}
}
#[test]
fn extra_shares_are_consistent() {
let f = small();
let mut r = rng();
let secret = BigUint::from_u64(98765);
let shares = split(&f, &mut r, &secret, 3, 6);
assert_eq!(reconstruct(&f, &shares, 3), Some(secret));
}
#[test]
fn tampered_extra_share_is_rejected() {
let f = small();
let mut r = rng();
let secret = BigUint::from_u64(424242);
let mut shares = split(&f, &mut r, &secret, 3, 5);
shares[4].b = f.add(&shares[4].b, &BigUint::from_u64(1));
assert!(reconstruct(&f, &shares, 3).is_none());
}
#[test]
fn below_threshold_returns_none() {
let f = small();
let mut r = rng();
let secret = BigUint::from_u64(7);
let shares = split(&f, &mut r, &secret, 4, 6);
assert!(reconstruct(&f, &shares[..3], 4).is_none());
}
#[test]
#[should_panic(expected = "k must be at least 2")]
fn split_rejects_k_one() {
let f = small();
let mut r = rng();
let _ = split(&f, &mut r, &BigUint::from_u64(1), 1, 3);
}
#[test]
fn tampered_first_extra_share_is_rejected() {
let f = small();
let mut r = rng();
let secret = BigUint::from_u64(0xCAFE);
let mut shares = split(&f, &mut r, &secret, 3, 5);
shares[3].b = f.add(&shares[3].b, &BigUint::from_u64(1));
assert!(reconstruct(&f, &shares, 3).is_none());
}
#[test]
fn tampered_exactly_one_extra_share_rejected() {
let f = small();
let mut r = rng();
let secret = BigUint::from_u64(0xBAD);
let mut shares = split(&f, &mut r, &secret, 3, 4);
shares[3].b = f.add(&shares[3].b, &BigUint::from_u64(1));
assert!(reconstruct(&f, &shares, 3).is_none());
}
#[test]
fn malformed_share_returns_none() {
let f = small();
let mut r = rng();
let secret = BigUint::from_u64(11);
let mut shares = split(&f, &mut r, &secret, 3, 4);
shares[0].a.push(BigUint::one());
assert!(reconstruct(&f, &shares, 3).is_none());
}
}