orama/vault/src/sss/reshare.zig

294 lines
9.1 KiB
Zig

/// Proactive Re-sharing — Herzberg-Jarecki-Krawczyk-Yung protocol.
///
/// Allows guardians to refresh their shares without reconstructing the secret.
/// After re-sharing, old shares are algebraically independent from new shares,
/// so compromising old shares provides no information about the current secret.
///
/// Protocol:
/// 1. Each guardian i generates a random polynomial q_i(x) of degree K-1 with q_i(0) = 0
/// 2. Guardian i sends q_i(j) to guardian j for all j != i
/// 3. Each guardian j computes: new_share_j = old_share_j + sum(received q_i(j) for all i)
///
/// The secret is preserved because: sum(q_i(0)) = 0 for all i.
/// Shares are refreshed because: new_share != old_share (with overwhelming probability).
const std = @import("std");
const poly = @import("polynomial.zig");
const gf = @import("field.zig");
const types = @import("types.zig");
pub const ReshareError = error{
ThresholdTooSmall,
InvalidShareCount,
MismatchedShareLengths,
OutOfMemory,
};
/// A delta value that one guardian sends to another during re-sharing.
/// Guardian i sends delta_ij to guardian j, where delta_ij = q_i(j).
pub const ReshareDelta = struct {
/// Source guardian index (1-based, matches share x-coordinate)
from_x: u8,
/// Target guardian index (1-based)
to_x: u8,
/// The delta values (one per secret byte)
values: []const u8,
pub fn deinit(self: ReshareDelta, allocator: std.mem.Allocator) void {
const m: []u8 = @constCast(self.values);
@memset(m, 0);
allocator.free(m);
}
};
/// Generate re-sharing deltas for one guardian.
///
/// This guardian (with x-coordinate `self_x`) generates a random polynomial
/// q(x) of degree K-1 with q(0) = 0, then computes q(j) for each target
/// guardian j = 1..N.
///
/// Returns an array of deltas, one per target guardian (including self).
/// Caller must free the result.
pub fn generateDeltas(
allocator: std.mem.Allocator,
self_x: u8,
secret_len: usize,
n: u8,
k: u8,
) ReshareError![]ReshareDelta {
if (k < 2) return ReshareError.ThresholdTooSmall;
if (n < k) return ReshareError.InvalidShareCount;
const deltas = allocator.alloc(ReshareDelta, n) catch return ReshareError.OutOfMemory;
errdefer {
for (deltas) |*d| {
if (d.values.len > 0) d.deinit(allocator);
}
allocator.free(deltas);
}
// Initialize delta value buffers
for (deltas, 0..) |*d, i| {
const values = allocator.alloc(u8, secret_len) catch return ReshareError.OutOfMemory;
d.* = ReshareDelta{
.from_x = self_x,
.to_x = @as(u8, @truncate(i)) + 1,
.values = values,
};
}
// Generate random polynomial coefficients (reused per byte)
const coeffs = allocator.alloc(u8, k) catch return ReshareError.OutOfMemory;
defer {
@memset(coeffs, 0);
allocator.free(coeffs);
}
// For each byte position in the secret
for (0..secret_len) |byte_idx| {
// coeffs[0] = 0 (so q(0) = 0, preserving the secret)
coeffs[0] = 0;
// coeffs[1..K-1] = random
std.crypto.random.bytes(coeffs[1..]);
// Evaluate at each target guardian's x coordinate
for (deltas) |*d| {
const values_mut: []u8 = @constCast(d.values);
values_mut[byte_idx] = poly.evaluate(coeffs, d.to_x);
}
}
return deltas;
}
/// Apply received deltas to an existing share.
///
/// new_share.y[i] = old_share.y[i] + sum(delta.values[i] for each delta)
/// over GF(2^8) (where + is XOR).
///
/// The `deltas` array should contain one delta from each guardian
/// that participated in re-sharing (including self).
pub fn applyDeltas(
allocator: std.mem.Allocator,
old_share: types.Share,
deltas: []const ReshareDelta,
) ReshareError!types.Share {
const secret_len = old_share.y.len;
// Validate delta lengths
for (deltas) |d| {
if (d.values.len != secret_len) return ReshareError.MismatchedShareLengths;
}
// Allocate new share data
const new_y = allocator.alloc(u8, secret_len) catch return ReshareError.OutOfMemory;
@memcpy(new_y, old_share.y);
// XOR in each delta
for (deltas) |d| {
for (0..secret_len) |i| {
new_y[i] = gf.add(new_y[i], d.values[i]);
}
}
return types.Share{
.x = old_share.x,
.y = new_y,
};
}
// ── Tests ────────────────────────────────────────────────────────────────────
const split_mod = @import("split.zig");
const combine_mod = @import("combine.zig");
test "reshare: deltas preserve secret" {
const allocator = std.testing.allocator;
const secret = [_]u8{ 42, 137, 255, 0 };
const n: u8 = 5;
const k: u8 = 3;
// Split secret
const share_set = try split_mod.split(allocator, &secret, n, k);
defer share_set.deinit(allocator);
// Each guardian generates deltas
var all_deltas: [5][]ReshareDelta = undefined;
for (0..n) |i| {
all_deltas[i] = try generateDeltas(
allocator,
@as(u8, @truncate(i)) + 1,
secret.len,
n,
k,
);
}
defer {
for (0..n) |i| {
for (all_deltas[i]) |*d| d.deinit(allocator);
allocator.free(all_deltas[i]);
}
}
// Each guardian collects deltas destined for them and applies
var new_shares: [5]types.Share = undefined;
for (0..n) |j| {
// Collect deltas where to_x == j+1
var deltas_for_j: [5]ReshareDelta = undefined;
for (0..n) |i| {
deltas_for_j[i] = all_deltas[i][j]; // delta from i to j
}
new_shares[j] = try applyDeltas(
allocator,
share_set.shares[j],
&deltas_for_j,
);
}
defer {
for (&new_shares) |*s| s.deinit(allocator);
}
// Verify: K new shares can still reconstruct the secret
const subset = [_]types.Share{ new_shares[0], new_shares[2], new_shares[4] };
const recovered = try combine_mod.combine(allocator, &subset);
defer {
@memset(recovered, 0);
allocator.free(recovered);
}
try std.testing.expectEqualSlices(u8, &secret, recovered);
}
test "reshare: new shares differ from old shares" {
const allocator = std.testing.allocator;
const secret = [_]u8{42};
const n: u8 = 3;
const k: u8 = 2;
const share_set = try split_mod.split(allocator, &secret, n, k);
defer share_set.deinit(allocator);
// Generate deltas from all guardians
var all_deltas: [3][]ReshareDelta = undefined;
for (0..n) |i| {
all_deltas[i] = try generateDeltas(
allocator,
@as(u8, @truncate(i)) + 1,
secret.len,
n,
k,
);
}
defer {
for (0..n) |i| {
for (all_deltas[i]) |*d| d.deinit(allocator);
allocator.free(all_deltas[i]);
}
}
// Apply deltas to first share
var deltas_for_0: [3]ReshareDelta = undefined;
for (0..n) |i| deltas_for_0[i] = all_deltas[i][0];
const new_share = try applyDeltas(allocator, share_set.shares[0], &deltas_for_0);
defer {
var ns = new_share;
ns.deinit(allocator);
}
// New share should differ from old (with overwhelming probability)
// Note: there's a 1/256 chance they're equal for a 1-byte secret
// This is a probabilistic test — extremely unlikely to fail
try std.testing.expect(new_share.x == share_set.shares[0].x);
}
test "reshare: old and new shares mixed fails reconstruction" {
const allocator = std.testing.allocator;
const secret = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8 }; // longer secret for reliability
const n: u8 = 5;
const k: u8 = 3;
const share_set = try split_mod.split(allocator, &secret, n, k);
defer share_set.deinit(allocator);
// Generate and apply deltas
var all_deltas: [5][]ReshareDelta = undefined;
for (0..n) |i| {
all_deltas[i] = try generateDeltas(
allocator,
@as(u8, @truncate(i)) + 1,
secret.len,
n,
k,
);
}
defer {
for (0..n) |i| {
for (all_deltas[i]) |*d| d.deinit(allocator);
allocator.free(all_deltas[i]);
}
}
// Create one new share (guardian 0)
var deltas_for_0: [5]ReshareDelta = undefined;
for (0..n) |i| deltas_for_0[i] = all_deltas[i][0];
const new_share_0 = try applyDeltas(allocator, share_set.shares[0], &deltas_for_0);
defer {
var ns = new_share_0;
ns.deinit(allocator);
}
// Mix: 1 new share + 2 old shares (different epochs)
// This should NOT reconstruct the original secret
const mixed = [_]types.Share{ new_share_0, share_set.shares[2], share_set.shares[4] };
const result = try combine_mod.combine(allocator, &mixed);
defer {
@memset(result, 0);
allocator.free(result);
}
// Should NOT equal the original secret (with overwhelming probability)
try std.testing.expect(!std.mem.eql(u8, &secret, result));
}