fix(clippy): apply auto-fixable linting suggestions

This commit is contained in:
Clippy Bot
2026-06-23 18:47:23 +00:00
parent 4fd75701c7
commit cc21036448
425 changed files with 79190 additions and 79190 deletions

View File

@@ -1,4 +1,4 @@
mod common;
#[path = "client/auth.rs"]
mod auth;
mod common;
#[path = "client/auth.rs"]
mod auth;

View File

@@ -1,408 +1,408 @@
use super::common::ChannelTransport;
use arbiter_crypto::{
authn::{self, AuthChallenge, CLIENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::{
ClientMetadata,
transport::{Receiver, Sender},
};
use arbiter_server::{
actors::{GlobalActors, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
};
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair};
fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> ClientMetadata {
ClientMetadata {
name: name.to_owned(),
description: description.map(str::to_owned),
version: version.map(str::to_owned),
}
}
fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> {
<SigningKey<MlDsa87> as Keypair>::verifying_key(key)
}
async fn insert_registered_client(
db: &db::DatabasePool,
actors: &GlobalActors,
pubkey: VerifyingKey<MlDsa87>,
metadata: &ClientMetadata,
) {
use arbiter_server::db::schema::{client_metadata, program_client};
let mut conn = db.get().await.unwrap();
let metadata_id: i32 = insert_into(client_metadata::table)
.values((
client_metadata::name.eq(&metadata.name),
client_metadata::description.eq(&metadata.description),
client_metadata::version.eq(&metadata.version),
))
.returning(client_metadata::id)
.get_result(&mut conn)
.await
.unwrap();
let client_id: i32 = insert_into(program_client::table)
.values((
program_client::public_key.eq(pubkey.encode().0.to_vec()),
program_client::metadata_id.eq(metadata_id),
))
.returning(program_client::id)
.get_result(&mut conn)
.await
.unwrap();
integrity::sign_entity(
&mut conn,
&actors.vault,
&ClientCredentials {
pubkey: pubkey.into(),
},
client_id,
)
.await
.unwrap();
}
fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -> authn::Signature {
let challenge = challenge.format();
key.signing_key()
.sign_deterministic(&challenge, CLIENT_CONTEXT)
.unwrap()
.into()
}
async fn insert_bootstrap_sentinel_operator(db: &db::DatabasePool) {
let mut conn = db.get().await.unwrap();
let sentinel_key = verifying_key(&MlDsa87::key_gen(&mut rand::rng()))
.encode()
.0
.to_vec();
insert_into(schema::operator_client::table)
.values((schema::operator_client::public_key.eq(sentinel_key),))
.execute(&mut conn)
.await
.unwrap();
}
async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
insert_bootstrap_sentinel_operator(db).await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
.await
.unwrap();
actors
}
#[tokio::test]
#[test_log::test]
pub async fn unregistered_pubkey_rejected() {
let db = db::create_test_pool().await;
let (server_transport, mut test_transport) = ChannelTransport::new();
let actors = spawn_test_actors(&db).await;
let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
let new_key = MlDsa87::key_gen(&mut rand::rng());
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
})
.await
.unwrap();
// Auth fails, connect_client returns, transport drops
task.await.unwrap();
}
#[tokio::test]
#[test_log::test]
pub async fn challenge_auth() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
Box::pin(insert_registered_client(
&db,
&actors,
verifying_key(&new_key),
&metadata("client", Some("desc"), Some("1.0.0")),
))
.await;
let (server_transport, mut test_transport) = ChannelTransport::new();
let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
// Send challenge request
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
})
.await
.unwrap();
// Read the challenge response
let response = test_transport
.recv()
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge,
other @ auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got {other:?}"),
},
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
// Sign the challenge and send solution
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive auth success");
match response {
Ok(auth::Outbound::AuthSuccess) => {}
Ok(other) => panic!("Expected AuthSuccess, got {other:?}"),
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}
// Auth completes, session spawned
task.await.unwrap();
}
#[tokio::test]
#[test_log::test]
pub async fn metadata_unchanged_does_not_append_history() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
let requested = metadata("client", Some("desc"), Some("1.0.0"));
Box::pin(insert_registered_client(
&db,
&actors,
verifying_key(&new_key),
&requested,
))
.await;
let props = ClientConnection::new(db.clone(), actors);
let (server_transport, mut test_transport) = ChannelTransport::new();
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: requested,
})
.await
.unwrap();
let response = test_transport.recv().await.unwrap().unwrap();
let challenge = match response {
auth::Outbound::AuthChallenge { challenge } => challenge,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
};
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
.unwrap();
let _ = test_transport.recv().await.unwrap();
task.await.unwrap();
{
use arbiter_server::db::schema::{client_metadata, client_metadata_history};
let mut conn = db.get().await.unwrap();
let metadata_count: i64 = client_metadata::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let history_count: i64 = client_metadata_history::table
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(metadata_count, 1);
assert_eq!(history_count, 0);
}
}
#[tokio::test]
#[test_log::test]
pub async fn metadata_change_appends_history_and_repoints_binding() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
Box::pin(insert_registered_client(
&db,
&actors,
verifying_key(&new_key),
&metadata("client", Some("old"), Some("1.0.0")),
))
.await;
let props = ClientConnection::new(db.clone(), actors);
let (server_transport, mut test_transport) = ChannelTransport::new();
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: metadata("client", Some("new"), Some("2.0.0")),
})
.await
.unwrap();
let response = test_transport.recv().await.unwrap().unwrap();
let challenge = match response {
auth::Outbound::AuthChallenge { challenge } => challenge,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
};
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
.unwrap();
drop(test_transport.recv().await.unwrap());
task.await.unwrap();
{
use arbiter_server::db::schema::{
client_metadata, client_metadata_history, program_client,
};
let mut conn = db.get().await.unwrap();
let metadata_count: i64 = client_metadata::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let history_count: i64 = client_metadata_history::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let metadata_id = program_client::table
.select(program_client::metadata_id)
.first::<i32>(&mut conn)
.await
.unwrap();
let current = client_metadata::table
.find(metadata_id)
.select((
client_metadata::name,
client_metadata::description.nullable(),
client_metadata::version.nullable(),
))
.first::<(String, Option<String>, Option<String>)>(&mut conn)
.await
.unwrap();
assert_eq!(metadata_count, 2);
assert_eq!(history_count, 1);
assert_eq!(
current,
(
"client".to_owned(),
Some("new".to_owned()),
Some("2.0.0".to_owned())
)
);
}
}
#[tokio::test]
#[test_log::test]
pub async fn challenge_auth_rejects_integrity_tag_mismatch() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
let requested = metadata("client", Some("desc"), Some("1.0.0"));
{
use arbiter_server::db::schema::{client_metadata, program_client};
let mut conn = db.get().await.unwrap();
let metadata_id: i32 = insert_into(client_metadata::table)
.values((
client_metadata::name.eq(&requested.name),
client_metadata::description.eq(&requested.description),
client_metadata::version.eq(&requested.version),
))
.returning(client_metadata::id)
.get_result(&mut conn)
.await
.unwrap();
insert_into(program_client::table)
.values((
program_client::public_key.eq(verifying_key(&new_key).encode().0.to_vec()),
program_client::metadata_id.eq(metadata_id),
))
.execute(&mut conn)
.await
.unwrap();
}
let (server_transport, mut test_transport) = ChannelTransport::new();
let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: requested,
})
.await
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive auth rejection");
assert!(matches!(response, Err(auth::Error::IntegrityCheckFailed)));
task.await.unwrap();
}
use super::common::ChannelTransport;
use arbiter_crypto::{
authn::{self, AuthChallenge, CLIENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::{
ClientMetadata,
transport::{Receiver, Sender},
};
use arbiter_server::{
actors::{GlobalActors, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
};
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair};
fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> ClientMetadata {
ClientMetadata {
name: name.to_owned(),
description: description.map(str::to_owned),
version: version.map(str::to_owned),
}
}
fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> {
<SigningKey<MlDsa87> as Keypair>::verifying_key(key)
}
async fn insert_registered_client(
db: &db::DatabasePool,
actors: &GlobalActors,
pubkey: VerifyingKey<MlDsa87>,
metadata: &ClientMetadata,
) {
use arbiter_server::db::schema::{client_metadata, program_client};
let mut conn = db.get().await.unwrap();
let metadata_id: i32 = insert_into(client_metadata::table)
.values((
client_metadata::name.eq(&metadata.name),
client_metadata::description.eq(&metadata.description),
client_metadata::version.eq(&metadata.version),
))
.returning(client_metadata::id)
.get_result(&mut conn)
.await
.unwrap();
let client_id: i32 = insert_into(program_client::table)
.values((
program_client::public_key.eq(pubkey.encode().0.to_vec()),
program_client::metadata_id.eq(metadata_id),
))
.returning(program_client::id)
.get_result(&mut conn)
.await
.unwrap();
integrity::sign_entity(
&mut conn,
&actors.vault,
&ClientCredentials {
pubkey: pubkey.into(),
},
client_id,
)
.await
.unwrap();
}
fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -> authn::Signature {
let challenge = challenge.format();
key.signing_key()
.sign_deterministic(&challenge, CLIENT_CONTEXT)
.unwrap()
.into()
}
async fn insert_bootstrap_sentinel_operator(db: &db::DatabasePool) {
let mut conn = db.get().await.unwrap();
let sentinel_key = verifying_key(&MlDsa87::key_gen(&mut rand::rng()))
.encode()
.0
.to_vec();
insert_into(schema::operator_client::table)
.values((schema::operator_client::public_key.eq(sentinel_key),))
.execute(&mut conn)
.await
.unwrap();
}
async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
insert_bootstrap_sentinel_operator(db).await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
.await
.unwrap();
actors
}
#[tokio::test]
#[test_log::test]
pub async fn unregistered_pubkey_rejected() {
let db = db::create_test_pool().await;
let (server_transport, mut test_transport) = ChannelTransport::new();
let actors = spawn_test_actors(&db).await;
let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
let new_key = MlDsa87::key_gen(&mut rand::rng());
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
})
.await
.unwrap();
// Auth fails, connect_client returns, transport drops
task.await.unwrap();
}
#[tokio::test]
#[test_log::test]
pub async fn challenge_auth() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
Box::pin(insert_registered_client(
&db,
&actors,
verifying_key(&new_key),
&metadata("client", Some("desc"), Some("1.0.0")),
))
.await;
let (server_transport, mut test_transport) = ChannelTransport::new();
let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
// Send challenge request
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
})
.await
.unwrap();
// Read the challenge response
let response = test_transport
.recv()
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge,
other @ auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got {other:?}"),
},
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
// Sign the challenge and send solution
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive auth success");
match response {
Ok(auth::Outbound::AuthSuccess) => {}
Ok(other) => panic!("Expected AuthSuccess, got {other:?}"),
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}
// Auth completes, session spawned
task.await.unwrap();
}
#[tokio::test]
#[test_log::test]
pub async fn metadata_unchanged_does_not_append_history() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
let requested = metadata("client", Some("desc"), Some("1.0.0"));
Box::pin(insert_registered_client(
&db,
&actors,
verifying_key(&new_key),
&requested,
))
.await;
let props = ClientConnection::new(db.clone(), actors);
let (server_transport, mut test_transport) = ChannelTransport::new();
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: requested,
})
.await
.unwrap();
let response = test_transport.recv().await.unwrap().unwrap();
let challenge = match response {
auth::Outbound::AuthChallenge { challenge } => challenge,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
};
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
.unwrap();
let _ = test_transport.recv().await.unwrap();
task.await.unwrap();
{
use arbiter_server::db::schema::{client_metadata, client_metadata_history};
let mut conn = db.get().await.unwrap();
let metadata_count: i64 = client_metadata::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let history_count: i64 = client_metadata_history::table
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(metadata_count, 1);
assert_eq!(history_count, 0);
}
}
#[tokio::test]
#[test_log::test]
pub async fn metadata_change_appends_history_and_repoints_binding() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
Box::pin(insert_registered_client(
&db,
&actors,
verifying_key(&new_key),
&metadata("client", Some("old"), Some("1.0.0")),
))
.await;
let props = ClientConnection::new(db.clone(), actors);
let (server_transport, mut test_transport) = ChannelTransport::new();
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: metadata("client", Some("new"), Some("2.0.0")),
})
.await
.unwrap();
let response = test_transport.recv().await.unwrap().unwrap();
let challenge = match response {
auth::Outbound::AuthChallenge { challenge } => challenge,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
};
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
.unwrap();
drop(test_transport.recv().await.unwrap());
task.await.unwrap();
{
use arbiter_server::db::schema::{
client_metadata, client_metadata_history, program_client,
};
let mut conn = db.get().await.unwrap();
let metadata_count: i64 = client_metadata::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let history_count: i64 = client_metadata_history::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let metadata_id = program_client::table
.select(program_client::metadata_id)
.first::<i32>(&mut conn)
.await
.unwrap();
let current = client_metadata::table
.find(metadata_id)
.select((
client_metadata::name,
client_metadata::description.nullable(),
client_metadata::version.nullable(),
))
.first::<(String, Option<String>, Option<String>)>(&mut conn)
.await
.unwrap();
assert_eq!(metadata_count, 2);
assert_eq!(history_count, 1);
assert_eq!(
current,
(
"client".to_owned(),
Some("new".to_owned()),
Some("2.0.0".to_owned())
)
);
}
}
#[tokio::test]
#[test_log::test]
pub async fn challenge_auth_rejects_integrity_tag_mismatch() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
let requested = metadata("client", Some("desc"), Some("1.0.0"));
{
use arbiter_server::db::schema::{client_metadata, program_client};
let mut conn = db.get().await.unwrap();
let metadata_id: i32 = insert_into(client_metadata::table)
.values((
client_metadata::name.eq(&requested.name),
client_metadata::description.eq(&requested.description),
client_metadata::version.eq(&requested.version),
))
.returning(client_metadata::id)
.get_result(&mut conn)
.await
.unwrap();
insert_into(program_client::table)
.values((
program_client::public_key.eq(verifying_key(&new_key).encode().0.to_vec()),
program_client::metadata_id.eq(metadata_id),
))
.execute(&mut conn)
.await
.unwrap();
}
let (server_transport, mut test_transport) = ChannelTransport::new();
let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
metadata: requested,
})
.await
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive auth rejection");
assert!(matches!(response, Err(auth::Error::IntegrityCheckFailed)));
task.await.unwrap();
}

