/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2022 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use crypto_box::SalsaBox; use fxhash::FxHashMap; use log::{info, warn}; use serde::Deserialize; use structopt::StructOpt; use structopt_toml::StructOptToml; use toml::Value; use url::Url; use darkfi::{net::settings::SettingsOpt, Result}; // Location for config file pub const CONFIG_FILE: &str = "ircd_config.toml"; pub const CONFIG_FILE_CONTENTS: &str = include_str!("../ircd_config.toml"); // Buffers and ordering configuration pub const SIZE_OF_MSGS_BUFFER: usize = 4095; pub const SIZE_OF_IDSS_BUFFER: usize = 65536; pub const LIFETIME_FOR_ORPHAN: i64 = 600; pub const TERM_MAX_TIME_DIFFERENCE: i64 = 180; pub const BROADCAST_LAST_TERM_MSG: u64 = 4; // Msg config pub const MAXIMUM_LENGTH_OF_MESSAGE: usize = 1024; pub const MAXIMUM_LENGTH_OF_NICKNAME: usize = 32; // Protocol config pub const MAX_CONFIRM: u8 = 4; pub const UNREAD_MSG_EXPIRE_TIME: i64 = 18000; pub const TIMEOUT_FOR_RESEND_UNREAD_MSGS: u64 = 240; // IRC Client pub enum RPL { NoTopic = 331, Topic = 332, NameReply = 353, EndOfNames = 366, } /// ircd cli #[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)] #[serde(default)] #[structopt(name = "ircd")] pub struct Args { /// Sets a custom config file #[structopt(long)] pub config: Option, /// JSON-RPC listen URL #[structopt(long = "rpc", default_value = "tcp://127.0.0.1:25550")] pub rpc_listen: Url, /// IRC listen URL #[structopt(long = "irc", default_value = "tcp://127.0.0.1:6667")] pub irc_listen: Url, /// Optional TLS certificate file path if `irc_listen` uses TLS pub irc_tls_cert: Option, /// Optional TLS certificate key file path if `irc_listen` uses TLS pub irc_tls_secret: Option, /// Generate a new NaCl secret and exit #[structopt(long)] pub gen_secret: bool, /// Generate a new NaCl keypair and exit #[structopt(long)] pub gen_keypair: bool, /// Path to save keypair in #[structopt(short)] pub output: Option, /// Autojoin channels #[structopt(long)] pub autojoin: Vec, /// Password #[structopt(long)] pub password: Option, #[structopt(flatten)] pub net: SettingsOpt, /// Increase verbosity #[structopt(short, parse(from_occurrences))] pub verbose: u8, } #[derive(Clone)] pub struct ContactInfo { /// Optional NaCl box for the channel, used for {en,de}cryption. pub salt_box: Option, } impl ContactInfo { pub fn new() -> Result { Ok(Self { salt_box: None }) } } /// This struct holds information about preconfigured channels. /// In the TOML configuration file, we can configure channels as such: /// ```toml /// [channel."#dev"] /// secret = "GvH4kno3kUu6dqPrZ8zjMhqxTUDZ2ev16EdprZiZJgj1" /// topic = "DarkFi Development Channel" /// ``` /// Having a secret will enable a NaCl box that is able to encrypt and /// decrypt messages in this channel using this set shared secret. /// The secret should be shared OOB, via a secure channel. /// Having a topic set is useful if one wants to have a topic in the /// configured channel. It is not shared with others, but it is useful /// for personal reference. #[derive(Clone)] pub struct ChannelInfo { /// Optional topic for the channel pub topic: Option, /// Optional NaCl box for the channel, used for {en,de}cryption. pub salt_box: Option, /// Flag indicates whether the user has joined the channel or not pub joined: bool, /// All nicknames which are visible on the channel pub names: Vec, } impl ChannelInfo { pub fn new() -> Result { Ok(Self { topic: None, salt_box: None, joined: false, names: vec![] }) } } fn salt_box_from_shared_secret(s: &str) -> Result { let bytes: [u8; 32] = bs58::decode(s).into_vec()?.try_into().unwrap(); let secret = crypto_box::SecretKey::from(bytes); let public = secret.public_key(); Ok(SalsaBox::new(&public, &secret)) } fn parse_priv_key(data: &str) -> Result { let mut pk = String::new(); let map = match toml::from_str(data)? { Value::Table(m) => m, _ => return Ok(pk), }; if !map.contains_key("private_key") { return Ok(pk) } if !map["private_key"].is_table() { return Ok(pk) } let private_keys = map["private_key"].as_table().unwrap(); for prv_key in private_keys { pk = prv_key.0.into(); } info!("Found secret key in config, noted it down."); Ok(pk) } /// Parse a TOML string for any configured contact list and return /// a map containing said configurations. /// /// ```toml /// [contact."nick"] /// contact_pubkey = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM" /// ``` pub fn parse_configured_contacts(data: &str) -> Result> { let mut ret = FxHashMap::default(); let map = match toml::from_str(data) { Ok(Value::Table(m)) => m, _ => { warn!("Invalid TOML string passed as argument to parse_configured_contacts()"); return Ok(ret) } }; if !map.contains_key("contact") { return Ok(ret) } if !map["contact"].is_table() { warn!("TOML configuration contains a \"contact\" field, but it is not a table."); return Ok(ret) } let contacts = map["contact"].as_table().unwrap(); // Our secret key for NaCl boxes. let found_priv = match parse_priv_key(data) { Ok(v) => v, Err(_) => { info!("Did not found private key in config, skipping contact configuration."); return Ok(ret) } }; let bytes: [u8; 32] = match bs58::decode(found_priv).into_vec() { Ok(v) => { if v.len() != 32 { warn!("Decoded base58 secret key string is not 32 bytes"); warn!("Skipping private contact configuration"); return Ok(ret) } v.try_into().unwrap() } Err(e) => { warn!("Failed to decode base58 secret key from string: {}", e); warn!("Skipping private contact configuration"); return Ok(ret) } }; let secret = crypto_box::SecretKey::from(bytes); for cnt in contacts { info!("Found configuration for contact {}", cnt.0); let mut contact_info = ContactInfo::new()?; if !cnt.1.is_table() { warn!("Config for contact {} isn't a TOML table", cnt.0); continue } let table = cnt.1.as_table().unwrap(); if table.is_empty() { warn!("Configuration for contact {} is empty.", cnt.0); continue } // Build the NaCl box if !table.contains_key("contact_pubkey") || !table["contact_pubkey"].is_str() { warn!("Contact {} doesn't have `contact_pubkey` set or is not a string.", cnt.0); continue } let pub_str = table["contact_pubkey"].as_str().unwrap(); let bytes: [u8; 32] = match bs58::decode(pub_str).into_vec() { Ok(v) => { if v.len() != 32 { warn!("Decoded base58 string is not 32 bytes"); continue } v.try_into().unwrap() } Err(e) => { warn!("Failed to decode base58 pubkey from string: {}", e); continue } }; let public = crypto_box::PublicKey::from(bytes); contact_info.salt_box = Some(SalsaBox::new(&public, &secret)); ret.insert(cnt.0.to_string(), contact_info); info!("Instantiated NaCl box for contact {}", cnt.0); } Ok(ret) } /// Parse a TOML string for any configured channels and return /// a map containing said configurations. /// /// ```toml /// [channel."#memes"] /// secret = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM" /// topic = "Dank Memes" /// ``` pub fn parse_configured_channels(data: &str) -> Result> { let mut ret = FxHashMap::default(); let map = match toml::from_str(data)? { Value::Table(m) => m, _ => return Ok(ret), }; if !map.contains_key("channel") { return Ok(ret) } if !map["channel"].is_table() { return Ok(ret) } for chan in map["channel"].as_table().unwrap() { info!("Found configuration for channel {}", chan.0); let mut channel_info = ChannelInfo::new()?; if chan.1.as_table().unwrap().contains_key("topic") { let topic = chan.1["topic"].as_str().unwrap().to_string(); info!("Found topic for channel {}: {}", chan.0, topic); channel_info.topic = Some(topic); } if chan.1.as_table().unwrap().contains_key("secret") { // Build the NaCl box if let Some(s) = chan.1["secret"].as_str() { let salt_box = salt_box_from_shared_secret(s)?; channel_info.salt_box = Some(salt_box); info!("Instantiated NaCl box for channel {}", chan.0); } } ret.insert(chan.0.to_string(), channel_info); } Ok(ret) } pub fn get_current_time() -> u64 { let start = std::time::SystemTime::now(); start .duration_since(std::time::UNIX_EPOCH) .expect("Time went backwards") .as_millis() .try_into() .unwrap() }