mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-17 02:54:13 +00:00
294 lines
9.1 KiB
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));
|
|
}
|