View File

@@ -1,90 +1,90 @@
#![allow(
dead_code,
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_server::{
actors::{GlobalActors, vault::Vault},
db::{self, schema},
};
use async_trait::async_trait;
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use tokio::sync::mpsc;
pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
actor
.bootstrap(SafeCell::new(b"test-seal-key".to_vec()))
.await
.unwrap();
actor
}
pub(crate) async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
let mut conn = db.get().await.unwrap();
let id = schema::arbiter_settings::table
.select(schema::arbiter_settings::root_key_id)
.first::<Option<i32>>(&mut conn)
.await
.unwrap();
id.expect("root_key_id should be set after bootstrap")
}
pub(crate) struct ChannelTransport<T, Y> {
receiver: mpsc::Receiver<T>,
sender: mpsc::Sender<Y>,
}
impl<T, Y> ChannelTransport<T, Y> {
pub(crate) fn new() -> (Self, ChannelTransport<Y, T>) {
let (tx1, rx1) = mpsc::channel(10);
let (tx2, rx2) = mpsc::channel(10);
(
Self {
receiver: rx1,
sender: tx2,
},
ChannelTransport {
receiver: rx2,
sender: tx1,
},
)
}
}
#[async_trait]
impl<T, Y> Sender<Y> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
async fn send(&mut self, item: Y) -> Result<(), Error> {
self.sender
.send(item)
.await
.map_err(|_| Error::ChannelClosed)
}
}
#[async_trait]
impl<T, Y> Receiver<T> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
async fn recv(&mut self) -> Option<T> {
self.receiver.recv().await
}
}
impl<T, Y> Bi<T, Y> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
}
#![allow(
dead_code,
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_server::{
actors::{GlobalActors, vault::Vault},
db::{self, schema},
};
use async_trait::async_trait;
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use tokio::sync::mpsc;
pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
actor
.bootstrap(SafeCell::new(b"test-seal-key".to_vec()))
.await
.unwrap();
actor
}
pub(crate) async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
let mut conn = db.get().await.unwrap();
let id = schema::arbiter_settings::table
.select(schema::arbiter_settings::root_key_id)
.first::<Option<i32>>(&mut conn)
.await
.unwrap();
id.expect("root_key_id should be set after bootstrap")
}
pub(crate) struct ChannelTransport<T, Y> {
receiver: mpsc::Receiver<T>,
sender: mpsc::Sender<Y>,
}
impl<T, Y> ChannelTransport<T, Y> {
pub(crate) fn new() -> (Self, ChannelTransport<Y, T>) {
let (tx1, rx1) = mpsc::channel(10);
let (tx2, rx2) = mpsc::channel(10);
(
Self {
receiver: rx1,
sender: tx2,
},
ChannelTransport {
receiver: rx2,
sender: tx1,
},
)
}
}
#[async_trait]
impl<T, Y> Sender<Y> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
async fn send(&mut self, item: Y) -> Result<(), Error> {
self.sender
.send(item)
.await
.map_err(|_| Error::ChannelClosed)
}
}
#[async_trait]
impl<T, Y> Receiver<T> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
async fn recv(&mut self) -> Option<T> {
self.receiver.recv().await
}
}
impl<T, Y> Bi<T, Y> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
}

