implemented encryption and decryption

This commit is contained in:
Kyattsukuro 2025-10-28 16:09:37 +01:00
parent bba1912507
commit a0a140fc0b
14 changed files with 226 additions and 48 deletions

24
.vscode/launch.json vendored
View File

@ -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",

52
Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -1,5 +1,5 @@
let
# Import nixpkgs and add the mozilla overlay
# Import nixpkgs and add the Mozilla overlay
nixpkgs = import <nixpkgs> {
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.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
'';
}

View File

@ -30,7 +30,7 @@ pub(crate) fn consent(consent_id: i32, db_conn: &State<DbConn>) -> Result<Templa
#[derive(FromForm)]
struct ConsentInput {
pub(crate) struct ConsentInput {
key: Option<String>,
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)))
}

View File

@ -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())
}

View File

@ -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<String, Beggars>, // We request to unlock owned services
pub(crate) providers: HashMap<String, Providers>, // 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::<HashMap<String, Providers>>())
).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::<HashMap<String, Providers>>()
})
.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::<TomlAppConfig>(&contents)
.map_err(|err| {

View File

@ -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<String, AutoDecryptError> {
pub(crate) fn decrypt_data(salt_data: &str, key: &[u8; 32]) -> Result<String, AutoDecryptError> {
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::<Vec<&str>>();
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)
}

View File

@ -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<String>,
#[arg(long = "provider", help = "Encrypt provider with key", default_value = None)]
encrypt_provider: Option<String>,
}
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();
}

View File

@ -21,6 +21,7 @@ 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;
@ -31,6 +32,7 @@ impl DbConn {
))
.execute(conn)
.expect("Error updating service record state");
}
self.get_record(id).unwrap()
}

View File

@ -1,5 +1,6 @@
use diesel::sqlite::SqliteConnection;
use diesel::Connection;
use std::path::Path;
use std::sync::{Arc, Mutex};
mod schema;

View File

@ -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,);

View File

@ -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<ActionType, AutoDecryptError> {
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
}

22
src/utils.rs Normal file
View File

@ -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<String, String> {
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)
}