diff --git a/.vscode/launch.json b/.vscode/launch.json index 07dd6ed..ff5613d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,6 +22,30 @@ "args": [], "cwd": "${workspaceFolder}" }, + { + "type": "lldb", + "request": "launch", + "name": "Debug (auto-decrypt)", + "program": "${workspaceFolder}/target/debug/auto-decrypt", + "cargo": { + "args": [ + "build", + "--bin=auto-decrypt", + "--package=auto-decrypt" + ], + "filter": { + "name": "auto-decrypt", + "kind": "bin" + } + }, + "args": [ + "--get-key", + "hello_world", + "--provider", + "debug_config/enc/test_provider.toml" + ], + "cwd": "${workspaceFolder}" + }, { "type": "lldb", "request": "launch", diff --git a/Cargo.lock b/Cargo.lock index 05c86f6..a85801c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,7 @@ dependencies = [ "reqwest", "rocket", "rocket_dyn_templates", + "rusqlite", "serde", "toml", "url", @@ -945,6 +946,18 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1263,12 +1276,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -1640,7 +1671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.4", "serde", ] @@ -1822,10 +1853,11 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -2756,6 +2788,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "russh" version = "0.51.1" diff --git a/Cargo.toml b/Cargo.toml index 6350b1e..d2da3f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ diesel-derive-enum = { version = "3.0.0-beta.1", features = ["sqlite"] } argon2 = "0.5.3" base64 = "0.22.1" chacha20poly1305 = "0.10.1" +rusqlite = { version = "0.32", features = ["bundled"] } \ No newline at end of file diff --git a/shell.nix b/shell.nix index 5083e83..8f814d1 100644 --- a/shell.nix +++ b/shell.nix @@ -1,5 +1,5 @@ let - # Import nixpkgs and add the mozilla overlay + # Import nixpkgs and add the Mozilla overlay nixpkgs = import { overlays = [ (import (builtins.fetchTarball { @@ -12,21 +12,31 @@ nixpkgs.mkShell { buildInputs = [ ((nixpkgs.rustChannelOf { channel = "stable"; - date = null; # null means latest + date = null; # null = latest stable channel }).rust.override { extensions = [ "rust-src" "rust-analysis" ]; }) + nixpkgs.openssl nixpkgs.pkg-config nixpkgs.sqlite - nixpkgs.libpq + nixpkgs.libpq + nixpkgs.lldb nixpkgs.libmysqlclient ]; shellHook = '' export TMPDIR=/tmp export PATH=$PATH:$HOME/.cargo/bin - cargo install diesel_cli - ''; -} + # Ensure dynamic linker can find required libs (SQLite, OpenSSL, PostgreSQL, MySQL) + export LD_LIBRARY_PATH=${nixpkgs.openssl.out}/lib:${nixpkgs.sqlite.out}/lib:${nixpkgs.libpq.out}/lib:${nixpkgs.libmysqlclient.out}/lib:$LD_LIBRARY_PATH + + + # Install diesel_cli if not already installed + if ! command -v diesel >/dev/null 2>&1; then + echo "Installing diesel_cli (SQLite)..." + cargo install diesel_cli --no-default-features --features "sqlite" + fi + ''; +} diff --git a/src/api/consent.rs b/src/api/consent.rs index 6ca04f7..2f23f59 100644 --- a/src/api/consent.rs +++ b/src/api/consent.rs @@ -30,7 +30,7 @@ pub(crate) fn consent(consent_id: i32, db_conn: &State) -> Result, rejected: bool, } @@ -86,7 +86,7 @@ pub(crate) async fn consent_post<'a>( Ok(msg) => (RecordStates::Accepted, UserMessage { level: MessageType::Success, message: msg }), Err(err) => (RecordStates::Failed, UserMessage { level: MessageType::Danger, message: err.to_string() }), }; - + println!("{:?}", message.message); record = db_conn.update_provider_state(record.id, new_state); Ok(render_consent(&record, Some(message))) } diff --git a/src/api/guards.rs b/src/api/guards.rs index 86bf053..a1ad809 100644 --- a/src/api/guards.rs +++ b/src/api/guards.rs @@ -25,7 +25,7 @@ impl<'r> FromRequest<'r> for APIProviderRequest<'r> { let path = req.uri().path().segments(); let share_name: &str = path.get(path.len() - 1).unwrap_or(&""); - let access_key: &str = &get_query_value(&req.uri().query(), "trigger_key") + let access_key: &str = &get_query_value(&req.uri().query(), "access_key") .unwrap_or_default(); if share_name.is_empty() { @@ -38,7 +38,7 @@ impl<'r> FromRequest<'r> for APIProviderRequest<'r> { Outcome::Success(APIProviderRequest { share: service.1, name: service.0 }) } else { Outcome::Error(HttpResult::ShareNotFound(format!( - "Share '{}' not found.", + "Share '{}' not found or access_key invalide.", share_name )).into()) } diff --git a/src/app_config/mod.rs b/src/app_config/mod.rs index d36a4a6..e79f678 100644 --- a/src/app_config/mod.rs +++ b/src/app_config/mod.rs @@ -25,6 +25,7 @@ pub(crate) struct AppConfig { pub(crate) user_confirmation_expiration: i64, pub(crate) db_file: String, pub(crate) crypto_key: String, + pub(crate) base_config_dir: String, pub(crate) beggars: HashMap, // We request to unlock owned services pub(crate) providers: HashMap, // We offer to unlock these services @@ -42,6 +43,7 @@ impl Default for AppConfig { platform: Platform::OMV, ssh_known_host_file: "".to_string(), db_file: "/var/auto-decrypt/db.sqlite".to_string(), + base_config_dir: "".to_string(), } } } @@ -58,12 +60,24 @@ impl AppConfig { user_confirmation_expiration: toml_config.user_confirmation_expiration.unwrap_or(default.user_confirmation_expiration), ssh_known_host_file: toml_config.ssh_known_host_file.unwrap_or(default.ssh_known_host_file), db_file: toml_config.db_file.unwrap_or(default.db_file), + base_config_dir: base_config_dir.to_str().unwrap_or(default.base_config_dir.as_str()).to_string(), beggars: toml_config.beggars.unwrap_or(default.beggars), - providers: toml_config.providers.and_then(|providers| - Some(providers.into_iter().map(|(name, service)| - (name, service.to_internal(base_config_dir).expect("Err")) - ).collect::>()) - ).unwrap_or(default.providers), + providers: toml_config.providers + .map(|providers| { + providers + .into_iter() + .filter_map(|(name, service)| { + match service.to_internal(base_config_dir) { + Ok(internal_service) => Some((name, internal_service)), + Err(err) => { + eprintln!("Error loading provider {}: {}. Skipping this provider.", name, err); + None + } + } + }) + .collect::>() + }) + .unwrap_or(default.providers), }; } return default; @@ -83,7 +97,6 @@ impl AppConfig { eprintln!("Failed to read config file: {err}. Using default configuration."); return String::new(); // Return empty string to fall back to default }); - let basic_config = AppConfig::from_toml( toml::from_str::(&contents) .map_err(|err| { diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index d402ded..38af178 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -8,13 +8,12 @@ use argon2::{ use crate::errors::AutoDecryptError; use base64::prelude::*; use chacha20poly1305::{ - aead::{Aead, AeadCore, KeyInit, OsRng as AeadOsRng}, - ChaCha20Poly1305, Nonce + ChaCha20Poly1305, Nonce, aead::{Aead, AeadCore, KeyInit, OsRng as AeadOsRng} }; use crate::app_config::CONFIG; -fn hash_hey(input: &str) -> Result<(String), AutoDecryptError> { +pub(crate) fn hash_hey(input: &str) -> Result<(String), AutoDecryptError> { let argon2 = Argon2::default(); let salt = SaltString::generate(&mut OsRng); Ok(argon2.hash_password(input.as_bytes(), &salt).map_err( @@ -22,33 +21,52 @@ fn hash_hey(input: &str) -> Result<(String), AutoDecryptError> { .to_string()) } -fn derive_key(password: &str, salt: &str) -> Result<[u8; 32], AutoDecryptError> { +pub(crate) fn derive_key(password: &str, salt: &str) -> Result<[u8; 32], AutoDecryptError> { let mut output = [0u8; 32]; Argon2::default().hash_password_into(password.as_bytes(), salt.as_bytes(), &mut output).map_err( |e| AutoDecryptError::CryptoError { comment: (e.to_string()) })?; Ok(output) } -fn encrypt_data(data: &str, key: &[u8; 32], salt: Option<[u8; 96]>) -> Result<(String, [u8; 96]), AutoDecryptError> { +pub(crate) fn encrypt_data(data: &str, key: &[u8; 32], salt: Option<[u8; 12]>) -> Result<(String, String), AutoDecryptError> { let cipher = ChaCha20Poly1305::new(key.into()); let salt = match salt { Some(s) => Nonce::clone_from_slice(&s), None => ChaCha20Poly1305::generate_nonce(&mut AeadOsRng), }; - let salt_arr: [u8; 96] = salt.as_slice().try_into().expect("Nonce should be 96 bytes"); + let salt_arr: [u8; 12] = salt.as_slice().try_into().expect("Nonce should be 12 bytes"); let ciphertext = cipher.encrypt(&salt, data.as_bytes()).map_err( |e| AutoDecryptError::APIError { comment: (e.to_string()) })?; - Ok((BASE64_STANDARD.encode(ciphertext), salt_arr)) + Ok((BASE64_STANDARD.encode(ciphertext), BASE64_STANDARD.encode(salt_arr))) } -fn decrypt_data(encrypted_data: &str, key: &[u8; 32], salt: &[u8; 96]) -> Result { +pub(crate) fn decrypt_data(salt_data: &str, key: &[u8; 32]) -> Result { let cipher = ChaCha20Poly1305::new(key.into()); - let nonce = Nonce::from_slice(salt); - let ciphertext = BASE64_STANDARD.decode(encrypted_data).map_err( - |e| AutoDecryptError::APIError { comment: (e.to_string()) })?; - let plaintext = cipher.decrypt(nonce, ciphertext.as_ref()).map_err( + + let data = salt_data.split("::").collect::>(); + let salt_part = read_salt_from_string(data.get(0).ok_or_else(|| AutoDecryptError::ConfigurationError { + comment: "Invalid encrypted data format: missing salt".to_string(), + })?)?; + let encrypted_data = data.get(1).ok_or_else(|| AutoDecryptError::ConfigurationError { + comment: "Invalid encrypted data format: missing ciphertext".to_string(), + })?; + + let nonce = Nonce::from_slice(&salt_part); + + let cypthertext_bytes = BASE64_STANDARD.decode(encrypted_data).map_err( + |e| AutoDecryptError::CryptoError { comment: (e.to_string()) })?; + let plaintext = cipher.decrypt(nonce, cypthertext_bytes.as_ref()).map_err( |e| AutoDecryptError::CryptoError { comment: (e.to_string()) })?; Ok(String::from_utf8(plaintext).map_err( |e| AutoDecryptError::CryptoError { comment: (e.to_string()) })?) +} + +pub(crate) fn read_salt_from_string(contents: &str) -> Result<[u8; 12], AutoDecryptError> { + + let salt = BASE64_STANDARD.decode(contents).map_err( + |e| AutoDecryptError::CryptoError { comment: (e.to_string()) })?; + let salt_arr: [u8; 12] = salt.as_slice().try_into().map_err( + |_| AutoDecryptError::CryptoError { comment: ("Invalid salt length".to_string()) })?; + Ok(salt_arr) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2d8c92e..0103596 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,18 @@ mod services; mod app_config; mod api; mod crypto; +use std::process::exit; + +use base64::Engine; +use base64::prelude::BASE64_STANDARD; use clap::Parser; +use diesel::expression::is_aggregate::No; +mod utils; use crate::app_config::{init_config, CONFIG}; +use crate::crypto::derive_key; +use crate::utils::new_encryption; + mod orm; mod errors; @@ -16,11 +25,29 @@ mod errors; struct InputArgs { #[arg(short='c', long, help = "Path to the configuration file", default_value = "debug_config/")] config_file: String, + #[arg(long = "get-key", help = "Provideing the passwort, get the key", default_value = None)] + get_key: Option, + #[arg(long = "provider", help = "Encrypt provider with key", default_value = None)] + encrypt_provider: Option, } fn main() { let args = InputArgs::parse(); - init_config(&args.config_file); + + + if let (Some(key), Some(provider_file)) = (&args.get_key, &args.encrypt_provider){ + let res = new_encryption(provider_file, key); + if res.is_err() { + eprintln!("Error encrypting provider file: {}", res.err().unwrap()); + std::process::exit(1); + } + exit(0) + } else if args.get_key.is_some() || args.encrypt_provider.is_some() { + eprintln!("Both --get-key and --provider must be provided together."); + std::process::exit(1); + } + api::start_api(); + } \ No newline at end of file diff --git a/src/orm/handler.rs b/src/orm/handler.rs index 5ed057b..b98e5bc 100644 --- a/src/orm/handler.rs +++ b/src/orm/handler.rs @@ -21,16 +21,18 @@ impl DbConn { .expect("Error saving new service record"); } pub(crate) fn update_provider_state(&self, id: i32, new_state: RecordStates) -> ProviderRecord { - let conn = &mut *self.0.lock().unwrap(); - let now_unix = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64; - diesel::update(providers_records::table.filter(providers_records::dsl::id.eq(id))) - .set(( - providers_records::state.eq(new_state.to_string()), - providers_records::awnsered_at.eq(Some(now_unix)), - )) - .execute(conn) - .expect("Error updating service record state"); + { + let conn = &mut *self.0.lock().unwrap(); + let now_unix = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64; + diesel::update(providers_records::table.filter(providers_records::dsl::id.eq(id))) + .set(( + providers_records::state.eq(new_state.to_string()), + providers_records::awnsered_at.eq(Some(now_unix)), + )) + .execute(conn) + .expect("Error updating service record state"); + } self.get_record(id).unwrap() } diff --git a/src/orm/mod.rs b/src/orm/mod.rs index eaab916..fa727e1 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -1,5 +1,6 @@ use diesel::sqlite::SqliteConnection; use diesel::Connection; +use std::path::Path; use std::sync::{Arc, Mutex}; mod schema; diff --git a/src/orm/schema.rs b/src/orm/schema.rs index 28f359a..4c4b387 100644 --- a/src/orm/schema.rs +++ b/src/orm/schema.rs @@ -25,7 +25,4 @@ diesel::table! { } } -diesel::allow_tables_to_appear_in_same_query!( - beggars_records, - providers_records, -); +diesel::allow_tables_to_appear_in_same_query!(beggars_records, providers_records,); diff --git a/src/services/providers.rs b/src/services/providers.rs index f92697c..7d632c8 100644 --- a/src/services/providers.rs +++ b/src/services/providers.rs @@ -2,7 +2,10 @@ use std::f32::consts::E; use std::path::Path; use async_ssh2_tokio::Config; +use base64::Engine; +use log::info; use reqwest::Client; +use base64::prelude::BASE64_STANDARD; use reqwest::header::{HeaderName, HeaderValue}; use async_trait::async_trait; use async_ssh2_tokio::client::{self, AuthMethod, Client as SSHClient, ServerCheckMethod}; @@ -10,8 +13,10 @@ use rocket::futures::TryFutureExt; use rocket::tokio::fs; use serde::Deserialize; use enum_dispatch::enum_dispatch; +use toml::value::Array; use crate::app_config::CONFIG; +use crate::crypto::derive_key; use crate::errors::AutoDecryptError; #[derive(Debug, Deserialize)] @@ -113,14 +118,26 @@ impl ProviderAction for ForigneAction {} impl ForigneAction { pub(crate) fn load_from_file(&mut self, find_in_dir: &Path) -> Result<&mut Self, AutoDecryptError> { let content = std::fs::read_to_string(find_in_dir.join(&self.filename)) - .map_err(|err| AutoDecryptError::ConfigurationError { comment: err.to_string() })?; + .map_err(|err| AutoDecryptError::ConfigurationError { comment: format!("{}: {}", self.filename, err.to_string() ) })?; self.content = Some(content); Ok(self) } pub(crate) fn to_executable_action(&self, key: Option<&str>) -> Result { - todo!() + let content = std::fs::read_to_string(Path::new(&CONFIG.wait().base_config_dir).join(&self.filename)) + .map_err(|err| AutoDecryptError::ConfigurationError { comment: format!("{}: {}", self.filename, err.to_string() ) })?.trim().to_string(); + let cleartext_toml = if self.is_encrypted { + let derived_key = derive_key(key.unwrap(), &CONFIG.wait().crypto_key)?; + crate::crypto::decrypt_data(&content, &derived_key)? + } else { + content + }; + + let action: ActionType = toml::from_str(&cleartext_toml) + .map_err(|err| AutoDecryptError::ConfigurationError { comment: format!("Failed to parse action TOML: {}", err.to_string()) })?; + return Ok(action); + //let cleartext_toml = //1. get Cleartext toml 2. load tomel as any other 3. return object } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..d4255ac --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,22 @@ +use crate::CONFIG; +use crate::crypto; +use base64::Engine; +use base64::prelude::BASE64_STANDARD; + +pub(crate) fn new_encryption(file: &str, key: &str) -> Result { + let derived_key = crypto::derive_key(key, &CONFIG.wait().crypto_key).unwrap(); + let hashed_key = crypto::hash_hey(key).unwrap(); + + let unencrypted_file = std::fs::read_to_string(file).map_err(|e| e.to_string())?; + + let (encrypted_data, salt) = crypto::encrypt_data(&unencrypted_file, &derived_key, None).map_err(|e| e.to_string())?; + // We store the salt at the start of the file, base64 encoded + let file_content = format!("{}::{}\n", salt, encrypted_data); + + let encrypted_file = format!("{}.enc", file); + std::fs::write(&encrypted_file, file_content).map_err(|e| e.to_string())?; + println!("File encrypted and saved to {}", encrypted_file); + println!("Key hash (store this to decrypt later): {}", hashed_key); + + Ok(hashed_key) +} \ No newline at end of file