refactor(server::crypto): use fixed-size [u8; 32] and KeyCell throughout seal key API
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful

This commit is contained in:
CleverWild
2026-06-12 21:15:07 +02:00
parent a3b98ca024
commit 0098c3c08a
12 changed files with 53 additions and 69 deletions

View File

@@ -162,13 +162,12 @@ impl Vault {
#[messages]
impl Vault {
#[message]
pub async fn bootstrap(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
pub async fn bootstrap(&mut self, mut seal_key: KeyCell) -> Result<(), Error> {
if !matches!(&self.state, State::Unbootstrapped) {
return Err(Error::AlreadyBootstrapped);
}
let mut root_key = KeyCell::new_secure_random();
let mut seal_key = KeyCell::try_from(seal_key_raw).map_err(|()| Error::InvalidKey)?;
// Zero nonces are fine because they are one-time
let root_key_nonce = Nonce::default();
@@ -227,7 +226,7 @@ impl Vault {
}
#[message]
pub async fn try_unseal(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
pub async fn try_unseal(&mut self, mut seal_key: KeyCell) -> Result<(), Error> {
let State::Sealed {
root_key_history_id,
} = &self.state
@@ -246,8 +245,6 @@ impl Vault {
.await?
};
let mut seal_key = KeyCell::try_from(seal_key_raw).map_err(|()| Error::InvalidKey)?;
let nonce =
Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(|()| {
error!("Broken database: invalid nonce for root key");
@@ -422,8 +419,7 @@ mod tests {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new([0u8; 32].to_vec());
actor.bootstrap(seal_key).await.unwrap();
actor.bootstrap(KeyCell::from([0u8; 32])).await.unwrap();
actor
}

View File

@@ -9,7 +9,7 @@ use tracing::error;
use crate::{
actors::vault::{Bootstrap, TryUnseal, Vault},
crypto::{derive_key, encryption::v1::Nonce, shamir},
crypto::{KeyCell, derive_key, encryption::v1::Nonce, shamir},
db::{self, models, schema},
};
@@ -94,14 +94,14 @@ async fn finalize_bootstrap(
let threshold = shamir_threshold(total);
// Generate random 32-byte seal key
let mut seal_key_bytes = vec![0u8; 32];
let mut seal_key_bytes = [0u8; 32];
OsRng.fill_bytes(&mut seal_key_bytes);
// Split seal key into shares using Shamir (OsRng from rand_core 0.6, compatible with vsss-rs)
let shares = shamir::split_key(threshold, total, &seal_key_bytes, OsRng)
.map_err(|e| Error::Shamir(e.to_string()))?;
let seal_key = SafeCell::new(seal_key_bytes);
let seal_key = KeyCell::from(seal_key_bytes);
let mut conn = db.get().await?;
@@ -136,9 +136,7 @@ async fn finalize_bootstrap(
}
vault
.ask(Bootstrap {
seal_key_raw: seal_key,
})
.ask(Bootstrap { seal_key })
.await
.map_err(|err| {
error!(?err, "Vault bootstrap failed");
@@ -189,12 +187,10 @@ async fn finalize_unseal(
let seal_key_bytes =
shamir::combine_shares(&shares).map_err(|e| Error::Shamir(e.to_string()))?;
let seal_key = SafeCell::new(seal_key_bytes);
let seal_key = KeyCell::from(seal_key_bytes);
vault
.ask(TryUnseal {
seal_key_raw: seal_key,
})
.ask(TryUnseal { seal_key })
.await
.map_err(|err| {
error!(?err, "Vault unseal failed");

View File

@@ -215,8 +215,6 @@ mod tests {
},
db::{self, schema},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::{Error, Integrable, sign_entity, verify_entity};
#[derive(Clone, arbiter_macros::Hashable)]
struct DummyEntity {
@@ -235,7 +233,7 @@ mod tests {
);
actor
.ask(Bootstrap {
seal_key_raw: SafeCell::new([0u8; 32].to_vec()),
seal_key: crate::crypto::KeyCell::from([0u8; 32]),
})
.await
.unwrap();

View File

@@ -21,6 +21,15 @@ impl From<SafeCell<Key>> for KeyCell {
Self(value)
}
}
impl From<[u8; 32]> for KeyCell {
fn from(bytes: [u8; 32]) -> Self {
let cell = SafeCell::new_inline_default(|key: &mut Key| {
key.copy_from_slice(&bytes);
});
Self(cell)
}
}
impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
type Error = ();

View File

@@ -13,15 +13,17 @@ pub enum ShamirError {
pub fn split_key(
threshold: usize,
total: usize,
key: &[u8],
key: &[u8; 32],
rng: impl rand_core::RngCore + rand_core::CryptoRng,
) -> Result<Vec<Vec<u8>>, ShamirError> {
Gf256::split_array(threshold, total, key, rng)
Gf256::split_array(threshold, total, key.as_slice(), rng)
.map_err(|e| ShamirError::Split(format!("{e:?}")))
}
/// Reconstruct the secret from `threshold` or more shares.
pub fn combine_shares(shares: &[Vec<u8>]) -> Result<Vec<u8>, ShamirError> {
Gf256::combine_array(shares)
.map_err(|e| ShamirError::Combine(format!("{e:?}")))
pub fn combine_shares(shares: &[Vec<u8>]) -> Result<[u8; 32], ShamirError> {
let bytes = Gf256::combine_array(shares)
.map_err(|e| ShamirError::Combine(format!("{e:?}")))?;
<[u8; 32]>::try_from(bytes.as_slice())
.map_err(|_| ShamirError::Combine("unexpected reconstructed key length".to_owned()))
}

View File

@@ -5,7 +5,7 @@ use crate::{
vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events},
vault_coordinator::{ContributeBootstrap, ContributeUnseal, StartBootstrap},
},
crypto::integrity::{self},
crypto::{KeyCell, integrity::{self}},
db::DatabasePool,
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
@@ -102,11 +102,9 @@ impl VaultGate {
nonce: &[u8],
ciphertext: &[u8],
associated_data: &[u8],
) -> Result<SafeCell<Vec<u8>>, ()> {
) -> Result<KeyCell, ()> {
let nonce = XNonce::from_slice(nonce);
let cipher = XChaCha20Poly1305::new(secret.as_bytes().into());
let mut key_buffer = SafeCell::new(ciphertext.to_vec());
let decryption_result = key_buffer.write_inline(|write_handle| {
@@ -114,7 +112,9 @@ impl VaultGate {
});
match decryption_result {
Ok(()) => Ok(key_buffer),
Ok(()) => KeyCell::try_from(key_buffer).map_err(|()| {
error!("Decrypted key material has unexpected length");
}),
Err(err) => {
error!(?err, "Failed to decrypt encrypted key material");
Err(())
@@ -122,6 +122,7 @@ impl VaultGate {
}
}
}
#[messages(enum)]
impl VaultGate {
#[message]
@@ -155,17 +156,14 @@ impl VaultGate {
return Err(Error::State);
};
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data)
else {
let Ok(seal_key) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) else {
return Err(Error::InvalidKey);
};
match self
.actors
.vault
.ask(TryUnseal {
seal_key_raw: seal_key_buffer,
})
.ask(TryUnseal { seal_key })
.await
{
Ok(()) => {
@@ -195,17 +193,14 @@ impl VaultGate {
return Err(Error::State);
};
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data)
else {
let Ok(seal_key) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) else {
return Err(Error::InvalidKey);
};
match self
.actors
.vault
.ask(Bootstrap {
seal_key_raw: seal_key_buffer,
})
.ask(Bootstrap { seal_key })
.await
{
Ok(()) => {
@@ -255,7 +250,6 @@ impl VaultGate {
&mut self,
passphrase: Vec<u8>,
) -> Result<bool, Error> {
use arbiter_crypto::safecell::SafeCell;
let passphrase_cell = SafeCell::new(passphrase);
self.actors
.vault_coordinator
@@ -272,7 +266,6 @@ impl VaultGate {
&mut self,
passphrase: Vec<u8>,
) -> Result<bool, Error> {
use arbiter_crypto::safecell::SafeCell;
let passphrase_cell = SafeCell::new(passphrase);
self.actors
.vault_coordinator