ソースを参照

ircd: Configurable autojoins and per-channel encryption.

parazyd 4 年 前
コミット
5cfe8be52c

+ 3 - 0
Cargo.lock

@@ -2261,7 +2261,9 @@ dependencies = [
  "async-executor",
  "async-std",
  "async-trait",
+ "bs58",
  "clap 3.1.12",
+ "crypto_box",
  "ctrlc-async",
  "darkfi",
  "easy-parallel",
@@ -2276,6 +2278,7 @@ dependencies = [
  "smol",
  "structopt",
  "structopt-toml",
+ "toml",
  "url",
 ]
 

+ 1 - 0
Cargo.toml

@@ -156,6 +156,7 @@ websockets = [
 ]
 
 util = [
+	"bs58",
 	"hex",
 	"bincode",
 	"serde",

+ 3 - 0
bin/ircd/Cargo.toml

@@ -21,6 +21,7 @@ async-executor = "1.4.1"
 easy-parallel = "3.2.0"
 
 # Crypto
+crypto_box = "0.7.2"
 rand = "0.8.5"
 
 # Misc
@@ -36,3 +37,5 @@ serde_json = "1.0.79"
 serde = {version = "1.0.136", features = ["derive"]}
 structopt = "0.3.26"
 structopt-toml = "0.5.0"
+bs58 = "0.4.0"
+toml = "0.5.9"

+ 4 - 0
bin/ircd/ircd_config.toml

@@ -7,6 +7,8 @@
 ## Sets Datastore Path
 #datastore="~/.config/ircd"
 
+autojoin = ["#dev"]
+
 ## Raft net settings
 [net]
 ## P2P accept address
@@ -31,3 +33,5 @@
 #channel_handshake_seconds=4
 #channel_heartbeat_seconds=10
 
+[channel."#dev"]
+secret = "f00b4r"

+ 42 - 0
bin/ircd/src/crypto.rs

@@ -0,0 +1,42 @@
+use crypto_box::aead::Aead;
+use rand::rngs::OsRng;
+
+/// Try decrypting a message given a NaCl box and a base58 string.
+/// The format we're using is nonce+ciphertext, where nonce is 24 bytes.
+pub fn try_decrypt_message(salt_box: &crypto_box::Box, ciphertext: &str) -> Option<String> {
+    let bytes = match bs58::decode(ciphertext).into_vec() {
+        Ok(v) => v,
+        Err(_) => return None,
+    };
+
+    // Try extracting the nonce
+    let nonce = match bytes[0..24].try_into() {
+        Ok(v) => v,
+        Err(_) => return None,
+    };
+
+    // Take the remaining ciphertext
+    if bytes.len() < 25 {
+        return None
+    }
+    let message = &bytes[24..];
+
+    // Try decrypting the message
+    match salt_box.decrypt(nonce, message) {
+        Ok(v) => Some(String::from_utf8_lossy(&v).to_string()),
+        Err(_) => None,
+    }
+}
+
+/// Encrypt a message given a NaCl box and a plaintext string.
+/// The format we're using is nonce+ciphertext, where nonce is 24 bytes.
+pub fn encrypt_message(salt_box: &crypto_box::Box, plaintext: &str) -> String {
+    let nonce = crypto_box::generate_nonce(&mut OsRng);
+    let mut ciphertext = salt_box.encrypt(&nonce, plaintext.as_bytes()).unwrap();
+
+    let mut concat = vec![];
+    concat.append(&mut nonce.as_slice().to_vec());
+    concat.append(&mut ciphertext);
+
+    bs58::encode(concat).into_string()
+}

+ 43 - 9
bin/ircd/src/main.rs

@@ -1,15 +1,16 @@
-use async_std::{
-    net::{TcpListener, TcpStream},
-    sync::{Arc, Mutex},
-};
-
 use std::net::SocketAddr;
 
 use async_channel::{Receiver, Sender};
 use async_executor::Executor;
+use async_std::{
+    net::{TcpListener, TcpStream},
+    sync::{Arc, Mutex},
+};
 use easy_parallel::Parallel;
 use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, FutureExt};
+use fxhash::FxHashMap;
 use log::{debug, error, info, warn};
+use rand::rngs::OsRng;
 use simplelog::{ColorChoice, TermLogger, TerminalMode};
 use smol::future;
 use structopt_toml::StructOptToml;
@@ -26,16 +27,18 @@ use darkfi::{
     Error, Result,
 };
 
+pub mod crypto;
 pub mod privmsg;
 pub mod rpc;
 pub mod server;
 pub mod settings;
 
 use crate::{
+    crypto::try_decrypt_message,
     privmsg::Privmsg,
     rpc::JsonRpcInterface,
     server::IrcServerConnection,
-    settings::{Args, CONFIG_FILE, CONFIG_FILE_CONTENTS},
+    settings::{parse_configured_channels, Args, ChannelInfo, CONFIG_FILE, CONFIG_FILE_CONTENTS},
 };
 
 pub type SeenMsgIds = Arc<Mutex<Vec<u32>>>;
@@ -43,7 +46,7 @@ pub type SeenMsgIds = Arc<Mutex<Vec<u32>>>;
 fn build_irc_msg(msg: &Privmsg) -> String {
     debug!("ABOUT TO SEND: {:?}", msg);
     let irc_msg =
-        format!(":{}!anon@dark.fi PRIVMSG {} :{}\r\n", msg.nickname, msg.channel, msg.message,);
+        format!(":{}!anon@dark.fi PRIVMSG {} :{}\r\n", msg.nickname, msg.channel, msg.message);
     irc_msg
 }
 
@@ -86,18 +89,26 @@ async fn process(
     peer_addr: SocketAddr,
     raft_sender: Sender<Privmsg>,
     seen_msg_id: SeenMsgIds,
+    autojoin_chans: Vec<String>,
+    configured_chans: FxHashMap<String, ChannelInfo>,
 ) -> Result<()> {
     let (reader, writer) = stream.split();
 
     let mut reader = BufReader::new(reader);
-    let mut conn = IrcServerConnection::new(writer, seen_msg_id.clone(), raft_sender);
+    let mut conn = IrcServerConnection::new(
+        writer,
+        seen_msg_id.clone(),
+        raft_sender,
+        autojoin_chans,
+        configured_chans,
+    );
 
     loop {
         let mut line = String::new();
         futures::select! {
             privmsg = raft_receiver.recv().fuse() => {
                 info!("Receive msg from raft");
-                let msg = privmsg?;
+                let mut msg = privmsg?;
 
                 let mut smi = seen_msg_id.lock().await;
                 if smi.contains(&msg.id) {
@@ -106,6 +117,16 @@ async fn process(
                 smi.push(msg.id);
                 drop(smi);
 
+                // Try to potentially decrypt the incoming message.
+                if conn.configured_chans.contains_key(&msg.channel) {
+                    let chan_info = conn.configured_chans.get(&msg.channel).unwrap();
+                    if let Some(salt_box) = &chan_info.salt_box {
+                        if let Some(decrypted_msg) = try_decrypt_message(salt_box, &msg.message) {
+                            msg.message = decrypted_msg;
+                        }
+                    }
+                }
+
                 let irc_msg = build_irc_msg(&msg);
                 conn.reply(&irc_msg).await?;
             }
@@ -127,8 +148,19 @@ async fn process(
 
 async_daemonize!(realmain);
 async fn realmain(settings: Args, executor: Arc<Executor<'_>>) -> Result<()> {
+    if settings.gen_secret {
+        let secret_key = crypto_box::SecretKey::generate(&mut OsRng);
+        let encoded = bs58::encode(secret_key.as_bytes());
+        println!("{}", encoded.into_string());
+        return Ok(())
+    }
+
     let seen_msg_id: SeenMsgIds = Arc::new(Mutex::new(vec![]));
 
+    // Pick up channel settings from the TOML configuration
+    let cfg_path = get_config_path(settings.config, CONFIG_FILE)?;
+    let configured_chans = parse_configured_channels(&cfg_path)?;
+
     //
     //Raft
     //
@@ -201,6 +233,8 @@ async fn realmain(settings: Args, executor: Arc<Executor<'_>>) -> Result<()> {
                     peer_addr,
                     raft_sender.clone(),
                     seen_msg_id.clone(),
+                    settings.autojoin.clone(),
+                    configured_chans.clone(),
                 ))
                 .detach();
         }

+ 0 - 2
bin/ircd/src/rpc.rs

@@ -1,5 +1,3 @@
-use std::sync::Arc;
-
 use async_trait::async_trait;
 use log::debug;
 use serde_json::{json, Value};

+ 35 - 7
bin/ircd/src/server.rs

@@ -1,12 +1,13 @@
 use async_std::net::TcpStream;
 
 use futures::{io::WriteHalf, AsyncWriteExt};
+use fxhash::FxHashMap;
 use log::{debug, info, warn};
 use rand::{rngs::OsRng, RngCore};
 
 use darkfi::{Error, Result};
 
-use crate::{privmsg::Privmsg, SeenMsgIds};
+use crate::{crypto::encrypt_message, privmsg::Privmsg, ChannelInfo, SeenMsgIds};
 
 pub struct IrcServerConnection {
     write_stream: WriteHalf<TcpStream>,
@@ -14,9 +15,10 @@ pub struct IrcServerConnection {
     is_user_init: bool,
     is_registered: bool,
     nickname: String,
-    _channels: Vec<String>,
     seen_msg_id: SeenMsgIds,
     p2p_sender: async_channel::Sender<Privmsg>,
+    auto_channels: Vec<String>,
+    pub configured_chans: FxHashMap<String, ChannelInfo>,
 }
 
 impl IrcServerConnection {
@@ -24,6 +26,8 @@ impl IrcServerConnection {
         write_stream: WriteHalf<TcpStream>,
         seen_msg_id: SeenMsgIds,
         p2p_sender: async_channel::Sender<Privmsg>,
+        auto_channels: Vec<String>,
+        configured_chans: FxHashMap<String, ChannelInfo>,
     ) -> Self {
         Self {
             write_stream,
@@ -31,9 +35,10 @@ impl IrcServerConnection {
             is_user_init: false,
             is_registered: false,
             nickname: "".to_string(),
-            _channels: vec![],
             seen_msg_id,
             p2p_sender,
+            auto_channels,
+            configured_chans,
         }
     }
 
@@ -60,7 +65,7 @@ impl IrcServerConnection {
                 self.reply(&nick_reply).await?;
             }
             "JOIN" => {
-                // Ignore since channels are all autojoin
+                // TODO:
                 // let channel = tokens.next().ok_or(Error::MalformedPacket)?;
                 // self.channels.push(channel.to_string());
 
@@ -88,6 +93,19 @@ impl IrcServerConnection {
                 let message = &line[substr_idx + 1..];
                 info!("Message {}: {}", channel, message);
 
+                let message = if self.configured_chans.contains_key(channel) {
+                    let channel_info = self.configured_chans.get(channel).unwrap();
+                    if let Some(salt_box) = &channel_info.salt_box {
+                        let encrypted = encrypt_message(salt_box, message);
+                        info!("Encrypted message {}: {}", channel, encrypted);
+                        encrypted
+                    } else {
+                        message.to_string()
+                    }
+                } else {
+                    message.to_string()
+                };
+
                 let random_id = OsRng.next_u32();
 
                 let protocol_msg = Privmsg {
@@ -128,9 +146,19 @@ impl IrcServerConnection {
                 };
             }
 
-            autojoin!("#dev", "Development of DarkFi");
-            autojoin!("#markets", "Markets, trading, DeFi, algo, biz, finance, and economics");
-            autojoin!("#memes", "Memetic engineering");
+            for chan in self.auto_channels.clone() {
+                let topic = if self.configured_chans.contains_key(&chan) {
+                    let c = self.configured_chans.get(&chan).unwrap();
+                    if let Some(topic) = &c.topic {
+                        topic
+                    } else {
+                        ""
+                    }
+                } else {
+                    ""
+                };
+                autojoin!(chan, topic);
+            }
         }
 
         Ok(())

+ 82 - 2
bin/ircd/src/settings.rs

@@ -1,10 +1,13 @@
-use std::net::SocketAddr;
+use std::{net::SocketAddr, path::PathBuf};
 
+use fxhash::FxHashMap;
+use log::info;
 use serde::Deserialize;
 use structopt::StructOpt;
 use structopt_toml::StructOptToml;
+use toml::Value;
 
-use darkfi::net::settings::SettingsOpt;
+use darkfi::{net::settings::SettingsOpt, Result};
 
 pub const CONFIG_FILE: &str = "ircd_config.toml";
 pub const CONFIG_FILE_CONTENTS: &str = include_str!("../ircd_config.toml");
@@ -17,18 +20,95 @@ pub struct Args {
     /// Sets a custom config file
     #[structopt(long)]
     pub config: Option<String>,
+
     /// JSON-RPC listen URL
     #[structopt(long = "rpc", default_value = "tcp://127.0.0.1:11055")]
     pub rpc_listen: String,
+
     /// IRC listen URL
     #[structopt(long = "irc", default_value = "127.0.0.1:11066")]
     pub irc_listen: SocketAddr,
+
     /// Sets Datastore Path
     #[structopt(long, default_value = "~/.config/ircd")]
     pub datastore: String,
+
+    /// Generate a new NaCl secret and exit
+    #[structopt(long)]
+    pub gen_secret: bool,
+
+    /// Autojoin channels
+    #[structopt(long)]
+    pub autojoin: Vec<String>,
+
     #[structopt(flatten)]
     pub net: SettingsOpt,
+
     /// Increase verbosity
     #[structopt(short, parse(from_occurrences))]
     pub verbose: u8,
 }
+
+/// 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<String>,
+    /// Optional NaCl box for the channel, used for {en,de}cryption.
+    pub salt_box: Option<crypto_box::Box>,
+}
+
+impl ChannelInfo {
+    pub fn new() -> Result<Self> {
+        Ok(Self { topic: None, salt_box: None })
+    }
+}
+
+/// Parse the configuration file for any configured channels and return
+/// a map containing said configurations.
+pub fn parse_configured_channels(config_file: &PathBuf) -> Result<FxHashMap<String, ChannelInfo>> {
+    let toml_contents = std::fs::read_to_string(config_file)?;
+    let mut ret = FxHashMap::default();
+
+    match toml::from_str(&toml_contents)? {
+        Value::Table(map) => {
+            if map.contains_key("channel") && map["channel"].is_table() {
+                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") {
+                        channel_info.topic = Some(chan.1["topic"].as_str().unwrap().to_string());
+                    }
+
+                    if chan.1.as_table().unwrap().contains_key("secret") {
+                        // Build the NaCl box
+                        let s = chan.1["secret"].as_str().unwrap();
+                        let bytes: [u8; 32] = bs58::decode(s).into_vec()?.try_into().unwrap();
+                        let secret = crypto_box::SecretKey::from(bytes);
+                        let public = secret.public_key();
+                        let msg_box = crypto_box::Box::new(&public, &secret);
+                        channel_info.salt_box = Some(msg_box);
+                    }
+
+                    ret.insert(chan.0.to_string(), channel_info);
+                }
+            }
+        }
+        _ => {}
+    };
+
+    Ok(ret)
+}