diff --git a/README.md b/README.md index 28efff6..7547983 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,7 @@ psql -h localhost -p 55432 -U mitra mitra ### Run web service -Generate instance key: - -``` -cargo run --bin mitractl generate-rsa-key -``` - -Create config file, set `instance_rsa_key`, adjust other settings if needed: +Create config file, adjust settings if needed: ``` cp config.yaml.example config.yaml diff --git a/config.yaml.example b/config.yaml.example index 8dc8ec0..33ec3e9 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -11,8 +11,6 @@ instance_short_description: my fedi instance # Long description can contain markdown syntax instance_description: my fedi instance -# RSA private key can be generated with `mitractl generate-rsa-key` -instance_rsa_key: null registrations_open: false # EIP-4361 login message login_message: 'Do not sign this message on other sites!' diff --git a/src/activitypub/actor.rs b/src/activitypub/actor.rs index 99b9fdb..c7ceacb 100644 --- a/src/activitypub/actor.rs +++ b/src/activitypub/actor.rs @@ -357,7 +357,7 @@ mod tests { #[test] fn test_local_actor() { let private_key = generate_weak_private_key().unwrap(); - let private_key_pem = serialize_private_key(private_key).unwrap(); + let private_key_pem = serialize_private_key(&private_key).unwrap(); let profile = DbActorProfile { username: "testuser".to_string(), ..Default::default() diff --git a/src/bin/mitractl.rs b/src/bin/mitractl.rs index e2094e5..e99c77d 100644 --- a/src/bin/mitractl.rs +++ b/src/bin/mitractl.rs @@ -42,7 +42,7 @@ struct GenerateRsaKey; impl GenerateRsaKey { fn execute(&self) -> () { let private_key = generate_private_key().unwrap(); - let private_key_str = serialize_private_key(private_key).unwrap(); + let private_key_str = serialize_private_key(&private_key).unwrap(); println!("{}", private_key_str); } } diff --git a/src/config.rs b/src/config.rs index 8acc48c..77ca93b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; use log::{Level as LogLevel}; @@ -10,7 +10,12 @@ use crate::activitypub::views::get_instance_actor_url; use crate::errors::ConversionError; use crate::ethereum::utils::{parse_caip2_chain_id, ChainIdError}; use crate::models::profiles::currencies::Currency; -use crate::utils::crypto::deserialize_private_key; +use crate::utils::crypto::{ + deserialize_private_key, + generate_private_key, + serialize_private_key, +}; +use crate::utils::files::{set_file_permissions, write_file}; #[derive(Clone, Debug)] pub enum Environment { @@ -115,7 +120,9 @@ pub struct Config { pub instance_title: String, pub instance_short_description: String, pub instance_description: String, - instance_rsa_key: String, + + #[serde(skip)] + instance_rsa_key: Option, #[serde(default)] pub registrations_open: bool, // default is false @@ -150,15 +157,11 @@ impl Config { Ok(url) } - fn try_instance_rsa_key(&self) -> Result { - deserialize_private_key(&self.instance_rsa_key) - } - pub fn instance(&self) -> Instance { Instance { _url: self.try_instance_url().unwrap(), _version: self.version.clone(), - actor_key: self.try_instance_rsa_key().unwrap(), + actor_key: self.instance_rsa_key.clone().unwrap(), is_private: matches!(self.environment, Environment::Development), } } @@ -212,6 +215,28 @@ impl Instance { } } +/// Generates new instance RSA key or returns existing key +fn read_instance_rsa_key(storage_dir: &Path) -> RsaPrivateKey { + let private_key_path = storage_dir.join("instance_rsa_key"); + if private_key_path.exists() { + let private_key_str = std::fs::read_to_string(&private_key_path) + .expect("failed to read instance RSA key"); + let private_key = deserialize_private_key(&private_key_str) + .expect("failed to read instance RSA key"); + private_key + } else { + let private_key = generate_private_key() + .expect("failed to generate RSA key"); + let private_key_str = serialize_private_key(&private_key) + .expect("failed to serialize RSA key"); + write_file(private_key_str.as_bytes(), &private_key_path) + .expect("failed to write instance RSA key"); + set_file_permissions(&private_key_path, 0o600) + .expect("failed to set permissions on RSA key file"); + private_key + } +} + pub fn parse_config() -> Config { let env = parse_env(); let config_yaml = std::fs::read_to_string(env.config_path) @@ -220,36 +245,38 @@ pub fn parse_config() -> Config { .expect("invalid yaml data"); // Override environment parameter in config if env variable is set config.environment = env.environment.unwrap_or(config.environment); - // Set_version + // Set version config.version = env.crate_version; // Validate config if !config.storage_dir.exists() { panic!("storage directory does not exist"); }; + config.try_instance_url().expect("invalid instance URI"); if let Some(blockchain_config) = config.blockchain.as_ref() { blockchain_config.try_ethereum_chain_id().unwrap(); if !blockchain_config.contract_dir.exists() { panic!("contract directory does not exist"); }; }; - config.try_instance_url().expect("invalid instance URI"); - config.try_instance_rsa_key().expect("invalid instance RSA key"); if config.ipfs_api_url.is_some() != config.ipfs_gateway_url.is_some() { panic!("both ipfs_api_url and ipfs_gateway_url must be set"); }; + // Insert instance RSA key + config.instance_rsa_key = Some(read_instance_rsa_key(&config.storage_dir)); + config } #[cfg(test)] mod tests { - use rand::rngs::OsRng; + use crate::utils::crypto::generate_weak_private_key; use super::*; #[test] fn test_instance_url_https_dns() { let instance_url = Url::parse("https://example.com/").unwrap(); - let instance_rsa_key = RsaPrivateKey::new(&mut OsRng, 512).unwrap(); + let instance_rsa_key = generate_weak_private_key().unwrap(); let instance = Instance { _url: instance_url, _version: "1.0.0".to_string(), @@ -265,7 +292,7 @@ mod tests { #[test] fn test_instance_url_http_ipv4() { let instance_url = Url::parse("http://1.2.3.4:3777/").unwrap(); - let instance_rsa_key = RsaPrivateKey::new(&mut OsRng, 512).unwrap(); + let instance_rsa_key = generate_weak_private_key().unwrap(); let instance = Instance { _url: instance_url, _version: "1.0.0".to_string(), diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index eacdaf9..49bd505 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -123,7 +123,7 @@ pub async fn create_account( Ok(Ok(private_key)) => private_key, _ => return Err(HttpError::InternalError), }; - let private_key_pem = serialize_private_key(private_key) + let private_key_pem = serialize_private_key(&private_key) .map_err(|_| HttpError::InternalError)?; let AccountCreateData { username, invite_code, .. } = diff --git a/src/utils/crypto.rs b/src/utils/crypto.rs index 70e0f3b..815300d 100644 --- a/src/utils/crypto.rs +++ b/src/utils/crypto.rs @@ -34,7 +34,7 @@ pub fn generate_weak_private_key() -> Result } pub fn serialize_private_key( - private_key: RsaPrivateKey, + private_key: &RsaPrivateKey, ) -> Result { private_key.to_pkcs8_pem().map(|val| val.to_string()) } diff --git a/src/utils/files.rs b/src/utils/files.rs index 889fa7d..a9345b6 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -1,5 +1,11 @@ -use std::fs::{remove_file, File}; +use std::fs::{ + remove_file, + set_permissions, + File, + Permissions, +}; use std::io::prelude::*; +use std::os::unix::fs::PermissionsExt; use std::path::Path; use mime_guess::get_mime_extensions_str; @@ -36,9 +42,15 @@ fn get_file_name(data: &[u8], media_type: Option<&str>) -> String { file_name } -fn write_file(data: Vec, file_path: &Path) -> Result<(), FileError> { +pub fn write_file(data: &[u8], file_path: &Path) -> Result<(), FileError> { let mut file = File::create(file_path)?; - file.write_all(&data)?; + file.write_all(data)?; + Ok(()) +} + +pub fn set_file_permissions(file_path: &Path, mode: u32) -> Result<(), FileError> { + let permissions = Permissions::from_mode(mode); + set_permissions(file_path, permissions)?; Ok(()) } @@ -49,7 +61,7 @@ pub fn save_file( let media_type = sniff_media_type(&data); let file_name = get_file_name(&data, media_type.as_deref()); let file_path = output_dir.join(&file_name); - write_file(data, &file_path)?; + write_file(&data, &file_path)?; Ok((file_name, media_type)) } @@ -61,7 +73,7 @@ pub fn save_b64_file( let media_type = sniff_media_type(&data); let file_name = get_file_name(&data, media_type.as_deref()); let file_path = output_dir.join(&file_name); - write_file(data, &file_path)?; + write_file(&data, &file_path)?; Ok((file_name, media_type)) } @@ -78,7 +90,7 @@ pub fn save_validated_b64_file( } let file_name = get_file_name(&data, Some(&media_type)); let file_path = output_dir.join(&file_name); - write_file(data, &file_path)?; + write_file(&data, &file_path)?; Ok((file_name, media_type)) }