bin/darkirc: added support for private channels and DMs

This commit is contained in:
Dastan-glitch
2023-05-26 19:45:25 +03:00
parent 87eb662e27
commit 40b8a71d7b
6 changed files with 295 additions and 143 deletions

View File

@@ -80,40 +80,48 @@ pub fn encrypt(salt_box: &SalsaBox, plaintext: &str) -> String {
bs58::encode(concat).into_string()
}
/// Decrypt PrivMsg target
pub fn decrypt_target(
contact: &mut String,
privmsg: &mut PrivMsgEvent,
configured_chans: &HashMap<String, ChannelInfo>,
configured_contacts: &HashMap<String, ContactInfo>,
private_key: &Option<String>,
configured_chans: HashMap<String, ChannelInfo>,
configured_contacts: HashMap<String, ContactInfo>,
) {
for (name, chan_info) in configured_chans {
for chan_name in configured_chans.keys() {
let chan_info = configured_chans.get(chan_name).unwrap();
if !chan_info.joined {
continue
}
let salt_box = chan_info.salt_box(name).clone();
let salt_box = chan_info.salt_box.clone();
if let Some(salt_box) = salt_box {
if try_decrypt(&salt_box, &privmsg.target).is_some() {
privmsg.target = name.clone();
let decrypted_target = try_decrypt(&salt_box, &privmsg.target);
if decrypted_target.is_none() {
continue
}
let target = decrypted_target.unwrap();
if *chan_name == target {
privmsg.target = target;
return
}
}
}
if private_key.is_none() {
return
}
for (name, contact_info) in configured_contacts {
let salt_box = contact_info.salt_box(private_key.as_ref().unwrap(), name).clone();
for cnt_name in configured_contacts.keys() {
let cnt_info = configured_contacts.get(cnt_name).unwrap();
let salt_box = cnt_info.salt_box.clone();
if let Some(salt_box) = salt_box {
if try_decrypt(&salt_box, &privmsg.target).is_some() {
privmsg.target = name.clone();
return
let decrypted_target = try_decrypt(&salt_box, &privmsg.target);
if decrypted_target.is_none() {
continue
}
let target = decrypted_target.unwrap();
privmsg.target = target;
*contact = cnt_name.into();
return
}
}
}

View File

@@ -26,7 +26,11 @@ use futures::{
use log::{debug, error, info, warn};
use darkfi::{event_graph::model::Event, system::Subscription, Error, Result};
use darkfi::{
event_graph::{model::Event, EventMsg},
system::Subscription,
Error, Result,
};
use crate::{
crypto::{decrypt_privmsg, decrypt_target, encrypt_privmsg},
@@ -115,23 +119,27 @@ impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
self.irc_config.channels.extend(new_config.channels);
self.irc_config.contacts.extend(new_config.contacts);
self.irc_config.nickname = new_config.nickname;
self.irc_config.private_key = new_config.private_key;
// self.irc_config.private_key = new_config.private_key;
self.irc_config.password = new_config.password;
if self.on_receive_join(self.irc_config.channels.keys().cloned().collect()).await.is_err() {
warn!("Error to join updated channels");
} else {
info!("[CLIENT {}] Config updated", self.address);
}
}
pub async fn process_msg(&mut self, msg: &mut PrivMsgEvent) -> Result<()> {
info!("[CLIENT {}] msg from View: {:?}", self.address, msg.to_string());
let mut msg = msg.clone();
let mut contact = String::new();
decrypt_target(
msg,
&self.irc_config.channels,
&self.irc_config.contacts,
&self.irc_config.private_key,
&mut contact,
&mut msg,
self.irc_config.channels.clone(),
self.irc_config.contacts.clone(),
);
if msg.target.starts_with('#') {
@@ -145,13 +153,9 @@ impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
return Ok(())
}
if let Some(salt_box) = &chan_info.salt_box(&msg.target) {
decrypt_privmsg(salt_box, msg);
info!(
"[CLIENT {}] Decrypted received message: {:?}",
self.address,
msg.to_string()
);
if let Some(salt_box) = &chan_info.salt_box {
decrypt_privmsg(salt_box, &mut msg);
info!("Decrypted received message: {:?}", msg);
}
// add the nickname to the channel's names
@@ -160,25 +164,20 @@ impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
}
self.reply(&msg.to_string()).await?;
} else if self.irc_config.private_key.is_some() {
if let Some(contact_info) = self.irc_config.contacts.get(&msg.target) {
let salt_box = &contact_info
.salt_box(self.irc_config.private_key.as_ref().unwrap(), &msg.target);
if salt_box.is_none() {
return Ok(())
}
decrypt_privmsg(salt_box.as_ref().unwrap(), msg);
info!(
"[CLIENT {}] Decrypted received message: {:?}",
self.address,
msg.to_string()
);
self.reply(&msg.to_string()).await?;
} else if self.irc_config.is_cap_end && self.irc_config.is_nick_init {
if !self.irc_config.contacts.contains_key(&contact) {
return Ok(())
}
let contact_info = self.irc_config.contacts.get(&contact).unwrap();
if let Some(salt_box) = &contact_info.salt_box {
decrypt_privmsg(salt_box, &mut msg);
// This is for /query
msg.nick = contact;
info!("[P2P] Decrypted received message: {:?}", msg);
}
self.reply(&msg.to_string()).await?;
}
Ok(())
@@ -246,6 +245,7 @@ impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
self.irc_config.is_registered = true;
// join all channels
self.on_receive_join(self.irc_config.auto_channels.clone()).await?;
self.on_receive_join(self.irc_config.channels.keys().cloned().collect()).await?;
if *self.irc_config.capabilities.get("no-history").unwrap() {
@@ -487,11 +487,11 @@ impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
info!("[CLIENT {}] (Plain) PRIVMSG {} :{}", self.address, target, message,);
let mut privmsg = PrivMsgEvent {
nick: self.irc_config.nickname.clone(),
target: target.to_string(),
msg: message,
};
let mut privmsg = PrivMsgEvent::new();
privmsg.nick = self.irc_config.nickname.clone();
privmsg.target = target.to_string();
privmsg.msg = message.clone();
if target.starts_with('#') {
if !self.irc_config.channels.contains_key(target) {
@@ -504,22 +504,19 @@ impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
return Ok(())
}
if let Some(salt_box) = &channel_info.salt_box(target) {
if let Some(salt_box) = &channel_info.salt_box {
encrypt_privmsg(salt_box, &mut privmsg);
info!("[CLIENT {}] (Encrypted) PRIVMSG: {:?}", self.address, privmsg.to_string());
info!("[CLIENT {}] (Encrypted) PRIVMSG: {:?}", self.address, privmsg);
}
} else if self.irc_config.private_key.is_some() {
if let Some(contact_info) = self.irc_config.contacts.get(target) {
if let Some(salt_box) =
&contact_info.salt_box(self.irc_config.private_key.as_ref().unwrap(), target)
{
encrypt_privmsg(salt_box, &mut privmsg);
info!(
"[CLIENT {}] (Encrypted) PRIVMSG: {:?}",
self.address,
privmsg.to_string()
);
}
} else {
if !self.irc_config.contacts.contains_key(target) {
return Ok(())
}
let contact_info = self.irc_config.contacts.get(target).unwrap();
if let Some(salt_box) = &contact_info.salt_box {
encrypt_privmsg(salt_box, &mut privmsg);
info!("[CLIENT {}] (Encrypted) PRIVMSG: {:?}", self.address, privmsg);
}
}
@@ -536,7 +533,7 @@ impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
continue
}
if !self.irc_config.channels.contains_key(chan) {
let mut chan_info = ChannelInfo::new();
let mut chan_info = ChannelInfo::new()?;
chan_info.topic = Some("n/a".to_string());
self.irc_config.channels.insert(chan.to_string(), chan_info);
}

View File

@@ -18,10 +18,13 @@
use std::collections::HashMap;
use darkfi::Result;
use darkfi::{util::path::get_config_path, Result};
use crate::{
settings::{Args, ChannelInfo, ContactInfo},
settings::{
parse_configured_channels, parse_configured_contacts, Args, ChannelInfo, ContactInfo,
CONFIG_FILE,
},
PrivMsgEvent,
};
@@ -43,10 +46,11 @@ pub struct IrcConfig {
// user config
pub nickname: String,
pub password: String,
pub private_key: Option<String>,
// pub private_key: Option<String>,
pub capabilities: HashMap<String, bool>,
// channels and contacts
pub auto_channels: Vec<String>,
pub channels: HashMap<String, ChannelInfo>,
pub contacts: HashMap<String, ContactInfo>,
}
@@ -54,17 +58,15 @@ pub struct IrcConfig {
impl IrcConfig {
pub fn new(settings: &Args) -> Result<Self> {
let password = settings.password.as_ref().unwrap_or(&String::new()).clone();
let private_key = settings.private_key.clone();
// let private_key = settings.private_key.clone();
let mut channels = settings.channels.clone();
let auto_channels = settings.autojoin.clone();
for chan in settings.autojoin.iter() {
if !channels.contains_key(chan) {
channels.insert(chan.clone(), ChannelInfo::new());
}
}
let contacts = settings.contacts.clone();
// Pick up channel settings from the TOML configuration
let cfg_path = get_config_path(settings.config.clone(), CONFIG_FILE)?;
let toml_contents = std::fs::read_to_string(cfg_path)?;
let channels = parse_configured_channels(&toml_contents)?;
let contacts = parse_configured_contacts(&toml_contents)?;
let mut capabilities = HashMap::new();
capabilities.insert("no-history".to_string(), false);
@@ -77,9 +79,9 @@ impl IrcConfig {
is_pass_init: false,
nickname: "anon".to_string(),
password,
auto_channels,
channels,
contacts,
private_key,
capabilities,
})
}

View File

@@ -96,6 +96,7 @@ impl IrcServer {
))
.detach();
// Listen to msgs from View
executor
.clone()
.spawn(Self::listen_to_view(

View File

@@ -17,18 +17,19 @@
*/
use crypto_box::SalsaBox;
use log::error;
use serde::{self, Deserialize, Serialize};
use log::{info, warn};
use serde::{self, Deserialize};
use std::collections::HashMap;
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");
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;
@@ -45,7 +46,7 @@ pub enum RPL {
/// ircd cli
#[derive(Clone, Deserialize, StructOpt, StructOptToml)]
#[serde(default)]
#[structopt(name = "ircd")]
#[structopt(name = "darkirc")]
pub struct Args {
/// Sets a custom config file
#[structopt(long)]
@@ -56,7 +57,7 @@ pub struct Args {
pub rpc_listen: Url,
/// IRC listen URL
#[structopt(long = "irc", default_value = "tcp://127.0.0.1:7776")]
#[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
@@ -81,18 +82,10 @@ pub struct Args {
#[structopt(long)]
pub password: Option<String>,
/// Channels
#[structopt(skip)]
pub channels: HashMap<String, ChannelInfo>,
/// Channels
#[structopt(skip)]
pub contacts: HashMap<String, ContactInfo>,
/// Private key
#[structopt(skip)]
pub private_key: Option<String>,
// /// Private key
// #[structopt(skip)]
// pub private_key: Option<String>,
/// Network settings
#[structopt(flatten)]
pub net: SettingsOpt,
@@ -108,30 +101,15 @@ pub struct Args {
/// [contact."nick"]
/// pubkey = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
/// ```
#[derive(Default, Clone, Debug, Deserialize, Serialize)]
#[derive(Clone)]
pub struct ContactInfo {
pub pubkey: Option<String>,
/// Optional NaCl box for the channel, used for {en,de}cryption.
pub salt_box: Option<SalsaBox>,
}
impl ContactInfo {
pub fn new() -> Self {
Self { pubkey: None }
}
pub fn salt_box(&self, private_key: &str, contact_name: &str) -> Option<SalsaBox> {
if let Ok(private) = parse_priv(private_key) {
if let Some(p) = &self.pubkey {
if let Ok(public) = parse_pub(p) {
return Some(SalsaBox::new(&public, &private))
} else {
error!("Uncorrect public key in for contact {}", contact_name);
}
}
} else {
error!("Uncorrect Private key in config",);
}
None
pub fn new() -> Result<Self> {
Ok(Self { salt_box: None })
}
}
@@ -148,48 +126,214 @@ impl ContactInfo {
/// 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, Debug, Serialize, Deserialize)]
#[derive(Default, Clone)]
pub struct ChannelInfo {
/// Optional topic for the channel
pub topic: Option<String>,
/// Optional NaCl box for the channel, used for {en,de}cryption.
pub secret: Option<String>,
pub salt_box: Option<SalsaBox>,
/// Flag indicates whether the user has joined the channel or not
#[serde(default, skip_serializing)]
pub joined: bool,
/// All nicknames which are visible on the channel
#[serde(default, skip_serializing)]
pub names: Vec<String>,
}
impl ChannelInfo {
pub fn new() -> Self {
Self { topic: None, secret: None, joined: false, names: vec![] }
pub fn new() -> Result<Self> {
Ok(Self { topic: None, salt_box: None, joined: false, names: vec![] })
}
}
/// 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<HashMap<String, ChannelInfo>> {
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)
}
pub fn salt_box(&self, channel_name: &str) -> Option<SalsaBox> {
if let Some(s) = &self.secret {
let secret = parse_priv(s);
if !map["channel"].is_table() {
return Ok(ret)
}
if secret.is_err() {
error!("Uncorrect secret key for the channel {}", channel_name);
return None
}
for chan in map["channel"].as_table().unwrap() {
info!("Found configuration for channel {}", chan.0);
let mut channel_info = ChannelInfo::new()?;
let secret = secret.unwrap();
let public = secret.public_key();
return Some(SalsaBox::new(&public, &secret))
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);
}
None
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)
}
fn parse_priv(key: &str) -> Result<crypto_box::SecretKey> {
let bytes: [u8; 32] = bs58::decode(key).into_vec()?.try_into().unwrap();
Ok(crypto_box::SecretKey::from(bytes))
/// 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<HashMap<String, ContactInfo>> {
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_priv = match parse_priv_key(data) {
Ok(v) => v,
Err(_) => {
info!("Did not find 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)
}
fn parse_pub(key: &str) -> Result<crypto_box::PublicKey> {
let bytes: [u8; 32] = bs58::decode(key).into_vec()?.try_into().unwrap();
Ok(crypto_box::PublicKey::from(bytes))
fn salt_box_from_shared_secret(s: &str) -> Result<SalsaBox> {
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<String> {
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)
}
// fn parse_priv(key: &str) -> Result<crypto_box::SecretKey> {
// let bytes: [u8; 32] = bs58::decode(key).into_vec()?.try_into().unwrap();
// Ok(crypto_box::SecretKey::from(bytes))
// }
// fn parse_pub(key: &str) -> Result<crypto_box::PublicKey> {
// let bytes: [u8; 32] = bs58::decode(key).into_vec()?.try_into().unwrap();
// Ok(crypto_box::PublicKey::from(bytes))
// }