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

View File

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

View File

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

View File

@@ -21,6 +21,15 @@ impl From<SafeCell<Key>> for KeyCell {
Self(value) 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 { impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
type Error = (); type Error = ();

View File

@@ -13,15 +13,17 @@ pub enum ShamirError {
pub fn split_key( pub fn split_key(
threshold: usize, threshold: usize,
total: usize, total: usize,
key: &[u8], key: &[u8; 32],
rng: impl rand_core::RngCore + rand_core::CryptoRng, rng: impl rand_core::RngCore + rand_core::CryptoRng,
) -> Result<Vec<Vec<u8>>, ShamirError> { ) -> 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:?}"))) .map_err(|e| ShamirError::Split(format!("{e:?}")))
} }
/// Reconstruct the secret from `threshold` or more shares. /// Reconstruct the secret from `threshold` or more shares.
pub fn combine_shares(shares: &[Vec<u8>]) -> Result<Vec<u8>, ShamirError> { pub fn combine_shares(shares: &[Vec<u8>]) -> Result<[u8; 32], ShamirError> {
Gf256::combine_array(shares) let bytes = Gf256::combine_array(shares)
.map_err(|e| ShamirError::Combine(format!("{e:?}"))) .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::{self, Bootstrap, GetState, TryUnseal, VaultState, events},
vault_coordinator::{ContributeBootstrap, ContributeUnseal, StartBootstrap}, vault_coordinator::{ContributeBootstrap, ContributeUnseal, StartBootstrap},
}, },
crypto::integrity::{self}, crypto::{KeyCell, integrity::{self}},
db::DatabasePool, db::DatabasePool,
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
@@ -102,11 +102,9 @@ impl VaultGate {
nonce: &[u8], nonce: &[u8],
ciphertext: &[u8], ciphertext: &[u8],
associated_data: &[u8], associated_data: &[u8],
) -> Result<SafeCell<Vec<u8>>, ()> { ) -> Result<KeyCell, ()> {
let nonce = XNonce::from_slice(nonce); let nonce = XNonce::from_slice(nonce);
let cipher = XChaCha20Poly1305::new(secret.as_bytes().into()); let cipher = XChaCha20Poly1305::new(secret.as_bytes().into());
let mut key_buffer = SafeCell::new(ciphertext.to_vec()); let mut key_buffer = SafeCell::new(ciphertext.to_vec());
let decryption_result = key_buffer.write_inline(|write_handle| { let decryption_result = key_buffer.write_inline(|write_handle| {
@@ -114,7 +112,9 @@ impl VaultGate {
}); });
match decryption_result { match decryption_result {
Ok(()) => Ok(key_buffer), Ok(()) => KeyCell::try_from(key_buffer).map_err(|()| {
error!("Decrypted key material has unexpected length");
}),
Err(err) => { Err(err) => {
error!(?err, "Failed to decrypt encrypted key material"); error!(?err, "Failed to decrypt encrypted key material");
Err(()) Err(())
@@ -122,6 +122,7 @@ impl VaultGate {
} }
} }
} }
#[messages(enum)] #[messages(enum)]
impl VaultGate { impl VaultGate {
#[message] #[message]
@@ -155,17 +156,14 @@ impl VaultGate {
return Err(Error::State); return Err(Error::State);
}; };
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) let Ok(seal_key) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) else {
else {
return Err(Error::InvalidKey); return Err(Error::InvalidKey);
}; };
match self match self
.actors .actors
.vault .vault
.ask(TryUnseal { .ask(TryUnseal { seal_key })
seal_key_raw: seal_key_buffer,
})
.await .await
{ {
Ok(()) => { Ok(()) => {
@@ -195,17 +193,14 @@ impl VaultGate {
return Err(Error::State); return Err(Error::State);
}; };
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) let Ok(seal_key) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) else {
else {
return Err(Error::InvalidKey); return Err(Error::InvalidKey);
}; };
match self match self
.actors .actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap { seal_key })
seal_key_raw: seal_key_buffer,
})
.await .await
{ {
Ok(()) => { Ok(()) => {
@@ -255,7 +250,6 @@ impl VaultGate {
&mut self, &mut self,
passphrase: Vec<u8>, passphrase: Vec<u8>,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
use arbiter_crypto::safecell::SafeCell;
let passphrase_cell = SafeCell::new(passphrase); let passphrase_cell = SafeCell::new(passphrase);
self.actors self.actors
.vault_coordinator .vault_coordinator
@@ -272,7 +266,6 @@ impl VaultGate {
&mut self, &mut self,
passphrase: Vec<u8>, passphrase: Vec<u8>,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
use arbiter_crypto::safecell::SafeCell;
let passphrase_cell = SafeCell::new(passphrase); let passphrase_cell = SafeCell::new(passphrase);
self.actors self.actors
.vault_coordinator .vault_coordinator

View File

@@ -1,8 +1,5 @@
use super::common::ChannelTransport; use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::authn::{self, AuthChallenge, CLIENT_CONTEXT};
authn::{self, AuthChallenge, CLIENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, ClientMetadata,
transport::{Receiver, Sender}, transport::{Receiver, Sender},
@@ -100,7 +97,7 @@ async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new([0u8; 32].to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();

View File

@@ -2,7 +2,6 @@
dead_code, dead_code,
reason = "Common test utilities that may not be used in every test" reason = "Common test utilities that may not be used in every test"
)] )]
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_proto::transport::{Bi, Error, Receiver, Sender}; use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
actors::{GlobalActors, vault::Vault}, actors::{GlobalActors, vault::Vault},
@@ -19,7 +18,7 @@ pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
.await .await
.unwrap(); .unwrap();
actor actor
.bootstrap(SafeCell::new([0u8; 32].to_vec())) .bootstrap(arbiter_server::crypto::KeyCell::from([0u8; 32]))
.await .await
.unwrap(); .unwrap();
actor actor

View File

@@ -1,8 +1,5 @@
use super::common::ChannelTransport; use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT};
authn::{self, AuthChallenge, OPERATOR_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::transport::{Error as TransportError, Receiver, Sender}; use arbiter_proto::transport::{Error as TransportError, Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
@@ -157,7 +154,7 @@ pub async fn bootstrap_token_auth() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new([0u8; 32].to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();
@@ -275,7 +272,7 @@ pub async fn challenge_auth() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new([0u8; 32].to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();
@@ -361,7 +358,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new([0u8; 32].to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();
@@ -434,7 +431,7 @@ pub async fn challenge_auth_rejects_invalid_signature() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new([0u8; 32].to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();

View File

@@ -1,7 +1,4 @@
use arbiter_crypto::{ use arbiter_crypto::authn;
authn,
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_server::{ use arbiter_server::{
actors::{ actors::{
GlobalActors, GlobalActors,
@@ -34,7 +31,7 @@ async fn setup_sealed_gate(
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(seal_key.to_vec()), seal_key: arbiter_server::crypto::KeyCell::from(*seal_key),
}) })
.await .await
.unwrap(); .unwrap();

View File

@@ -166,7 +166,7 @@ async fn decrypt_roundtrip_after_high_concurrency() {
.await .await
.unwrap(); .unwrap();
decryptor decryptor
.try_unseal(SafeCell::new([0u8; 32].to_vec())) .try_unseal(arbiter_server::crypto::KeyCell::from([0u8; 32]))
.await .await
.unwrap(); .unwrap();

View File

@@ -5,7 +5,7 @@ use arbiter_server::{
GlobalActors, GlobalActors,
vault::{Error, Vault}, vault::{Error, Vault},
}, },
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, crypto::{KeyCell, encryption::v1::{Nonce, ROOT_KEY_TAG}},
db::{self, models, schema}, db::{self, models, schema},
}; };
@@ -20,7 +20,7 @@ async fn test_bootstrap() {
.await .await
.unwrap(); .unwrap();
let seal_key = SafeCell::new([0u8; 32].to_vec()); let seal_key = KeyCell::from([0u8; 32]);
actor.bootstrap(seal_key).await.unwrap(); actor.bootstrap(seal_key).await.unwrap();
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
@@ -43,7 +43,7 @@ async fn test_bootstrap_rejects_double() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
let seal_key2 = SafeCell::new([0u8; 32].to_vec()); let seal_key2 = KeyCell::from([0u8; 32]);
let err = actor.bootstrap(seal_key2).await.unwrap_err(); let err = actor.bootstrap(seal_key2).await.unwrap_err();
assert!(matches!(err, Error::AlreadyBootstrapped)); assert!(matches!(err, Error::AlreadyBootstrapped));
} }
@@ -105,7 +105,7 @@ async fn test_unseal_correct_password() {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await .await
.unwrap(); .unwrap();
let seal_key = SafeCell::new([0u8; 32].to_vec()); let seal_key = KeyCell::from([0u8; 32]);
actor.try_unseal(seal_key).await.unwrap(); actor.try_unseal(seal_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap(); let mut decrypted = actor.decrypt(aead_id).await.unwrap();
@@ -129,11 +129,11 @@ async fn test_unseal_wrong_then_correct_password() {
.await .await
.unwrap(); .unwrap();
let bad_key = SafeCell::new([1u8; 32].to_vec()); let bad_key = KeyCell::from([1u8; 32]);
let err = actor.try_unseal(bad_key).await.unwrap_err(); let err = actor.try_unseal(bad_key).await.unwrap_err();
assert!(matches!(err, Error::InvalidKey)); assert!(matches!(err, Error::InvalidKey));
let good_key = SafeCell::new([0u8; 32].to_vec()); let good_key = KeyCell::from([0u8; 32]);
actor.try_unseal(good_key).await.unwrap(); actor.try_unseal(good_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap(); let mut decrypted = actor.decrypt(aead_id).await.unwrap();