Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/nvisy-cli/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ impl Cli {
service.postgres.into(),
service.nats.into(),
service.session_keys.into(),
service.master_key.into(),
service.crypto.into(),
service.health.into(),
webhook,
)
Expand Down
12 changes: 6 additions & 6 deletions crates/nvisy-cli/src/config/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::time::Duration;
use clap::Args;
use nvisy_nats::NatsConfig;
use nvisy_postgres::PgConfig;
use nvisy_server::service::{HealthConfig, MasterKeyConfig, SessionKeysConfig};
use nvisy_server::service::{CryptoConfig, HealthConfig, SessionKeysConfig};

/// Aggregated external-service arguments (database, NATS, auth keys).
#[derive(Debug, Clone, Args)]
Expand All @@ -28,7 +28,7 @@ pub struct ServiceArgs {

/// Master encryption key path.
#[clap(flatten)]
pub master_key: MasterKeyArgs,
pub crypto: CryptoArgs,

/// Health monitoring configuration.
#[clap(flatten)]
Expand Down Expand Up @@ -152,9 +152,9 @@ impl From<SessionKeysArgs> for SessionKeysConfig {
}
}

/// Master encryption key path arguments.
/// Encryption key path arguments.
#[derive(Debug, Clone, Args)]
pub struct MasterKeyArgs {
pub struct CryptoArgs {
/// File path to the 32-byte master encryption key.
#[arg(
long,
Expand All @@ -164,8 +164,8 @@ pub struct MasterKeyArgs {
pub key_path: PathBuf,
}

impl From<MasterKeyArgs> for MasterKeyConfig {
fn from(args: MasterKeyArgs) -> Self {
impl From<CryptoArgs> for CryptoConfig {
fn from(args: CryptoArgs) -> Self {
Self {
key_path: args.key_path,
}
Expand Down
27 changes: 6 additions & 21 deletions crates/nvisy-server/src/handler/connections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ use crate::handler::request::{
WorkspacePathParams,
};
use crate::handler::response::{Connection, ConnectionsPage, ErrorResponse};
use crate::handler::{Error, ErrorKind, Result};
use crate::service::crypto::encrypt_json;
use crate::service::{MasterKey, ServiceState};
use crate::handler::{Error, Result};
use crate::service::{CryptoService, ServiceState};

/// Tracing target for workspace connection operations.
const TRACING_TARGET: &str = "nvisy_server::handler::connections";
Expand All @@ -48,7 +47,7 @@ const TRACING_TARGET: &str = "nvisy_server::handler::connections";
)]
async fn create_connection(
State(pg_client): State<PgClient>,
State(master_key): State<MasterKey>,
State(crypto): State<CryptoService>,
AuthState(auth_state): AuthState,
Path(path_params): Path<WorkspacePathParams>,
ValidateJson(request): ValidateJson<CreateConnection>,
Expand All @@ -65,14 +64,7 @@ async fn create_connection(
)
.await?;

let workspace_key = master_key.derive_workspace_key(path_params.workspace_id);
let encrypted_data = encrypt_json(&workspace_key, &request.data).map_err(
|e: crate::service::crypto::CryptoError| {
ErrorKind::InternalServerError
.with_message("Failed to encrypt connection data")
.with_context(e.to_string())
},
)?;
let encrypted_data = crypto.encrypt_json(path_params.workspace_id, &request.data)?;

let new_connection = NewWorkspaceConnection {
workspace_id: path_params.workspace_id,
Expand Down Expand Up @@ -233,7 +225,7 @@ fn read_connection_docs(op: TransformOperation) -> TransformOperation {
)]
async fn update_connection(
State(pg_client): State<PgClient>,
State(master_key): State<MasterKey>,
State(crypto): State<CryptoService>,
AuthState(auth_state): AuthState,
Path(path_params): Path<ConnectionPathParams>,
ValidateJson(request): ValidateJson<UpdateConnection>,
Expand All @@ -255,14 +247,7 @@ async fn update_connection(

let encrypted_data = request
.data
.map(|data| {
let workspace_key = master_key.derive_workspace_key(existing.workspace_id);
encrypt_json(&workspace_key, &data).map_err(|e: crate::service::crypto::CryptoError| {
ErrorKind::InternalServerError
.with_message("Failed to encrypt connection data")
.with_context(e.to_string())
})
})
.map(|data| crypto.encrypt_json(existing.workspace_id, &data))
.transpose()?;

let update_data = UpdateWorkspaceConnection {
Expand Down
26 changes: 6 additions & 20 deletions crates/nvisy-server/src/handler/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ use crate::handler::request::{ContextPathParams, CursorPagination, WorkspacePath
use crate::handler::response::{Context, ContextsPage, ErrorResponse};
use crate::handler::{Error, ErrorKind, Result};
use crate::middleware::DEFAULT_MAX_FILE_BODY_SIZE;
use crate::service::crypto::encrypt;
use crate::service::{MasterKey, ServiceState};
use crate::service::{CryptoService, ServiceState};

/// Tracing target for workspace context operations.
const TRACING_TARGET: &str = "nvisy_server::handler::contexts";
Expand All @@ -44,7 +43,7 @@ const TRACING_TARGET: &str = "nvisy_server::handler::contexts";
async fn create_context(
State(pg_client): State<PgClient>,
State(nats_client): State<NatsClient>,
State(master_key): State<MasterKey>,
State(crypto): State<CryptoService>,
AuthState(auth_state): AuthState,
Path(path_params): Path<WorkspacePathParams>,
Multipart(mut multipart): Multipart,
Expand Down Expand Up @@ -119,14 +118,8 @@ async fn create_context(
let content = file_content
.ok_or_else(|| ErrorKind::BadRequest.with_message("Missing required 'file' field"))?;

// Encrypt the content with workspace-derived key
let workspace_key = master_key.derive_workspace_key(path_params.workspace_id);
let encrypted_content =
encrypt(&workspace_key, &content).map_err(|e: crate::service::crypto::CryptoError| {
ErrorKind::InternalServerError
.with_message("Failed to encrypt context content")
.with_context(e.to_string())
})?;
// Encrypt the content with the workspace-derived key
let encrypted_content = crypto.encrypt(path_params.workspace_id, &content)?;

// Generate the object store key
let context_id = Uuid::now_v7();
Expand Down Expand Up @@ -288,7 +281,7 @@ fn read_context_docs(op: TransformOperation) -> TransformOperation {
async fn update_context(
State(pg_client): State<PgClient>,
State(nats_client): State<NatsClient>,
State(master_key): State<MasterKey>,
State(crypto): State<CryptoService>,
AuthState(auth_state): AuthState,
Path(path_params): Path<ContextPathParams>,
Multipart(mut multipart): Multipart,
Expand Down Expand Up @@ -357,14 +350,7 @@ async fn update_context(

// If file content was provided, encrypt and store new content
if let Some(content) = file_content {
let workspace_key = master_key.derive_workspace_key(existing.workspace_id);
let encrypted_content = encrypt(&workspace_key, &content).map_err(
|e: crate::service::crypto::CryptoError| {
ErrorKind::InternalServerError
.with_message("Failed to encrypt context content")
.with_context(e.to_string())
},
)?;
let encrypted_content = crypto.encrypt(existing.workspace_id, &content)?;

let context_key = ContextKey::new(existing.workspace_id, existing.id);
let context_store = nats_client
Expand Down
25 changes: 25 additions & 0 deletions crates/nvisy-server/src/handler/error/crypto_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Crypto error to HTTP error conversion.
//!
//! Converts a [`CryptoError`] into the handler HTTP error. Encryption and
//! decryption failures are internal faults, so they map to a 500 with the
//! crypto error attached as context.

use super::http_error::{Error as HttpError, ErrorKind};
use crate::service::crypto::CryptoError;

/// Tracing target for crypto error conversions.
const TRACING_TARGET: &str = "nvisy_server::handler::crypto";

impl From<CryptoError> for HttpError<'static> {
fn from(error: CryptoError) -> Self {
tracing::error!(
target: TRACING_TARGET,
error = %error,
"Cryptographic operation failed"
);

ErrorKind::InternalServerError
.with_message("Cryptographic operation failed")
.with_context(error.to_string())
}
}
1 change: 1 addition & 0 deletions crates/nvisy-server/src/handler/error/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! [`Error`], [`ErrorKind`] and [`Result`].

mod crypto_error;
mod http_error;
mod nats_error;
mod pg_account;
Expand Down
14 changes: 7 additions & 7 deletions crates/nvisy-server/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ mod test {
use nvisy_webhook::reqwest::ReqwestClient;

use crate::handler::{CustomRoutes, routes};
use crate::service::{HealthConfig, MasterKeyConfig, ServiceState, SessionKeysConfig};
use crate::service::{CryptoConfig, HealthConfig, ServiceState, SessionKeysConfig};

/// Builds the service sub-configs from the environment for integration tests.
fn configs_from_env()
-> anyhow::Result<(PgConfig, NatsConfig, SessionKeysConfig, MasterKeyConfig)> {
fn configs_from_env() -> anyhow::Result<(PgConfig, NatsConfig, SessionKeysConfig, CryptoConfig)>
{
dotenvy::dotenv().ok();
let var = std::env::var;

Expand All @@ -143,24 +143,24 @@ mod test {
encoding_key: var("AUTH_PRIVATE_PEM_FILEPATH")?.into(),
};

let master_key = MasterKeyConfig {
let crypto = CryptoConfig {
key_path: var("ENCRYPTION_KEY_FILEPATH")?.into(),
};

Ok((postgres, nats, session, master_key))
Ok((postgres, nats, session, crypto))
}

/// Returns a new [`TestServer`] with the given router.
pub async fn create_test_server_with_router(
router: impl Fn(ServiceState) -> ApiRouter<ServiceState>,
) -> anyhow::Result<TestServer> {
let (postgres, nats, session, master_key) = configs_from_env()?;
let (postgres, nats, session, crypto) = configs_from_env()?;
let webhook_service = ReqwestClient::default().into_service();
let state = ServiceState::from_config(
postgres,
nats,
session,
master_key,
crypto,
HealthConfig::default(),
webhook_service,
)
Expand Down
9 changes: 2 additions & 7 deletions crates/nvisy-server/src/service/crypto/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::fmt;

use hkdf::Hkdf;
#[cfg(test)]
use rand::Rng;
use sha2::Sha256;
use uuid::Uuid;
Expand Down Expand Up @@ -38,6 +39,7 @@ impl EncryptionKey {
}

/// Generates a new random encryption key using a cryptographically secure RNG.
#[cfg(test)]
#[must_use]
pub fn generate() -> Self {
let mut bytes = [0u8; KEY_SIZE];
Expand All @@ -52,13 +54,6 @@ impl EncryptionKey {
&self.bytes
}

/// Consumes the key and returns the raw bytes.
#[inline]
#[must_use]
pub fn into_bytes(self) -> [u8; KEY_SIZE] {
self.bytes
}

/// Derives a workspace-specific encryption key using HKDF-SHA256.
///
/// This creates a unique encryption key for each workspace by combining
Expand Down
6 changes: 4 additions & 2 deletions crates/nvisy-server/src/service/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
mod cipher;
mod error;
mod key;
mod service;

pub use cipher::{decrypt, decrypt_json, encrypt, encrypt_json};
pub(crate) use cipher::{decrypt, decrypt_json, encrypt, encrypt_json};
pub use error::{CryptoError, CryptoResult};
pub use key::EncryptionKey;
pub(crate) use key::EncryptionKey;
pub use service::{CryptoConfig, CryptoService};
Loading