/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 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::{BufRead, BufReader}, path::PathBuf, sync::Arc, }; use darkfi::{ event_graph::Event, system::{StoppableTask, StoppableTaskPtr, Subscription}, util::path::expand_path, Error, Result, }; use darkfi_serial::{deserialize_async, deserialize_async_partial, serialize_async}; use futures_rustls::{ rustls::{ self, pki_types::{CertificateDer, PrivateKeyDer}, }, TlsAcceptor, }; use kvdb_overlay::{Batch, Database}; use smol::{ fs, lock::{Mutex, RwLock}, net::{SocketAddr, TcpListener}, prelude::{AsyncRead, AsyncWrite}, Executor, }; use tracing::{debug, error, info, warn}; use url::Url; use super::{ client::Client, services::nickserv::{ACCOUNTS_DB_PREFIX, ACCOUNTS_DEFAULT_TREE, ACCOUNTS_KEY_RLN_IDENTITY}, IrcChannel, IrcContact, }; use crate::{ crypto::{rln::RlnIdentity, saltbox}, pad, settings::{parse_autojoin_channels, parse_configured_channels, parse_configured_contacts}, unpad, DarkIrc, Privmsg, }; /// Max channel/nick length pub const MAX_NICK_LEN: usize = 24; /// Max message length pub const MAX_MSG_LEN: usize = 512; /// Kvdb tree storing every public (`#`-prefixed) IRC channel we have /// observed on the p2p network. Keys are channel names; values are empty. pub const SEEN_CHANNELS_TREE: &str = "darkirc_seen_channels"; /// Result of attempting to reserve the next RLN message slot. pub enum RlnMessageReservation { /// No active RLN identity is configured. MissingIdentity, /// The active identity has already used its epoch budget. BudgetExhausted, /// A message slot was persisted and can be used to build a proof. Reserved { identity: RlnIdentity, message_id: u64 }, } /// Persist the active RLN counter to the default mirror and matching account tree. async fn persist_rln_identity_counter(kvdb: &Database, identity: &RlnIdentity) -> Result<()> { let encoded = serialize_async(identity).await; let active_commitment = identity.commitment(); let mut updated_account = false; for name in kvdb.tree_names()? { let Some(account_name) = name.strip_prefix(ACCOUNTS_DB_PREFIX) else { continue }; if account_name == "default" || account_name.is_empty() { continue } let tree = kvdb.open_tree_default(&name)?; let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else { continue }; let Ok(stored): std::result::Result = deserialize_async(&blob).await else { continue }; if stored.commitment() == active_commitment { tree.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded)?; updated_account = true; } } if !updated_account { warn!( target: "darkirc::irc::server", "active RLN identity has no matching account tree; persisting default mirror only", ); } let default_db = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?; default_db.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded)?; kvdb.flush_default_mode_async().await?; Ok(()) } /// Reserve the next RLN message ID and persist it before proof creation. pub(crate) async fn reserve_rln_message_id_in_store( kvdb: &Database, active: &mut Option, now_millis: u64, ) -> Result { let Some(current) = active else { return Ok(RlnMessageReservation::MissingIdentity) }; let mut updated = *current; let Some(message_id) = updated.next_message_id(now_millis) else { return Ok(RlnMessageReservation::BudgetExhausted) }; persist_rln_identity_counter(kvdb, &updated).await?; *current = updated; Ok(RlnMessageReservation::Reserved { identity: updated, message_id }) } fn parse_tls_secret(reader: &mut R) -> Result> where R: BufRead, { let key = rustls_pemfile::pkcs8_private_keys(reader) .next() .ok_or(Error::ParseFailed("TLS key missing PKCS#8 private key"))? .map_err(|_| Error::ParseFailed("TLS key contains invalid PKCS#8 private key"))?; Ok(PrivateKeyDer::Pkcs8(key)) } fn parse_tls_cert(reader: &mut R) -> Result> where R: BufRead, { rustls_pemfile::certs(reader) .next() .ok_or(Error::ParseFailed("TLS certificate missing"))? .map_err(|_| Error::ParseFailed("TLS certificate contains invalid DER")) } fn tls_acceptor_from_pem( cert_reader: &mut CR, secret_reader: &mut KR, ) -> Result where CR: BufRead, KR: BufRead, { let secret = parse_tls_secret(secret_reader)?; let cert = parse_tls_cert(cert_reader)?; let config = rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(vec![cert], secret) .map_err(|_| Error::ParseFailed("TLS certificate and key are invalid"))?; Ok(TlsAcceptor::from(Arc::new(config))) } fn load_tls_acceptor(tls_cert: &str, tls_secret: &str) -> Result { let f = File::open(expand_path(tls_secret)?)?; let mut secret_reader = BufReader::new(f); let f = File::open(expand_path(tls_cert)?)?; let mut cert_reader = BufReader::new(f); tls_acceptor_from_pem(&mut cert_reader, &mut secret_reader) } async fn load_default_rln_identity(kvdb: &Database) -> Result> { let default_db = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?; let Some(blob) = default_db.get(ACCOUNTS_KEY_RLN_IDENTITY)? else { if default_db.is_empty()? { return Ok(None) } return Err(Error::ParseFailed("Default RLN account is missing identity record")) }; let identity: RlnIdentity = deserialize_async(&blob) .await .map_err(|_| Error::ParseFailed("Default RLN account identity is corrupted"))?; Ok(Some(identity)) } /// 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>, /// Static-DAG events whose broadcast is deferred until the /// EventGraph is synced. pub pending_static_broadcasts: Mutex)>>, /// Active client connections clients: Mutex>, /// IRC server Password pub password: String, } impl IrcServer { /// Reserve and persist the next RLN message slot before proof creation. pub async fn reserve_rln_message_id(&self, now_millis: u64) -> Result { let mut active = self.rln_identity.write().await; reserve_rln_message_id_in_store(&self.darkirc.kvdb, &mut active, now_millis).await } /// 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 (Some(tls_cert), Some(tls_secret)) = (tls_cert.as_ref(), tls_secret.as_ref()) else { return Err(Error::ParseFailed("TLS certificate and key are required")) }; Some(load_tls_acceptor(tls_cert, tls_secret)?) } _ => None, }; // Set the default RLN account if any. When RLN is disabled, avoid // loading account state that cannot affect outbound messages. let rln_identity = if darkirc.event_graph.rln_enabled() { let rln_identity = load_default_rln_identity(&darkirc.kvdb).await?; if rln_identity.is_some() { info!("Default RLN account set"); } rln_identity } else { info!("RLN disabled; skipping default RLN account load"); None }; 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()), rln_identity: RwLock::new(rln_identity), pending_static_broadcasts: Mutex::new(Vec::new()), clients: Mutex::new(HashMap::new()), password, }); // Load any channel/contact configuration. self_.rehash().await?; Ok(self_) } /// Drain `pending_static_broadcasts` and broadcast each entry. pub async fn drain_pending_static_broadcasts(&self) -> Result { let drained: Vec<(Event, Vec)> = { let mut guard = self.pending_static_broadcasts.lock().await; std::mem::take(&mut *guard) }; let n = drained.len(); for (event, blob) in drained { self.darkirc.event_graph.static_broadcast(event, blob).await?; } Ok(n) } /// 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 configured_channels = parse_configured_channels(&contents)?; // Parse configured contacts let contacts = parse_configured_contacts(&contents)?; // Persist unconfigured channels (joined from client, or autojoined without config) let channels = { let old_channels = self.channels.read().await.clone(); let unconfigured_channels: HashMap = old_channels .into_iter() .filter(|(chan_str, _)| !configured_channels.contains_key(chan_str)) .collect(); configured_channels.into_iter().chain(unconfigured_channels).collect() }; // Only if everything is fine, replace. *self.autojoin.write().await = autojoin; *self.channels.write().await = channels; *self.contacts.write().await = contacts; // Record configured public channels so `/LIST` can report them even // before any traffic is observed for them on the network. let names: Vec = self.channels.read().await.keys().cloned().collect(); for name in &names { self.record_seen_channel(name).await?; } 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 matches!( e.raw_os_error(), Some(libc::EAGAIN | libc::ECONNABORTED | libc::EPROTO | libc::EINTR) ) => { continue } 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; let incoming_st = self.darkirc.event_graph.static_pub.clone().subscribe().await; if let Err(e) = self .clone() .process_connection(stream, peer_addr, incoming, incoming_st, 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; let incoming_st = self.darkirc.event_graph.static_pub.clone().subscribe().await; if let Err(e) = self .clone() .process_connection(stream, peer_addr, incoming, incoming_st, 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, incoming_st: Subscription, ex: Arc>, ) -> Result<()> { let port = peer_addr.port(); let client = Client::new(self.clone(), incoming, incoming_st, 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(()) } /// Record a public (`#`-prefixed) IRC channel in the `SEEN_CHANNELS_TREE` /// so that `/LIST` can report channels observed on the p2p network. /// Private (encrypted) channels — those with a configured saltbox — are /// skipped. Idempotent. Emits a debug log the first time a given channel /// is seen. pub async fn record_seen_channel(&self, channel: &str) -> Result<()> { if !channel.starts_with('#') { return Ok(()) } // Skip private channels that have a configured saltbox. if let Some(chan) = self.channels.read().await.get(channel) { if chan.saltbox.is_some() { return Ok(()) } } let tree = self.darkirc.kvdb.open_tree_default(SEEN_CHANNELS_TREE)?; if tree.insert(channel.as_bytes(), &[])?.is_none() { debug!( target: "darkirc::irc::server", "Recorded new public channel: {channel}" ); } Ok(()) } /// Walk every stored event in the DAG and record all public (`#`-prefixed) /// channels into the `SEEN_CHANNELS_TREE`. Intended to be called once the /// event graph finishes syncing, so that `/LIST` reflects the full set of /// known public channels even before any new live traffic arrives. /// Only the raw wire channel field is inspected: encrypted (private) /// channels carry base58 ciphertext and are skipped. pub async fn populate_seen_channels(&self) -> Result { let events = self.darkirc.event_graph.order_events().await?; let tree = self.darkirc.kvdb.open_tree_default(SEEN_CHANNELS_TREE)?; let mut batch = Batch::default(); let mut count = 0usize; for event in events.iter() { let Ok((privmsg, _)) = deserialize_async_partial::(event.content()).await else { continue }; if privmsg.channel.starts_with('#') { batch.insert(privmsg.channel.as_bytes(), &[]); count += 1; } } if count > 0 { self.darkirc.kvdb.atomic_write(&[(&tree, &batch)])?; } Ok(count) } /// Try encrypting a given `Privmsg` if there is such a channel/contact. pub async fn try_encrypt(&self, privmsg: &mut Privmsg) { 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, &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) { // 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(&contact.saltbox, &[0x00; MAX_NICK_LEN]); // We will encrypt the dummy nick value using our own self saltbox, // so we can identify our messages. privmsg.nick = saltbox::encrypt(&contact.self_saltbox, &[0x00; MAX_NICK_LEN]); privmsg.msg = saltbox::encrypt(&contact.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 }; 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() { if saltbox::try_decrypt(&contact.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. let nick = if saltbox::try_decrypt(&contact.self_saltbox, &nick_ciphertext).is_some() { String::from(self_nickname) } else { name.to_string() }; let Some(msg_dec) = saltbox::try_decrypt(&contact.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 } } } #[cfg(test)] mod tests { use std::io::Cursor; use darkfi::{event_graph::rln::epoch_of, Error}; use darkfi_sdk::pasta::pallas; use darkfi_serial::deserialize_async; use super::*; fn test_identity(limit: u64) -> RlnIdentity { RlnIdentity { nullifier: pallas::Base::from(0xabc_u64), trapdoor: pallas::Base::from(0xdef_u64), user_message_limit: limit, message_id: 0, last_epoch: 0, } } #[test] fn tls_secret_parser_rejects_malformed_key() { let mut reader = Cursor::new(b"not a private key".as_slice()); assert!(matches!(parse_tls_secret(&mut reader), Err(Error::ParseFailed(_)))); } #[test] fn tls_cert_parser_rejects_malformed_cert() { let mut reader = Cursor::new(b"not a certificate".as_slice()); assert!(matches!(parse_tls_cert(&mut reader), Err(Error::ParseFailed(_)))); } #[test] fn load_default_rln_identity_returns_none_for_empty_tree() { smol::block_on(async { let (kvdb, _kvdb_folder) = Database::open_temp().unwrap(); let identity = load_default_rln_identity(&kvdb).await.unwrap(); assert!(identity.is_none()); }) } #[test] fn load_default_rln_identity_rejects_missing_identity_record() { smol::block_on(async { let (kvdb, _kvdb_folder) = Database::open_temp().unwrap(); let default = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE).unwrap(); default.insert(b"other", b"value").unwrap(); let err = match load_default_rln_identity(&kvdb).await { Ok(_) => panic!("expected missing identity record error"), Err(e) => e, }; assert!(matches!( err, Error::ParseFailed("Default RLN account is missing identity record") )); }) } #[test] fn load_default_rln_identity_rejects_corrupted_identity_record() { smol::block_on(async { let (kvdb, _kvdb_folder) = Database::open_temp().unwrap(); let default = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE).unwrap(); default.insert(ACCOUNTS_KEY_RLN_IDENTITY, b"not an identity").unwrap(); let err = match load_default_rln_identity(&kvdb).await { Ok(_) => panic!("expected corrupted identity record error"), Err(e) => e, }; assert!(matches!(err, Error::ParseFailed("Default RLN account identity is corrupted"))); }) } #[test] fn rln_message_reservation_persists_default_and_account_counters() { smol::block_on(async { let (kvdb, _kvdb_folder) = Database::open_temp().unwrap(); let account = kvdb.open_tree_default(&format!("{ACCOUNTS_DB_PREFIX}alice")).unwrap(); let default = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE).unwrap(); let identity = test_identity(2); let encoded = serialize_async(&identity).await; account.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded).unwrap(); default.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded).unwrap(); let now = 1_704_067_800_000; let mut active = Some(identity); let reservation = reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap(); let RlnMessageReservation::Reserved { identity: reserved, message_id } = reservation else { panic!("expected reservation") }; assert_eq!(message_id, 0); assert_eq!(reserved.message_id, 1); assert_eq!(reserved.last_epoch, epoch_of(now)); let stored_default: RlnIdentity = deserialize_async(&default.get(ACCOUNTS_KEY_RLN_IDENTITY).unwrap().unwrap()) .await .unwrap(); let stored_account: RlnIdentity = deserialize_async(&account.get(ACCOUNTS_KEY_RLN_IDENTITY).unwrap().unwrap()) .await .unwrap(); assert_eq!(stored_default.message_id, 1); assert_eq!(stored_account.message_id, 1); assert_eq!(stored_default.last_epoch, epoch_of(now)); assert_eq!(stored_account.last_epoch, epoch_of(now)); let reservation = reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap(); let RlnMessageReservation::Reserved { message_id, .. } = reservation else { panic!("expected second reservation") }; assert_eq!(message_id, 1); let exhausted = reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap(); assert!(matches!(exhausted, RlnMessageReservation::BudgetExhausted)); }) } }