/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2025 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,
fs::File,
io::{BufReader, Cursor},
path::PathBuf,
sync::Arc,
};
use darkfi::{
event_graph::Event,
system::{StoppableTask, StoppableTaskPtr, Subscription},
util::path::expand_path,
zk::{empty_witnesses, ProvingKey, VerifyingKey, ZkCircuit},
zkas::ZkBinary,
Error, Result,
};
use darkfi_sdk::crypto::MerkleTree;
use darkfi_serial::serialize_async;
use futures_rustls::{
rustls::{self, pki_types::PrivateKeyDer},
TlsAcceptor,
};
use log::{debug, error, info, warn};
use sled_overlay::sled;
use smol::{
fs,
lock::{Mutex, RwLock},
net::{SocketAddr, TcpListener},
prelude::{AsyncRead, AsyncWrite},
Executor,
};
use url::Url;
use super::{client::Client, ChaChaBox, IrcChannel, IrcContact, Priv, Privmsg};
use crate::{
crypto::{
rln::{RlnIdentity, RLN2_SIGNAL_ZKBIN, RLN2_SLASH_ZKBIN},
saltbox,
},
settings::{
parse_autojoin_channels, parse_configured_channels, parse_configured_contacts,
parse_rln_identity,
},
DarkIrc,
};
/// Max channel/nick length
pub const MAX_NICK_LEN: usize = 24;
/// Max message length
pub const MAX_MSG_LEN: usize = 512;
/// IRC server instance
pub struct IrcServer {
/// DarkIrc instance
pub darkirc: Arc,
/// Path to the darkirc config file
config_path: PathBuf,
/// TCP listener
listener: TcpListener,
/// TLS acceptor
acceptor: Option,
/// Configured autojoin channels
pub autojoin: RwLock>,
/// Configured IRC channels
pub channels: RwLock>,
/// Configured IRC contacts
pub contacts: RwLock>,
/// Configured RLN identity
pub rln_identity: RwLock>,
/// Saltbox used to encrypt our nick in direct messages
saltbox: RwLock >>,
/// Active client connections
clients: Mutex>,
/// IRC server Password
pub password: String,
/// Persistent server storage
pub server_store: sled::Tree,
/// RLN identity storage
pub rln_identity_store: sled::Tree,
/// RLN Signal VerifyingKey
pub rln_signal_vk: VerifyingKey,
}
impl IrcServer {
/// Instantiate a new IRC server. This function will try to bind a TCP socket,
/// and optionally load a TLS certificate and key. To start the listening loop,
/// call `IrcServer::listen()`.
pub async fn new(
darkirc: Arc,
listen: Url,
tls_cert: Option,
tls_secret: Option,
config_path: PathBuf,
password: String,
) -> Result> {
let scheme = listen.scheme();
if scheme != "tcp" && scheme != "tcp+tls" {
error!("IRC server supports listening only on tcp:// or tcp+tls://");
return Err(Error::BindFailed(listen.to_string()))
}
if scheme == "tcp+tls" && (tls_cert.is_none() || tls_secret.is_none()) {
error!("You must provide a TLS certificate and key if you want a TLS server");
return Err(Error::BindFailed(listen.to_string()))
}
// Bind listener
let listen_addr = listen.socket_addrs(|| None)?[0];
let listener = TcpListener::bind(listen_addr).await?;
let acceptor = match scheme {
"tcp+tls" => {
// openssl genpkey -algorithm ED25519 > example.com.key
// openssl req -new -out example.com.csr -key example.com.key
// openssl x509 -req -in example.com.csr -signkey example.com.key -out example.com.crt
let f = File::open(expand_path(tls_secret.as_ref().unwrap())?)?;
let mut reader = BufReader::new(f);
let secret = PrivateKeyDer::Pkcs8(
rustls_pemfile::pkcs8_private_keys(&mut reader).next().unwrap().unwrap(),
);
let f = File::open(expand_path(tls_cert.as_ref().unwrap())?)?;
let mut reader = BufReader::new(f);
let cert = rustls_pemfile::certs(&mut reader).next().unwrap().unwrap();
let config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert], secret)
.unwrap();
let acceptor = TlsAcceptor::from(Arc::new(config));
Some(acceptor)
}
_ => None,
};
// Open persistent dbs
let server_store = darkirc.sled.open_tree("server_store")?;
let rln_identity_store = darkirc.sled.open_tree("rln_identity_store")?;
// Generate RLN proving and verifying keys, if needed
let rln_signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN)?;
let rln_signal_circuit =
ZkCircuit::new(empty_witnesses(&rln_signal_zkbin)?, &rln_signal_zkbin);
if server_store.get("rlnv2-diff-signal-pk")?.is_none() {
info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Signal ProvingKey");
let provingkey = ProvingKey::build(rln_signal_zkbin.k, &rln_signal_circuit);
let mut buf = vec![];
provingkey.write(&mut buf)?;
server_store.insert("rlnv2-diff-signal-pk", buf)?;
}
let rln_signal_vk = match server_store.get("rlnv2-diff-signal-vk")? {
Some(vk) => {
let mut reader = Cursor::new(vk);
VerifyingKey::read(&mut reader, rln_signal_circuit)?
}
None => {
info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Signal VerifyingKey");
let verifyingkey = VerifyingKey::build(rln_signal_zkbin.k, &rln_signal_circuit);
let mut buf = vec![];
verifyingkey.write(&mut buf)?;
server_store.insert("rlnv2-diff-signal-vk", buf)?;
verifyingkey
}
};
if server_store.get("rlnv2-diff-slash-pk")?.is_none() {
info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Slash ProvingKey");
let zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN)?;
let circuit = ZkCircuit::new(empty_witnesses(&zkbin).unwrap(), &zkbin);
let provingkey = ProvingKey::build(zkbin.k, &circuit);
let mut buf = vec![];
provingkey.write(&mut buf)?;
server_store.insert("rlnv2-diff-slash-pk", buf)?;
}
if server_store.get("rlnv2-diff-slash-vk")?.is_none() {
info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Slash VerifyingKey");
let zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN)?;
let circuit = ZkCircuit::new(empty_witnesses(&zkbin).unwrap(), &zkbin);
let verifyingkey = VerifyingKey::build(zkbin.k, &circuit);
let mut buf = vec![];
verifyingkey.write(&mut buf)?;
server_store.insert("rlnv2-diff-slash-vk", buf)?;
}
// Initialize RLN Incremental Merkle tree if necessary
if server_store.get("rln_identity_tree")?.is_none() {
let tree = MerkleTree::new(1);
server_store.insert("rln_identity_tree", serialize_async(&tree).await)?;
}
let self_ = Arc::new(Self {
darkirc,
config_path,
listener,
acceptor,
autojoin: RwLock::new(Vec::new()),
channels: RwLock::new(HashMap::new()),
contacts: RwLock::new(HashMap::new()),
saltbox: RwLock::new(None),
rln_identity: RwLock::new(None),
clients: Mutex::new(HashMap::new()),
password,
server_store,
rln_identity_store,
rln_signal_vk,
});
// Load any channel/contact configuration.
self_.rehash().await?;
Ok(self_)
}
/// Reload the darkirc configuration file and reconfigure channels and contacts.
pub async fn rehash(&self) -> Result<()> {
let contents = fs::read_to_string(&self.config_path).await?;
let contents = match toml::from_str(&contents) {
Ok(v) => v,
Err(e) => {
error!("Failed parsing TOML config: {}", e);
return Err(Error::ParseFailed("Failed parsing TOML config"))
}
};
// Parse autojoin channels
let autojoin = parse_autojoin_channels(&contents)?;
// Parse configured channels
let channels = parse_configured_channels(&contents)?;
// Parse configured contacts
let (contacts, saltbox) = parse_configured_contacts(&contents)?;
// Parse RLN identity
let rln_identity = parse_rln_identity(&contents)?;
// FIXME: This will remove clients' joined channels. They need to stay.
// Only if everything is fine, replace.
*self.autojoin.write().await = autojoin;
*self.channels.write().await = channels;
*self.contacts.write().await = contacts;
*self.saltbox.write().await = saltbox;
*self.rln_identity.write().await = rln_identity;
Ok(())
}
/// Start accepting new IRC connections.
pub async fn listen(self: Arc, ex: Arc>) -> Result<()> {
loop {
let (stream, peer_addr) = match self.listener.accept().await {
Ok((s, a)) => (s, a),
// As per usual accept(2) recommendations
Err(e) if e.raw_os_error().is_some() => match e.raw_os_error().unwrap() {
libc::EAGAIN | libc::ECONNABORTED | libc::EPROTO | libc::EINTR => continue,
_ => {
error!("[IRC SERVER] Failed accepting connection: {}", e);
return Err(e.into())
}
},
Err(e) => {
error!("[IRC SERVER] Failed accepting new connection: {}", e);
continue
}
};
match &self.acceptor {
// Expecting encrypted TLS connection
Some(acceptor) => {
let stream = match acceptor.accept(stream).await {
Ok(s) => s,
Err(e) => {
error!("[IRC SERVER] Failed accepting new TLS connection: {}", e);
continue
}
};
// Subscribe to incoming events and set up the connection.
let incoming = self.darkirc.event_graph.event_pub.clone().subscribe().await;
if let Err(e) = self
.clone()
.process_connection(stream, peer_addr, incoming, ex.clone())
.await
{
error!("[IRC SERVER] Failed processing new connection: {}", e);
continue
};
}
// Expecting plain TCP connection
None => {
// Subscribe to incoming events and set up the connection.
let incoming = self.darkirc.event_graph.event_pub.clone().subscribe().await;
if let Err(e) = self
.clone()
.process_connection(stream, peer_addr, incoming, ex.clone())
.await
{
error!("[IRC SERVER] Failed processing new connection: {}", e);
continue
};
}
}
info!("[IRC SERVER] Accepted new client connection at: {}", peer_addr);
}
}
/// IRC client connection process.
/// Sets up multiplexing between the server and client.
/// Detaches the connection as a `StoppableTask`.
async fn process_connection(
self: Arc,
stream: C,
peer_addr: SocketAddr,
incoming: Subscription,
ex: Arc>,
) -> Result<()> {
let port = peer_addr.port();
let client = Client::new(self.clone(), incoming, peer_addr).await?;
let conn_task = StoppableTask::new();
self.clients.lock().await.insert(port, conn_task.clone());
conn_task.clone().start(
async move { client.multiplex_connection(stream).await },
move |res| async move {
match res {
Ok(()) => info!("[IRC SERVER] Disconnected client from {}", peer_addr),
Err(e) => error!("[IRC SERVER] Disconnected client from {}: {}", peer_addr, e),
}
self.clone().clients.lock().await.remove(&port);
},
Error::ChannelStopped,
ex,
);
Ok(())
}
fn pad(string: &str) -> Vec {
let mut bytes = string.as_bytes().to_vec();
bytes.resize(MAX_NICK_LEN, 0x00);
bytes
}
fn unpad(vec: &mut Vec) {
if let Some(i) = vec.iter().rposition(|x| *x != 0) {
let new_len = i + 1;
vec.truncate(new_len);
}
}
/// Try encrypting a given `Privmsg` if there is such a channel/contact.
pub async fn try_encrypt(&self, privmsg: &mut T) {
if let Some((name, channel)) = self.channels.read().await.get_key_value(privmsg.channel()) {
if let Some(saltbox) = &channel.saltbox {
// We will use a dummy channel value of MAX_NICK_LEN,
// since its not used, so all encrypted messages look the same.
*privmsg.channel() = saltbox::encrypt(saltbox, &[0x00; MAX_NICK_LEN]);
// We will pad the name to MAX_NICK_LEN so they all look the same
*privmsg.nick() = saltbox::encrypt(saltbox, &Self::pad(privmsg.nick()));
*privmsg.msg() = saltbox::encrypt(saltbox, privmsg.msg().as_bytes());
debug!("Successfully encrypted message for {}", name);
return
}
};
if let Some((name, contact)) = self.contacts.read().await.get_key_value(privmsg.channel()) {
if let Some(saltbox) = &contact.saltbox {
// We will use dummy channel and nick values of MAX_NICK_LEN,
// since they are not used, so all encrypted messages look the same.
*privmsg.channel() = saltbox::encrypt(saltbox, &[0x00; MAX_NICK_LEN]);
// We will encrypt the dummy nick value using our own self saltbox,
// so we can identify our messages. We can safely unwrap here since
// we know that if contacts exist, our self saltbox does as well.
*privmsg.nick() = saltbox::encrypt(
self.saltbox.read().await.as_ref().unwrap(),
&[0x00; MAX_NICK_LEN],
);
*privmsg.msg() = saltbox::encrypt(saltbox, privmsg.msg().as_bytes());
debug!("Successfully encrypted message for {}", name);
}
};
}
/// Try decrypting a given potentially encrypted `Privmsg` object.
pub async fn try_decrypt(&self, privmsg: &mut Privmsg, self_nickname: &str) {
// If all fields have base58, then we can consider decrypting.
let channel_ciphertext = match bs58::decode(&privmsg.channel).into_vec() {
Ok(v) => v,
Err(_) => return,
};
let nick_ciphertext = match bs58::decode(&privmsg.nick).into_vec() {
Ok(v) => v,
Err(_) => return,
};
let msg_ciphertext = match bs58::decode(&privmsg.msg).into_vec() {
Ok(v) => v,
Err(_) => return,
};
// Now go through all 3 ciphertexts. We'll use intermediate buffers
// for decryption, iff all passes, we will return a modified
// (i.e. decrypted) privmsg, otherwise we return the original.
for (name, channel) in self.channels.read().await.iter() {
let Some(saltbox) = &channel.saltbox else { continue };
if saltbox::try_decrypt(saltbox, &channel_ciphertext).is_none() {
continue
};
let Some(mut nick_dec) = saltbox::try_decrypt(saltbox, &nick_ciphertext) else {
warn!(target: "darkirc::irc::server::try_decrypt", "Could not decrypt nick ciphertext for channel: {name}");
continue
};
let Some(msg_dec) = saltbox::try_decrypt(saltbox, &msg_ciphertext) else {
warn!(target: "darkirc::irc::server::try_decrypt", "Could not decrypt message ciphertext for channel: {name}");
continue
};
Self::unpad(&mut nick_dec);
privmsg.channel = name.to_string();
privmsg.nick = String::from_utf8_lossy(&nick_dec).into();
privmsg.msg = String::from_utf8_lossy(&msg_dec).into();
debug!("Successfully decrypted message for {}", name);
return
}
for (name, contact) in self.contacts.read().await.iter() {
let Some(saltbox) = &contact.saltbox else { continue };
if saltbox::try_decrypt(saltbox, &channel_ciphertext).is_none() {
continue
};
// Since everyone encrypts the dummy nick value with their self saltbox,
// we try to decrypt using our, to identify our messages. We can safely
// unwrap here since we know that if contacts exist, our self saltbox does as well.
let nick = if saltbox::try_decrypt(
self.saltbox.read().await.as_ref().unwrap(),
&nick_ciphertext,
)
.is_some()
{
String::from(self_nickname)
} else {
name.to_string()
};
let Some(msg_dec) = saltbox::try_decrypt(saltbox, &msg_ciphertext) else {
warn!(target: "darkirc::irc::server::try_decrypt", "Could not decrypt message ciphertext for contact: {name}");
continue
};
privmsg.channel = name.to_string();
privmsg.nick = nick;
privmsg.msg = String::from_utf8_lossy(&msg_dec).into();
debug!("Successfully decrypted message from {}", name);
return
}
}
}