/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2023 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 std::{ collections::{HashMap, HashSet}, sync::Arc, }; use crypto_box::ChaChaBox; use log::{info, warn}; use serde::{self, 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 = "darkirc_config.toml"; pub const CONFIG_FILE_CONTENTS: &str = include_str!("../darkirc_config.toml"); // Msg config pub const MAXIMUM_LENGTH_OF_MESSAGE: usize = 1024; pub const MAXIMUM_LENGTH_OF_NICK_CHAN_CNT: usize = 32; // IRC Client pub enum RPL { NoTopic = 331, Topic = 332, NameReply = 353, EndOfNames = 366, } /// ircd cli #[derive(Clone, Deserialize, StructOpt, StructOptToml)] #[serde(default)] #[structopt(name = "darkirc")] pub struct Args { /// Sets a custom config file #[structopt(long)] pub config: Option, /// Sets Datastore Path #[structopt(long, default_value = "~/.local/darkfi/darkirc")] pub datastore: String, /// JSON-RPC listen URL #[structopt(long = "rpc", default_value = "tcp://127.0.0.1:26660")] 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 keypair and exit #[structopt(long)] pub gen_keypair: bool, /// Generate a new NaCl secret for an encrypted channel and exit #[structopt(long)] pub gen_secret: bool, /// Recover public key from secret key #[structopt(long = "recover_pubkey")] pub secret: Option, /// Path to save keypair in #[structopt(short)] pub output: Option, /// Autojoin channels #[structopt(long)] pub autojoin: Vec, /// Password #[structopt(long)] pub password: Option, /// Network settings #[structopt(flatten)] pub net: SettingsOpt, #[structopt(short, long)] /// Set log file to ouput into pub log: Option, /// Increase verbosity #[structopt(short, parse(from_occurrences))] pub verbose: u8, } /// This struct holds information about preconfigured contacts. /// In the TOML configuration file, we can configure contacts as such: /// /// ```toml /// [contact."nick"] /// pubkey = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM" /// ``` #[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 }) } } /// Defined user modes #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] pub enum UserMode { None, Op, Voice, HalfOp, Admin, Owner, } impl std::fmt::Display for UserMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match self { Self::None => write!(f, ""), Self::Op => write!(f, "@"), Self::Voice => write!(f, "+"), Self::HalfOp => write!(f, "%"), Self::Admin => write!(f, "&"), Self::Owner => write!(f, "~"), } } } /// This struct holds info about a specific nickname within a channel. /// We usually use it to implement modes. #[derive(Debug, Clone, Eq)] pub struct Nick { name: String, mode: UserMode, } impl Nick { pub fn new(name: String) -> Self { Self { name, mode: UserMode::None } } pub fn set_mode(&mut self, mode: UserMode) -> Option { if self.mode == mode { return None } self.mode = mode; Some(format!("+{}", mode)) } pub fn unset_mode(&mut self, mode: UserMode) -> Option { if self.mode != mode { return None } self.mode = mode; Some(format!("-{}", mode)) } } impl PartialEq for Nick { fn eq(&self, other: &Self) -> bool { self.name == other.name } } impl From for Nick { fn from(name: String) -> Self { Self { name, mode: UserMode::None } } } impl std::hash::Hash for Nick { fn hash(&self, state: &mut H) { state.write(&self.name.clone().into_bytes()); } } impl std::fmt::Display for Nick { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!(f, "{}{}", self.mode, self.name) } } /// 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(Default, 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>, /// All nicknames which are visible on the channel pub names: HashSet, } impl ChannelInfo { pub fn new() -> Result { Ok(Self { topic: None, salt_box: None, names: HashSet::new() }) } pub fn names(&self) -> String { self.names.iter().map(|n| n.to_string()).collect::>().join(" ") } } /// 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 = HashMap::new(); 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() { if chan.0.len() > MAXIMUM_LENGTH_OF_NICK_CHAN_CNT { warn!("Channel name is too long, skipping..."); continue } 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(Arc::new(salt_box)); info!("Instantiated NaCl box for channel {}", chan.0); } } ret.insert(chan.0.to_string(), channel_info); } Ok(ret) } /// Parse a TOML string for any configured contact list and return /// a map containing said configurations. /// /// ```toml /// [contact."nick"] /// public_key = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM" /// ``` pub fn parse_configured_contacts(data: &str) -> Result> { let mut ret = HashMap::new(); 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_secret = match parse_secret_key(data) { Ok(v) => v, Err(_) => { info!("Did not find secret key in config, skipping contact configuration."); return Ok(ret) } }; let bytes: [u8; 32] = match bs58::decode(found_secret).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 { if cnt.0.len() > MAXIMUM_LENGTH_OF_NICK_CHAN_CNT { warn!("Contact name is too long, skipping..."); continue } 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("public_key") || !table["public_key"].is_str() { warn!("Contact {} doesn't have `public_key` set or is not a valid string.", cnt.0); continue } let pub_str = table["public_key"].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(Arc::new(ChaChaBox::new(&public, &secret))); ret.insert(cnt.0.to_string(), contact_info); info!("Instantiated NaCl box for contact \"{}\"", cnt.0); } Ok(ret) } 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(ChaChaBox::new(&public, &secret)) } fn parse_secret_key(data: &str) -> Result { let mut sk = String::new(); let map = match toml::from_str(data)? { Value::Table(m) => m, _ => return Ok(sk), }; if !map.contains_key("secret_key") { return Ok(sk) } if !map["secret_key"].is_table() { return Ok(sk) } let secret_keys = map["secret_key"].as_table().unwrap(); for key in secret_keys { sk = key.0.into(); } info!("Found secret key in config, noted it down."); Ok(sk) }