diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c7eec65 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Non-sensitive info +DTIM__DEFAULT__ADDRESS="0.0.0.0" +DTIM__DEFAULT__PORT=3030 +DTIM__DEFAULT__LOG_LEVEL="info" + +# Sensitive info (.env only) +DTIM__DEFAULT__STORAGE__DATABASE_URL="postgres://user:pass@localhost/db" +DTIM__DEFAULT__WATCHERS__VIRUSTOTAL_API_KEY="your_api_key" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 45e2938..2d89324 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -619,6 +619,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dtim" version = "0.1.0" @@ -630,11 +636,13 @@ dependencies = [ "base64 0.22.1", "chrono", "config", + "dotenvy", "ed25519-dalek", "env_logger", "hex", "http-body-util", "log", + "once_cell", "regex", "rustls", "serde", diff --git a/Cargo.toml b/Cargo.toml index 71ee87a..dc1789d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,4 +25,6 @@ ed25519-dalek = { version = "2.1", features = ["rand_core"] } sha2 = "0.10" hex = "0.4" async-trait = "0.1" -http-body-util = "0.1" \ No newline at end of file +http-body-util = "0.1" +dotenvy = "0.15" +once_cell = "1.21" diff --git a/src/api.rs b/src/api.rs index 97e0c3f..b2732a6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -13,8 +13,7 @@ use ed25519_dalek::VerifyingKey; use http_body_util::BodyExt as _; use rustls::ServerConfig; use serde::Serialize; -use std::sync::Arc; -use std::{io, str::FromStr}; +use std::{str::FromStr as _, sync::Arc}; use tokio::sync::Mutex; use crate::{ @@ -31,7 +30,6 @@ use crate::{ #[derive(Clone)] pub struct AppState { pub node: Arc>, - pub mesh_identity: MeshIdentity, pub key_mgr: SymmetricKeyManager, } @@ -83,7 +81,7 @@ async fn authorize_client_node( if valid { Some(MeshIdentity::Remote { id: node_id, - verifying_key, + verifying_key: Box::new(verifying_key), }) } else { None @@ -94,10 +92,9 @@ pub async fn start_server( node: Arc>, key_mgr: SymmetricKeyManager, config: Arc, + address: String, port: u16, ) -> Result<(), Box> { - let mesh_identity = node.lock().await.identity().clone(); - let app = Router::new() // Peer registry endpoints .route("/api/v1/echo", post(echo_handler)) @@ -128,24 +125,15 @@ pub async fn start_server( "/taxii2/root/collections/{id}/objects/", post(taxii_post_objects_handler), ) - .with_state(Arc::new(AppState { - node, - mesh_identity, - key_mgr, - })) + .with_state(Arc::new(AppState { node, key_mgr })) .layer(middleware::from_fn(auth)); - let addr = format!("0.0.0.0:{}", port).parse()?; + let addr = format!("{}:{}", address, port).parse()?; let tls_config = RustlsConfig::from_config(config); axum_server::bind_rustls(addr, tls_config) .serve(app.into_make_service()) .await - .map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("Failed to start server: {}", e), - ) - })?; + .map_err(|e| std::io::Error::other(format!("Failed to start server: {}", e)))?; Ok(()) } diff --git a/src/crypto/mesh_identity.rs b/src/crypto/mesh_identity.rs index e65c4b6..bf41808 100644 --- a/src/crypto/mesh_identity.rs +++ b/src/crypto/mesh_identity.rs @@ -13,12 +13,12 @@ pub const PUBLIC_KEY_PATH: &str = "data/keys/mesh.pub"; pub enum MeshIdentity { Local { id: String, - verifying_key: VerifyingKey, - signing_key: SigningKey, + verifying_key: Box, + signing_key: Box, }, Remote { id: String, - verifying_key: VerifyingKey, + verifying_key: Box, }, } @@ -36,8 +36,8 @@ impl MeshIdentity { .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; Ok(Self::Local { id: MeshIdentity::derive_hex_id(&pub_bytes), - signing_key, - verifying_key, + signing_key: Box::new(signing_key), + verifying_key: Box::new(verifying_key), }) } @@ -73,7 +73,7 @@ impl MeshIdentity { pub fn derive_hex_id(pubkey_bytes: &[u8; 32]) -> String { let hash = Sha256::digest(pubkey_bytes); - hex::encode(&hash) + hex::encode(hash) } pub fn id(&self) -> &String { diff --git a/src/crypto/symmetric_key_manager.rs b/src/crypto/symmetric_key_manager.rs index a88257c..4265314 100644 --- a/src/crypto/symmetric_key_manager.rs +++ b/src/crypto/symmetric_key_manager.rs @@ -30,7 +30,7 @@ impl SymmetricKeyManager { fn generate_key() -> Key { let mut key_bytes = [0u8; 32]; OsRng.fill_bytes(&mut key_bytes); - Key::::from_slice(&key_bytes).clone() + Key::::from_slice(&key_bytes).to_owned() } pub fn rotate_key(&mut self) { @@ -39,7 +39,7 @@ impl SymmetricKeyManager { .unwrap() .as_secs(); if now - self.key_rotation_time >= self.rotation_interval { - self.previous_key = Some(self.current_key.clone()); + self.previous_key = Some(self.current_key); self.current_key = Self::generate_key(); self.key_rotation_time = now; } diff --git a/src/logging.rs b/src/logging.rs index eda7fd5..9a46c47 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,7 +1,7 @@ use chrono::Utc; use log::{error, Level, LevelFilter, Metadata, Record}; use std::fs::{self, OpenOptions}; -use std::io::{self, Write}; +use std::io::Write as _; use std::path::PathBuf; use crate::crypto::SymmetricKeyManager; @@ -18,7 +18,7 @@ impl EncryptedLogger { log_path: PathBuf, key_mgr: SymmetricKeyManager, level: LevelFilter, - ) -> io::Result { + ) -> std::io::Result { if let Some(parent) = log_path.parent() { fs::create_dir_all(parent)?; } @@ -29,13 +29,14 @@ impl EncryptedLogger { }) } - pub fn write_log(&mut self, level: Level, message: &str) -> io::Result<()> { + pub fn write_log(&mut self, level: Level, message: &str) -> std::io::Result<()> { let timestamp = Utc::now().to_rfc3339(); let log_entry = format!("[{}] [{}] {}\n", timestamp, level, message); - let (ciphertext, nonce, mac) = self.key_mgr.encrypt(log_entry.as_bytes()).map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Encryption failed: {}", e)) - })?; + let (ciphertext, nonce, mac) = self + .key_mgr + .encrypt(log_entry.as_bytes()) + .map_err(|e| std::io::Error::other(format!("Encryption failed: {}", e)))?; let encrypted_entry = format!("{}\n{}\n{}\n", ciphertext, nonce, mac); let filename = format!("{}.log", Utc::now().format("%Y-%m-%d")); @@ -52,7 +53,7 @@ impl EncryptedLogger { Ok(()) } - pub fn read_logs(&self, date: &str) -> io::Result> { + pub fn read_logs(&self, date: &str) -> std::io::Result> { let filename = format!("{}.log", date); let log_file = self.log_path.join(filename); @@ -65,15 +66,15 @@ impl EncryptedLogger { let mut lines = content.lines().peekable(); while lines.peek().is_some() { - let ciphertext = lines - .next() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid log format"))?; - let nonce = lines - .next() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid log format"))?; - let mac = lines - .next() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid log format"))?; + let ciphertext = lines.next().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid log format") + })?; + let nonce = lines.next().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid log format") + })?; + let mac = lines.next().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid log format") + })?; match self.key_mgr.decrypt(ciphertext, nonce, mac) { Ok(decrypted) => { diff --git a/src/main.rs b/src/main.rs index 27ddb53..34f317d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod uuid; use axum::body::Body; use base64::prelude::BASE64_STANDARD; use base64::Engine; +use crypto::MeshIdentity; use http_body_util::BodyExt as _; use log::LevelFilter; use models::{IndicatorType, ThreatIndicator}; @@ -75,22 +76,27 @@ fn make_server_config(settings: &settings::Settings) -> Arc Result<(), Box> { init_logging(); let settings = Settings::new()?; - let mesh_identity = crypto::MeshIdentity::load_or_generate()?; let mut key_mgr = crypto::SymmetricKeyManager::new(settings.tls.key_rotation_days); let tls_config = make_server_config(&settings); let logger = logging::EncryptedLogger::new( settings.storage.encrypted_logs_path.clone(), key_mgr.clone(), - LevelFilter::from_str(&settings.log_level).unwrap(), + LevelFilter::from_str(&settings.log_level).unwrap_or_else(|_| { + log::warn!( + "Invalid log level: {}, defaulting to Info", + settings.log_level + ); + LevelFilter::Info + }), )?; - let node = node::Node::new(mesh_identity.clone(), logger, settings.privacy); + let node = node::Node::new(logger, settings.privacy)?; let id = node.get_id(); println!("Node ID: {:?}", id); - let base64_pubkey = BASE64_STANDARD.encode(mesh_identity.verifying_key().to_bytes()); + let base64_pubkey = BASE64_STANDARD.encode(node.identity().verifying_key().to_bytes()); let mut data = NodePeer { id: id.to_string(), @@ -107,8 +113,11 @@ async fn main() -> Result<(), Box> { .expect("Failed to collect body") .to_bytes(); - let signature = - crypto::MeshIdentity::sign(mesh_identity.signing_key().unwrap().clone(), &bytes); + let signing_key = node + .identity() + .signing_key() + .ok_or_else(|| std::io::Error::other("Failed to get signing key".to_string()))?; + let signature = MeshIdentity::sign(signing_key.clone(), &bytes); data.set_signature(signature); @@ -137,8 +146,9 @@ async fn main() -> Result<(), Box> { node.bootstrap_peers(settings.network.init_peers.clone()); } - let server_handle = - tokio::spawn(async move { api::start_server(node, key_mgr, tls_config, 3030).await }); + let server_handle = tokio::spawn(async move { + api::start_server(node, key_mgr, tls_config, settings.address, settings.port).await + }); server_handle.await??; diff --git a/src/models.rs b/src/models.rs index 3fd66af..553db92 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,6 +1,6 @@ use std::{ collections::{BTreeSet, HashMap}, - fmt, io, + fmt, net::IpAddr, }; @@ -69,11 +69,11 @@ impl ThreatIndicator { pub fn encrypt( &self, key_mgr: &mut SymmetricKeyManager, - ) -> Result { + ) -> Result { let serialized = serde_json::to_vec(self).expect("Failed to serialize ThreatIndicator"); - let (ciphertext, nonce, mac) = key_mgr.encrypt(&serialized).map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Encryption failed: {}", e)) - })?; + let (ciphertext, nonce, mac) = key_mgr + .encrypt(&serialized) + .map_err(|e| std::io::Error::other(format!("Encryption failed: {}", e)))?; Ok(EncryptedThreatIndicator { ciphertext, @@ -94,6 +94,7 @@ impl ThreatIndicator { .map_err(|e| format!("Failed to deserialize ThreatIndicator: {}", e)) } + #[allow(unused)] // TODO: implement in watchers pub fn infer_type(value: &str) -> IndicatorType { if let Ok(ip) = value.parse::() { return match ip { diff --git a/src/node.rs b/src/node.rs index c57aabe..c3050c1 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,8 +1,8 @@ use crate::{ - settings::PrivacyConfig, - crypto::MeshIdentity, + crypto::{self, MeshIdentity}, logging::EncryptedLogger, models::{PrivacyLevel, ThreatIndicator, TlpLevel}, + settings::PrivacyConfig, uuid::Uuid, }; use chrono::Utc; @@ -20,8 +20,9 @@ pub struct Node { } impl Node { - pub fn new(identity: MeshIdentity, logger: EncryptedLogger, privacy: PrivacyConfig) -> Self { - Node { + pub fn new(logger: EncryptedLogger, privacy: PrivacyConfig) -> Result { + let identity = crypto::MeshIdentity::load_or_generate()?; + Ok(Node { identity, indicators: HashMap::new(), peers: HashMap::new(), @@ -32,7 +33,7 @@ impl Node { _ => PrivacyLevel::Moderate, }, allow_custom_fields: privacy.allow_custom_fields, - } + }) } pub fn identity(&self) -> &MeshIdentity { @@ -107,8 +108,7 @@ impl Node { let mut stix_indicators: Vec = indicators .iter() - .map(|i| i.to_stix(self.privacy_level, self.allow_custom_fields)) - .filter_map(|i| i) + .filter_map(|i| i.to_stix(self.privacy_level, self.allow_custom_fields)) .collect(); let stix_mds: Vec = indicators @@ -147,14 +147,6 @@ impl NodePeer { &self.endpoint } - pub fn get_public_key(&self) -> &str { - &self.public_key - } - - pub fn get_signature(&self) -> Option<&str> { - self.signature.as_deref() - } - pub fn set_signature(&mut self, signature: String) { self.signature = Some(signature); } diff --git a/src/settings.rs b/src/settings.rs index d5b8787..d5ac94a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -72,10 +72,12 @@ struct Root { } impl Settings { - pub(crate) fn new() -> Result { + pub(crate) fn new() -> Result> { + dotenvy::dotenv_override()?; + let config = config::Config::builder() .add_source(config::File::with_name("config/mesh")) - .add_source(config::Environment::with_prefix("DTIM")) + .add_source(config::Environment::with_prefix("DTIM").separator("__")) .build()?; let root = config.try_deserialize::()?; diff --git a/src/uuid.rs b/src/uuid.rs index e4ac277..cc50089 100644 --- a/src/uuid.rs +++ b/src/uuid.rs @@ -1,11 +1,15 @@ use chrono::{DateTime, Utc}; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display, Formatter}; use std::ops::Deref; use std::str; use std::str::FromStr; +use std::sync::Mutex; use uuid::ContextV7; +static TIMESTAMP_CONTEXT: Lazy> = Lazy::new(|| Mutex::new(ContextV7::new())); + #[derive( Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Hash, )] @@ -55,8 +59,6 @@ impl Deref for Uuid { } impl Uuid { - const TIMESTAMP_CONTEXT: ContextV7 = ContextV7::new(); - /// Generate a new UUID pub fn new() -> Self { Self(uuid::Uuid::now_v7()) @@ -67,21 +69,22 @@ impl Uuid { } /// Generate a new V7 UUID pub fn new_v7_from_datetime(timestamp: DateTime) -> Self { + let ctx = TIMESTAMP_CONTEXT.lock().unwrap(); let ts = uuid::Timestamp::from_unix( - Self::TIMESTAMP_CONTEXT, + &*ctx, timestamp.timestamp() as u64, timestamp.timestamp_subsec_nanos(), ); Self(uuid::Uuid::new_v7(ts)) } /// Convert the Uuid to a raw String - pub fn to_raw(&self) -> String { + pub fn to_raw(self) -> String { self.0.to_string() } } impl Display for Uuid { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self.0.to_string()) + write!(f, "{}", self.0) } }