View File

@@ -1,6 +1,6 @@
mod common;
#[path = "operator/auth.rs"]
mod auth;
#[path = "operator/unseal.rs"]
mod unseal;
mod common;
#[path = "operator/auth.rs"]
mod auth;
#[path = "operator/unseal.rs"]
mod unseal;

File diff suppressed because it is too large Load Diff

View File

@@ -1,167 +1,167 @@
use arbiter_crypto::{
authn,
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_server::{
actors::{
GlobalActors,
vault::{Bootstrap, Seal},
},
db,
peers::operator::{
Credentials,
vault_gate::{
Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate,
},
},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use kameo::actor::Spawn as _;
use tokio::sync::oneshot;
use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_gate(
seal_key: &[u8],
) -> (
db::DatabasePool,
kameo::actor::ActorRef<VaultGate>,
oneshot::Receiver<Result<(), VaultGateError>>,
) {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(seal_key.to_vec()),
})
.await
.unwrap();
actors.vault.ask(Seal).await.unwrap();
let (promotion_tx, promotion_rx) = oneshot::channel();
let pubkey = authn::SigningKey::generate().public_key();
let auth_creds = Credentials { id: 1, pubkey };
let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx));
(db, gate, promotion_rx)
}
async fn client_dh_encrypt(
gate: &kameo::actor::ActorRef<VaultGate>,
key_to_send: &[u8],
) -> HandleUnsealEncryptedKey {
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
let response = gate
.ask(HandleHandshake {
client_pubkey: client_public,
})
.await
.unwrap();
let server_pubkey = response.server_pubkey;
let shared_secret = client_secret.diffie_hellman(&server_pubkey);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let nonce = XNonce::from([0u8; 24]);
let associated_data = b"unseal";
let mut ciphertext = key_to_send.to_vec();
cipher
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
.unwrap();
HandleUnsealEncryptedKey {
nonce: nonce.to_vec(),
ciphertext,
associated_data: associated_data.to_vec(),
}
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_success() {
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, seal_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(response, Ok(())));
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_wrong_seal_key() {
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
VaultGateError::InvalidKey
))
));
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_corrupted_ciphertext() {
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
gate.ask(HandleHandshake {
client_pubkey: client_public,
})
.await
.unwrap();
let response = gate
.ask(HandleUnsealEncryptedKey {
nonce: vec![0u8; 24],
ciphertext: vec![0u8; 32],
associated_data: vec![],
})
.await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
VaultGateError::InvalidKey
))
));
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_retry_after_invalid_key() {
let seal_key = b"real-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
{
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
VaultGateError::InvalidKey
))
));
}
{
let encrypted_key = client_dh_encrypt(&gate, seal_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(response, Ok(())));
}
}
use arbiter_crypto::{
authn,
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_server::{
actors::{
GlobalActors,
vault::{Bootstrap, Seal},
},
db,
peers::operator::{
Credentials,
vault_gate::{
Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate,
},
},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use kameo::actor::Spawn as _;
use tokio::sync::oneshot;
use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_gate(
seal_key: &[u8],
) -> (
db::DatabasePool,
kameo::actor::ActorRef<VaultGate>,
oneshot::Receiver<Result<(), VaultGateError>>,
) {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(seal_key.to_vec()),
})
.await
.unwrap();
actors.vault.ask(Seal).await.unwrap();
let (promotion_tx, promotion_rx) = oneshot::channel();
let pubkey = authn::SigningKey::generate().public_key();
let auth_creds = Credentials { id: 1, pubkey };
let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx));
(db, gate, promotion_rx)
}
async fn client_dh_encrypt(
gate: &kameo::actor::ActorRef<VaultGate>,
key_to_send: &[u8],
) -> HandleUnsealEncryptedKey {
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
let response = gate
.ask(HandleHandshake {
client_pubkey: client_public,
})
.await
.unwrap();
let server_pubkey = response.server_pubkey;
let shared_secret = client_secret.diffie_hellman(&server_pubkey);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let nonce = XNonce::from([0u8; 24]);
let associated_data = b"unseal";
let mut ciphertext = key_to_send.to_vec();
cipher
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
.unwrap();
HandleUnsealEncryptedKey {
nonce: nonce.to_vec(),
ciphertext,
associated_data: associated_data.to_vec(),
}
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_success() {
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, seal_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(response, Ok(())));
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_wrong_seal_key() {
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
VaultGateError::InvalidKey
))
));
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_corrupted_ciphertext() {
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
gate.ask(HandleHandshake {
client_pubkey: client_public,
})
.await
.unwrap();
let response = gate
.ask(HandleUnsealEncryptedKey {
nonce: vec![0u8; 24],
ciphertext: vec![0u8; 32],
associated_data: vec![],
})
.await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
VaultGateError::InvalidKey
))
));
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_retry_after_invalid_key() {
let seal_key = b"real-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
{
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
VaultGateError::InvalidKey
))
));
}
{
let encrypted_key = client_dh_encrypt(&gate, seal_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(response, Ok(())));
}
}

