From 50fe18d6ce5dcbb88fe4161a3861af3f952ad4fe Mon Sep 17 00:00:00 2001 From: CleverWild Date: Fri, 12 Jun 2026 19:42:56 +0200 Subject: [PATCH] feat(server::crypto): add Shamir secret sharing utilities Wraps vsss_rs Gf256::split_array / combine_array into thin split_key / combine_shares helpers. Also widens derive_key salt parameter from &[u8;16] to &[u8] to accommodate the 32-byte share salts. --- .../src/crypto/encryption/v1.rs | 12 ++++----- .../crates/arbiter-server/src/crypto/mod.rs | 9 ++++--- .../arbiter-server/src/crypto/shamir.rs | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 server/crates/arbiter-server/src/crypto/shamir.rs diff --git a/server/crates/arbiter-server/src/crypto/encryption/v1.rs b/server/crates/arbiter-server/src/crypto/encryption/v1.rs index 0dac366..bc921c5 100644 --- a/server/crates/arbiter-server/src/crypto/encryption/v1.rs +++ b/server/crates/arbiter-server/src/crypto/encryption/v1.rs @@ -61,12 +61,12 @@ mod tests { #[test] fn derive_seal_key_deterministic() { static PASSWORD: &[u8] = b"password"; - let password = SafeCell::new(PASSWORD.to_vec()); - let password2 = SafeCell::new(PASSWORD.to_vec()); + let mut password = SafeCell::new(PASSWORD.to_vec()); + let mut password2 = SafeCell::new(PASSWORD.to_vec()); let salt = generate_salt(); - let mut key1 = derive_key(password, &salt); - let mut key2 = derive_key(password2, &salt); + let mut key1 = derive_key(&mut password, &salt); + let mut key2 = derive_key(&mut password2, &salt); let key1_reader = key1.0.read(); let key2_reader = key2.0.read(); @@ -77,10 +77,10 @@ mod tests { #[test] fn successful_derive() { static PASSWORD: &[u8] = b"password"; - let password = SafeCell::new(PASSWORD.to_vec()); + let mut password = SafeCell::new(PASSWORD.to_vec()); let salt = generate_salt(); - let mut key = derive_key(password, &salt); + let mut key = derive_key(&mut password, &salt); let key_reader = key.0.read(); assert_ne!(key_reader.as_slice(), &[0u8; 32][..]); diff --git a/server/crates/arbiter-server/src/crypto/mod.rs b/server/crates/arbiter-server/src/crypto/mod.rs index 13f2485..a100001 100644 --- a/server/crates/arbiter-server/src/crypto/mod.rs +++ b/server/crates/arbiter-server/src/crypto/mod.rs @@ -1,5 +1,5 @@ use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; -use encryption::v1::{Nonce, Salt}; +use encryption::v1::Nonce; use argon2::{Algorithm, Argon2}; use chacha20poly1305::{ @@ -13,6 +13,7 @@ use rand::{ pub mod encryption; pub mod integrity; +pub mod shamir; pub struct KeyCell(pub SafeCell); impl From> for KeyCell { @@ -94,7 +95,7 @@ impl KeyCell { } /// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation. -pub fn derive_key(password: &mut SafeCell>, salt: &Salt) -> KeyCell { +pub fn derive_key(password: &mut SafeCell>, salt: &[u8]) -> KeyCell { let params = { #[cfg(debug_assertions)] { @@ -132,10 +133,10 @@ mod tests { #[test] fn encrypt_decrypt() { static PASSWORD: &[u8] = b"password"; - let password = SafeCell::new(PASSWORD.to_vec()); + let mut password = SafeCell::new(PASSWORD.to_vec()); let salt = generate_salt(); - let mut key = derive_key(password, &salt); + let mut key = derive_key(&mut password, &salt); let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305 let associated_data = b"associated data"; let mut buffer = b"secret data".to_vec(); diff --git a/server/crates/arbiter-server/src/crypto/shamir.rs b/server/crates/arbiter-server/src/crypto/shamir.rs new file mode 100644 index 0000000..b369390 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/shamir.rs @@ -0,0 +1,27 @@ +use vsss_rs::Gf256; + +#[derive(Debug, thiserror::Error)] +pub enum ShamirError { + #[error("Failed to split key: {0}")] + Split(String), + #[error("Failed to combine shares: {0}")] + Combine(String), +} + +/// Split `key` into `total` shares where any `threshold` shares can reconstruct it. +/// Each returned Vec is a share with format [`identifier_byte`, `value_bytes`...]. +pub fn split_key( + threshold: usize, + total: usize, + key: &[u8], + rng: impl rand_core::RngCore + rand_core::CryptoRng, +) -> Result>, ShamirError> { + Gf256::split_array(threshold, total, key, rng) + .map_err(|e| ShamirError::Split(format!("{e:?}"))) +} + +/// Reconstruct the secret from `threshold` or more shares. +pub fn combine_shares(shares: &[Vec]) -> Result, ShamirError> { + Gf256::combine_array(shares) + .map_err(|e| ShamirError::Combine(format!("{e:?}"))) +}