View File

@@ -1,8 +1,8 @@
mod common;
#[path = "vault/concurrency.rs"]
mod concurrency;
#[path = "vault/lifecycle.rs"]
mod lifecycle;
#[path = "vault/storage.rs"]
mod storage;
mod common;
#[path = "vault/concurrency.rs"]
mod concurrency;
#[path = "vault/lifecycle.rs"]
mod lifecycle;
#[path = "vault/storage.rs"]
mod storage;

View File

@@ -1,177 +1,177 @@
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{
GlobalActors,
vault::{CreateNew, Error, Vault},
},
db::{self, models, schema},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
use diesel_async::RunQueryDsl;
use kameo::actor::{ActorRef, Spawn as _};
use std::collections::{HashMap, HashSet};
use tokio::task::JoinSet;
async fn write_concurrently(
actor: ActorRef<Vault>,
prefix: &'static str,
count: usize,
) -> Vec<(i32, Vec<u8>)> {
let mut set = JoinSet::new();
for i in 0..count {
let actor = actor.clone();
set.spawn(async move {
let plaintext = format!("{prefix}-{i}").into_bytes();
let id = actor
.ask(CreateNew {
plaintext: SafeCell::new(plaintext.clone()),
})
.await
.unwrap();
(id, plaintext)
});
}
let mut out = Vec::with_capacity(count);
while let Some(res) = set.join_next().await {
out.push(res.unwrap());
}
out
}
#[tokio::test]
#[test_log::test]
async fn concurrent_create_new_no_duplicate_nonces_() {
let db = db::create_test_pool().await;
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
let writes = write_concurrently(actor, "nonce-unique", 32).await;
assert_eq!(writes.len(), 32);
let mut conn = db.get().await.unwrap();
let rows: Vec<models::AeadEncrypted> = schema::aead_encrypted::table
.select(models::AeadEncrypted::as_select())
.load(&mut conn)
.await
.unwrap();
assert_eq!(rows.len(), 32);
let nonces: Vec<&Vec<u8>> = rows.iter().map(|r| &r.current_nonce).collect();
let unique: HashSet<&Vec<u8>> = nonces.iter().copied().collect();
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
}
#[tokio::test]
#[test_log::test]
async fn concurrent_create_new_root_nonce_never_moves_backward() {
let db = db::create_test_pool().await;
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
write_concurrently(actor, "root-max", 24).await;
let mut conn = db.get().await.unwrap();
let rows: Vec<models::AeadEncrypted> = schema::aead_encrypted::table
.select(models::AeadEncrypted::as_select())
.load(&mut conn)
.await
.unwrap();
let max_nonce = rows
.iter()
.map(|r| r.current_nonce.clone())
.max()
.expect("at least one row");
let root_row: models::RootKeyHistory = schema::root_key_history::table
.select(models::RootKeyHistory::as_select())
.first(&mut conn)
.await
.unwrap();
assert_eq!(root_row.data_encryption_nonce, max_nonce);
}
#[tokio::test]
#[test_log::test]
async fn insert_failure_does_not_create_partial_row() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let root_key_history_id = common::root_key_history_id(&db).await;
let mut conn = db.get().await.unwrap();
let before_count: i64 = schema::aead_encrypted::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let before_root_nonce: Vec<u8> = schema::root_key_history::table
.filter(schema::root_key_history::id.eq(root_key_history_id))
.select(schema::root_key_history::data_encryption_nonce)
.first(&mut conn)
.await
.unwrap();
sql_query(
"CREATE TRIGGER fail_aead_insert BEFORE INSERT ON aead_encrypted BEGIN SELECT RAISE(ABORT, 'forced test failure'); END;",
)
.execute(&mut conn)
.await
.unwrap();
drop(conn);
let err = actor
.create_new(SafeCell::new(b"should fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::DatabaseTransaction(_)));
let mut conn = db.get().await.unwrap();
sql_query("DROP TRIGGER fail_aead_insert;")
.execute(&mut conn)
.await
.unwrap();
let after_count: i64 = schema::aead_encrypted::table
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(
before_count, after_count,
"failed insert must not create row"
);
let after_root_nonce: Vec<u8> = schema::root_key_history::table
.filter(schema::root_key_history::id.eq(root_key_history_id))
.select(schema::root_key_history::data_encryption_nonce)
.first(&mut conn)
.await
.unwrap();
assert!(
after_root_nonce > before_root_nonce,
"current behavior allows nonce gap on failed insert"
);
}
#[tokio::test]
#[test_log::test]
async fn decrypt_roundtrip_after_high_concurrency() {
let db = db::create_test_pool().await;
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
let writes = write_concurrently(actor, "roundtrip", 40).await;
let expected: HashMap<i32, Vec<u8>> = writes.into_iter().collect();
let mut decryptor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
decryptor
.try_unseal(SafeCell::new(b"test-seal-key".to_vec()))
.await
.unwrap();
for (id, plaintext) in expected {
let mut decrypted = decryptor.decrypt(id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
}
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{
GlobalActors,
vault::{CreateNew, Error, Vault},
},
db::{self, models, schema},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
use diesel_async::RunQueryDsl;
use kameo::actor::{ActorRef, Spawn as _};
use std::collections::{HashMap, HashSet};
use tokio::task::JoinSet;
async fn write_concurrently(
actor: ActorRef<Vault>,
prefix: &'static str,
count: usize,
) -> Vec<(i32, Vec<u8>)> {
let mut set = JoinSet::new();
for i in 0..count {
let actor = actor.clone();
set.spawn(async move {
let plaintext = format!("{prefix}-{i}").into_bytes();
let id = actor
.ask(CreateNew {
plaintext: SafeCell::new(plaintext.clone()),
})
.await
.unwrap();
(id, plaintext)
});
}
let mut out = Vec::with_capacity(count);
while let Some(res) = set.join_next().await {
out.push(res.unwrap());
}
out
}
#[tokio::test]
#[test_log::test]
async fn concurrent_create_new_no_duplicate_nonces_() {
let db = db::create_test_pool().await;
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
let writes = write_concurrently(actor, "nonce-unique", 32).await;
assert_eq!(writes.len(), 32);
let mut conn = db.get().await.unwrap();
let rows: Vec<models::AeadEncrypted> = schema::aead_encrypted::table
.select(models::AeadEncrypted::as_select())
.load(&mut conn)
.await
.unwrap();
assert_eq!(rows.len(), 32);
let nonces: Vec<&Vec<u8>> = rows.iter().map(|r| &r.current_nonce).collect();
let unique: HashSet<&Vec<u8>> = nonces.iter().copied().collect();
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
}
#[tokio::test]
#[test_log::test]
async fn concurrent_create_new_root_nonce_never_moves_backward() {
let db = db::create_test_pool().await;
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
write_concurrently(actor, "root-max", 24).await;
let mut conn = db.get().await.unwrap();
let rows: Vec<models::AeadEncrypted> = schema::aead_encrypted::table
.select(models::AeadEncrypted::as_select())
.load(&mut conn)
.await
.unwrap();
let max_nonce = rows
.iter()
.map(|r| r.current_nonce.clone())
.max()
.expect("at least one row");
let root_row: models::RootKeyHistory = schema::root_key_history::table
.select(models::RootKeyHistory::as_select())
.first(&mut conn)
.await
.unwrap();
assert_eq!(root_row.data_encryption_nonce, max_nonce);
}
#[tokio::test]
#[test_log::test]
async fn insert_failure_does_not_create_partial_row() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let root_key_history_id = common::root_key_history_id(&db).await;
let mut conn = db.get().await.unwrap();
let before_count: i64 = schema::aead_encrypted::table
.count()
.get_result(&mut conn)
.await
.unwrap();
let before_root_nonce: Vec<u8> = schema::root_key_history::table
.filter(schema::root_key_history::id.eq(root_key_history_id))
.select(schema::root_key_history::data_encryption_nonce)
.first(&mut conn)
.await
.unwrap();
sql_query(
"CREATE TRIGGER fail_aead_insert BEFORE INSERT ON aead_encrypted BEGIN SELECT RAISE(ABORT, 'forced test failure'); END;",
)
.execute(&mut conn)
.await
.unwrap();
drop(conn);
let err = actor
.create_new(SafeCell::new(b"should fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::DatabaseTransaction(_)));
let mut conn = db.get().await.unwrap();
sql_query("DROP TRIGGER fail_aead_insert;")
.execute(&mut conn)
.await
.unwrap();
let after_count: i64 = schema::aead_encrypted::table
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(
before_count, after_count,
"failed insert must not create row"
);
let after_root_nonce: Vec<u8> = schema::root_key_history::table
.filter(schema::root_key_history::id.eq(root_key_history_id))
.select(schema::root_key_history::data_encryption_nonce)
.first(&mut conn)
.await
.unwrap();
assert!(
after_root_nonce > before_root_nonce,
"current behavior allows nonce gap on failed insert"
);
}
#[tokio::test]
#[test_log::test]
async fn decrypt_roundtrip_after_high_concurrency() {
let db = db::create_test_pool().await;
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
let writes = write_concurrently(actor, "roundtrip", 40).await;
let expected: HashMap<i32, Vec<u8>> = writes.into_iter().collect();
let mut decryptor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
decryptor
.try_unseal(SafeCell::new(b"test-seal-key".to_vec()))
.await
.unwrap();
for (id, plaintext) in expected {
let mut decrypted = decryptor.decrypt(id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
}

View File

@@ -1,141 +1,141 @@
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{
GlobalActors,
vault::{Error, Vault},
},
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
db::{self, models, schema},
};
use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
#[tokio::test]
#[test_log::test]
async fn bootstrap() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.bootstrap(seal_key).await.unwrap();
let mut conn = db.get().await.unwrap();
let row: models::RootKeyHistory = schema::root_key_history::table
.select(models::RootKeyHistory::as_select())
.first(&mut conn)
.await
.unwrap();
assert_eq!(row.schema_version, 1);
assert_eq!(row.tag, ROOT_KEY_TAG);
assert!(!row.ciphertext.is_empty());
assert!(!row.salt.is_empty());
assert_eq!(row.data_encryption_nonce, Nonce::default().to_vec());
}
#[tokio::test]
#[test_log::test]
async fn bootstrap_rejects_double() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let seal_key2 = SafeCell::new(b"test-seal-key".to_vec());
let err = actor.bootstrap(seal_key2).await.unwrap_err();
assert!(matches!(err, Error::AlreadyBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn create_new_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor
.create_new(SafeCell::new(b"data".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::NotBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn decrypt_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor.decrypt(1).await.unwrap_err();
assert!(matches!(err, Error::NotBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn new_restores_sealed_state() {
let db = db::create_test_pool().await;
let actor = common::bootstrapped_vault(&db).await;
drop(actor);
let mut actor2 = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor2.decrypt(1).await.unwrap_err();
assert!(matches!(err, Error::Sealed));
}
#[tokio::test]
#[test_log::test]
async fn unseal_correct_password() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"survive a restart";
let aead_id = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
drop(actor);
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(seal_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
#[tokio::test]
#[test_log::test]
async fn unseal_wrong_then_correct_password() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"important data";
let aead_id = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
drop(actor);
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let bad_key = SafeCell::new(b"wrong-password".to_vec());
let err = actor.try_unseal(bad_key).await.unwrap_err();
assert!(matches!(err, Error::InvalidKey));
let good_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(good_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{
GlobalActors,
vault::{Error, Vault},
},
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
db::{self, models, schema},
};
use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
#[tokio::test]
#[test_log::test]
async fn bootstrap() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.bootstrap(seal_key).await.unwrap();
let mut conn = db.get().await.unwrap();
let row: models::RootKeyHistory = schema::root_key_history::table
.select(models::RootKeyHistory::as_select())
.first(&mut conn)
.await
.unwrap();
assert_eq!(row.schema_version, 1);
assert_eq!(row.tag, ROOT_KEY_TAG);
assert!(!row.ciphertext.is_empty());
assert!(!row.salt.is_empty());
assert_eq!(row.data_encryption_nonce, Nonce::default().to_vec());
}
#[tokio::test]
#[test_log::test]
async fn bootstrap_rejects_double() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let seal_key2 = SafeCell::new(b"test-seal-key".to_vec());
let err = actor.bootstrap(seal_key2).await.unwrap_err();
assert!(matches!(err, Error::AlreadyBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn create_new_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor
.create_new(SafeCell::new(b"data".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::NotBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn decrypt_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor.decrypt(1).await.unwrap_err();
assert!(matches!(err, Error::NotBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn new_restores_sealed_state() {
let db = db::create_test_pool().await;
let actor = common::bootstrapped_vault(&db).await;
drop(actor);
let mut actor2 = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor2.decrypt(1).await.unwrap_err();
assert!(matches!(err, Error::Sealed));
}
#[tokio::test]
#[test_log::test]
async fn unseal_correct_password() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"survive a restart";
let aead_id = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
drop(actor);
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(seal_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
#[tokio::test]
#[test_log::test]
async fn unseal_wrong_then_correct_password() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"important data";
let aead_id = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
drop(actor);
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let bad_key = SafeCell::new(b"wrong-password".to_vec());
let err = actor.try_unseal(bad_key).await.unwrap_err();
assert!(matches!(err, Error::InvalidKey));
let good_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(good_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}

View File

@@ -1,161 +1,161 @@
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::vault::Error,
crypto::encryption::v1::Nonce,
db::{self, models, schema},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
use diesel_async::RunQueryDsl;
use std::collections::HashSet;
#[tokio::test]
#[test_log::test]
async fn create_decrypt_roundtrip() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"hello arbiter";
let aead_id = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
#[tokio::test]
#[test_log::test]
async fn decrypt_nonexistent_returns_not_found() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let err = actor.decrypt(9999).await.unwrap_err();
assert!(matches!(err, Error::NotFound));
}
#[tokio::test]
#[test_log::test]
async fn ciphertext_differs_across_entries() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"same content";
let id1 = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let id2 = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let mut conn = db.get().await.unwrap();
let row1: models::AeadEncrypted = schema::aead_encrypted::table
.filter(schema::aead_encrypted::id.eq(id1))
.select(models::AeadEncrypted::as_select())
.first(&mut conn)
.await
.unwrap();
let row2: models::AeadEncrypted = schema::aead_encrypted::table
.filter(schema::aead_encrypted::id.eq(id2))
.select(models::AeadEncrypted::as_select())
.first(&mut conn)
.await
.unwrap();
assert_ne!(row1.ciphertext, row2.ciphertext);
let mut d1 = actor.decrypt(id1).await.unwrap();
let mut d2 = actor.decrypt(id2).await.unwrap();
assert_eq!(*d1.read(), plaintext);
assert_eq!(*d2.read(), plaintext);
}
#[tokio::test]
#[test_log::test]
async fn nonce_never_reused() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let n = 5;
for i in 0..n {
actor
.create_new(SafeCell::new(format!("secret {i}").into_bytes()))
.await
.unwrap();
}
let mut conn = db.get().await.unwrap();
let rows: Vec<models::AeadEncrypted> = schema::aead_encrypted::table
.select(models::AeadEncrypted::as_select())
.load(&mut conn)
.await
.unwrap();
assert_eq!(rows.len(), n);
let nonces: Vec<&Vec<u8>> = rows.iter().map(|r| &r.current_nonce).collect();
let unique: HashSet<&Vec<u8>> = nonces.iter().copied().collect();
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
for (i, row) in rows.iter().enumerate() {
let mut expected = Nonce::default();
for _ in 0..=i {
expected.increment();
}
assert_eq!(row.current_nonce, expected.to_vec(), "nonce {i} mismatch");
}
let root_row: models::RootKeyHistory = schema::root_key_history::table
.select(models::RootKeyHistory::as_select())
.first(&mut conn)
.await
.unwrap();
let last_nonce = &rows.last().unwrap().current_nonce;
assert_eq!(&root_row.data_encryption_nonce, last_nonce);
}
#[tokio::test]
#[test_log::test]
async fn broken_db_nonce_format_fails_closed() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let root_key_history_id = common::root_key_history_id(&db).await;
let mut conn = db.get().await.unwrap();
update(
schema::root_key_history::table
.filter(schema::root_key_history::id.eq(root_key_history_id)),
)
.set(schema::root_key_history::data_encryption_nonce.eq(vec![1, 2, 3]))
.execute(&mut conn)
.await
.unwrap();
drop(conn);
let err = actor
.create_new(SafeCell::new(b"must fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::BrokenDatabase));
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let id = actor
.create_new(SafeCell::new(b"decrypt target".to_vec()))
.await
.unwrap();
let mut conn = db.get().await.unwrap();
update(schema::aead_encrypted::table.filter(schema::aead_encrypted::id.eq(id)))
.set(schema::aead_encrypted::current_nonce.eq(vec![7, 8]))
.execute(&mut conn)
.await
.unwrap();
drop(conn);
let err = actor.decrypt(id).await.unwrap_err();
assert!(matches!(err, Error::BrokenDatabase));
}
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::vault::Error,
crypto::encryption::v1::Nonce,
db::{self, models, schema},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
use diesel_async::RunQueryDsl;
use std::collections::HashSet;
#[tokio::test]
#[test_log::test]
async fn create_decrypt_roundtrip() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"hello arbiter";
let aead_id = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
#[tokio::test]
#[test_log::test]
async fn decrypt_nonexistent_returns_not_found() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let err = actor.decrypt(9999).await.unwrap_err();
assert!(matches!(err, Error::NotFound));
}
#[tokio::test]
#[test_log::test]
async fn ciphertext_differs_across_entries() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"same content";
let id1 = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let id2 = actor
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let mut conn = db.get().await.unwrap();
let row1: models::AeadEncrypted = schema::aead_encrypted::table
.filter(schema::aead_encrypted::id.eq(id1))
.select(models::AeadEncrypted::as_select())
.first(&mut conn)
.await
.unwrap();
let row2: models::AeadEncrypted = schema::aead_encrypted::table
.filter(schema::aead_encrypted::id.eq(id2))
.select(models::AeadEncrypted::as_select())
.first(&mut conn)
.await
.unwrap();
assert_ne!(row1.ciphertext, row2.ciphertext);
let mut d1 = actor.decrypt(id1).await.unwrap();
let mut d2 = actor.decrypt(id2).await.unwrap();
assert_eq!(*d1.read(), plaintext);
assert_eq!(*d2.read(), plaintext);
}
#[tokio::test]
#[test_log::test]
async fn nonce_never_reused() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let n = 5;
for i in 0..n {
actor
.create_new(SafeCell::new(format!("secret {i}").into_bytes()))
.await
.unwrap();
}
let mut conn = db.get().await.unwrap();
let rows: Vec<models::AeadEncrypted> = schema::aead_encrypted::table
.select(models::AeadEncrypted::as_select())
.load(&mut conn)
.await
.unwrap();
assert_eq!(rows.len(), n);
let nonces: Vec<&Vec<u8>> = rows.iter().map(|r| &r.current_nonce).collect();
let unique: HashSet<&Vec<u8>> = nonces.iter().copied().collect();
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
for (i, row) in rows.iter().enumerate() {
let mut expected = Nonce::default();
for _ in 0..=i {
expected.increment();
}
assert_eq!(row.current_nonce, expected.to_vec(), "nonce {i} mismatch");
}
let root_row: models::RootKeyHistory = schema::root_key_history::table
.select(models::RootKeyHistory::as_select())
.first(&mut conn)
.await
.unwrap();
let last_nonce = &rows.last().unwrap().current_nonce;
assert_eq!(&root_row.data_encryption_nonce, last_nonce);
}
#[tokio::test]
#[test_log::test]
async fn broken_db_nonce_format_fails_closed() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let root_key_history_id = common::root_key_history_id(&db).await;
let mut conn = db.get().await.unwrap();
update(
schema::root_key_history::table
.filter(schema::root_key_history::id.eq(root_key_history_id)),
)
.set(schema::root_key_history::data_encryption_nonce.eq(vec![1, 2, 3]))
.execute(&mut conn)
.await
.unwrap();
drop(conn);
let err = actor
.create_new(SafeCell::new(b"must fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::BrokenDatabase));
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await;
let id = actor
.create_new(SafeCell::new(b"decrypt target".to_vec()))
.await
.unwrap();
let mut conn = db.get().await.unwrap();
update(schema::aead_encrypted::table.filter(schema::aead_encrypted::id.eq(id)))
.set(schema::aead_encrypted::current_nonce.eq(vec![7, 8]))
.execute(&mut conn)
.await
.unwrap();
drop(conn);
let err = actor.decrypt(id).await.unwrap_err();
assert!(matches!(err, Error::BrokenDatabase));
}