settings.rs
1 /* This file is part of DarkFi (https://dark.fi) 2 * 3 * Copyright (C) 2020-2025 Dyne.org foundation 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation, either version 3 of the 8 * License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU Affero General Public License for more details. 14 * 15 * You should have received a copy of the GNU Affero General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19 use std::{ 20 collections::{HashMap, HashSet}, 21 sync::Arc, 22 time::UNIX_EPOCH, 23 }; 24 25 use crypto_box::PublicKey; 26 use darkfi::{Error::ParseFailed, Result}; 27 use darkfi_sdk::{crypto::pasta_prelude::PrimeField, pasta::pallas}; 28 use tracing::info; 29 30 use crate::{ 31 crypto::rln::{closest_epoch, RlnIdentity}, 32 irc::{IrcChannel, IrcContact}, 33 }; 34 35 /// Parse configured autojoin channels from a TOML map. 36 /// 37 /// ```toml 38 /// autojoin = ["#dev", "#memes"] 39 /// ``` 40 pub fn parse_autojoin_channels(data: &toml::Value) -> Result<Vec<String>> { 41 let mut ret = vec![]; 42 43 let Some(autojoin) = data.get("autojoin") else { return Ok(ret) }; 44 let Some(autojoin) = autojoin.as_array() else { 45 return Err(ParseFailed("autojoin not an array")) 46 }; 47 48 for item in autojoin { 49 let Some(channel) = item.as_str() else { 50 return Err(ParseFailed("autojoin channel not a string")) 51 }; 52 53 if !channel.starts_with('#') { 54 return Err(ParseFailed("autojoin channel not a valid channel")) 55 } 56 57 if ret.contains(&channel.to_string()) { 58 return Err(ParseFailed("Duplicate autojoin channel found")) 59 } 60 61 ret.push(channel.to_string()); 62 } 63 64 Ok(ret) 65 } 66 67 pub fn list_configured_contacts( 68 data: &toml::Value, 69 ) -> Result<HashMap<String, (PublicKey, crypto_box::SecretKey)>> { 70 let mut ret = HashMap::new(); 71 72 let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) }; 73 let Some(contacts) = table.get("contact") else { return Ok(ret) }; 74 let Some(contacts) = contacts.as_table() else { 75 return Err(ParseFailed("`contact` not a map")) 76 }; 77 78 for (name, items) in contacts { 79 let Some(public_str) = items.get("dm_chacha_public") else { 80 return Err(ParseFailed("Invalid contact configuration dm_chacha_public missing")) 81 }; 82 83 let Some(public_str) = public_str.as_str() else { 84 return Err(ParseFailed("dm_chacha_public not a string")) 85 }; 86 87 let Ok(public_bytes) = bs58::decode(public_str).into_vec() else { 88 return Err(ParseFailed("Invalid base58 for contact pubkey")) 89 }; 90 91 if public_bytes.len() != 32 { 92 return Err(ParseFailed("Invalid contact pubkey (not 32 bytes)")) 93 } 94 95 let public_bytes: [u8; 32] = public_bytes.try_into().unwrap(); 96 97 let public = crypto_box::PublicKey::from(public_bytes); 98 99 if ret.contains_key(name) { 100 return Err(ParseFailed("Duplicate contact found")) 101 } 102 103 // Parse the secret key for that specific contact 104 let Some(my_secret) = items.get("my_dm_chacha_secret") else { 105 return Err(ParseFailed("Invalid contact configuration my_dm_chacha_secret missing. \ 106 You can generate a keypair with: 'darkirc --gen-chacha-keypair' and then add that keypair to your config toml file.")) 107 }; 108 109 let Some(my_secret_str) = my_secret.as_str() else { 110 return Err(ParseFailed("my_dm_chacha_secret not a string")) 111 }; 112 113 let Ok(my_secret_bytes) = bs58::decode(my_secret_str).into_vec() else { 114 return Err(ParseFailed("my_dm_chacha_secret not valid base58")) 115 }; 116 117 if my_secret_bytes.len() != 32 { 118 return Err(ParseFailed("my_dm_chacha_secret not 32 bytes long")) 119 } 120 121 let my_secret_bytes: [u8; 32] = my_secret_bytes.try_into().unwrap(); 122 123 let my_secret = crypto_box::SecretKey::from(my_secret_bytes); 124 125 ret.insert(name.to_string(), (public, my_secret)); 126 } 127 128 Ok(ret) 129 } 130 131 /// Parse configured contacts from a TOML map. 132 /// 133 /// ```toml 134 /// [contact."anon"] 135 /// dm_chacha_public = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM" 136 /// my_dm_chacha_secret = "A3mLrq4aW9UkFVY4zCfR2aLdEEWVUdH4u8v4o2dgi4kC" 137 /// ``` 138 #[allow(clippy::type_complexity)] 139 pub fn parse_configured_contacts(data: &toml::Value) -> Result<HashMap<String, IrcContact>> { 140 let mut ret = HashMap::new(); 141 142 let contacts = list_configured_contacts(data)?; 143 if contacts.is_empty() { 144 return Ok(ret); 145 } 146 147 for (name, (public, my_secret)) in contacts { 148 let saltbox: Arc<crypto_box::ChaChaBox> = 149 Arc::new(crypto_box::ChaChaBox::new(&public, &my_secret)); 150 let self_saltbox: Arc<crypto_box::ChaChaBox> = 151 Arc::new(crypto_box::ChaChaBox::new(&my_secret.public_key(), &my_secret)); 152 153 if ret.contains_key(&name) { 154 return Err(ParseFailed("Duplicate contact found")) 155 } 156 157 info!("Instantiated ChaChaBox for contact \"{name}\""); 158 ret.insert(name.to_string(), IrcContact { saltbox, self_saltbox }); 159 } 160 161 Ok(ret) 162 } 163 164 /// Parse configured RLN identity from a TOML map. 165 /// 166 /// ```toml 167 /// [rln] 168 /// nullifier = "6EGKCm3FdSK3fySbjY19pxG49aB34poXhaepsW5NMxFB" 169 /// trapdoor = "dCbf5fD2w3K9eYHA2ppgio3ui12tSMZXnEGm8dHS5x6" 170 /// user_message_limit = 100 171 /// ``` 172 pub fn parse_rln_identity(data: &toml::Value) -> Result<Option<RlnIdentity>> { 173 let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) }; 174 let Some(rlninfo) = table.get("rln") else { return Ok(None) }; 175 176 let Some(nullifier) = rlninfo.get("nullifier") else { 177 return Err(ParseFailed("RLN identity nullifier missing")) 178 }; 179 180 let Some(trapdoor) = rlninfo.get("trapdoor") else { 181 return Err(ParseFailed("RLN identity trapdoor missing")) 182 }; 183 184 let Some(msglimit) = rlninfo.get("user_message_limit") else { 185 return Err(ParseFailed("RLN user message limit missing")) 186 }; 187 188 // Decode 189 let identity_nullifier = if let Some(nullifier) = nullifier.as_str() { 190 let Ok(nullifier_bytes) = bs58::decode(nullifier).into_vec() else { 191 return Err(ParseFailed("RLN nullifier not valid base58")) 192 }; 193 194 if nullifier_bytes.len() != 32 { 195 return Err(ParseFailed("RLN nullifier not 32 bytes long")) 196 } 197 198 let Some(identity_nullifier) = 199 pallas::Base::from_repr(nullifier_bytes.try_into().unwrap()).into() 200 else { 201 return Err(ParseFailed("RLN nullifier not a pallas base field element")) 202 }; 203 204 identity_nullifier 205 } else { 206 return Err(ParseFailed("RLN nullifier not a string")) 207 }; 208 209 let identity_trapdoor = if let Some(trapdoor) = trapdoor.as_str() { 210 let Ok(trapdoor_bytes) = bs58::decode(trapdoor).into_vec() else { 211 return Err(ParseFailed("RLN trapdoor not valid base58")) 212 }; 213 214 if trapdoor_bytes.len() != 32 { 215 return Err(ParseFailed("RLN trapdoor not 32 bytes long")) 216 } 217 218 let Some(identity_trapdoor) = 219 pallas::Base::from_repr(trapdoor_bytes.try_into().unwrap()).into() 220 else { 221 return Err(ParseFailed("RLN trapdoor not a pallas base field element")) 222 }; 223 224 identity_trapdoor 225 } else { 226 return Err(ParseFailed("RLN trapdoor not a string")) 227 }; 228 229 let user_message_limit = if let Some(msglimit) = msglimit.as_float() { 230 msglimit as u64 231 } else { 232 return Err(ParseFailed("RLN user message limit not a number")) 233 }; 234 235 Ok(Some(RlnIdentity { 236 nullifier: identity_nullifier, 237 trapdoor: identity_trapdoor, 238 user_message_limit, 239 // TODO: FIXME: We should probably keep track of these rather than 240 // resetting here 241 message_id: 1, 242 last_epoch: closest_epoch(UNIX_EPOCH.elapsed().unwrap().as_secs()), 243 })) 244 } 245 246 /// Parse a TOML string for any configured channels and return 247 /// a map containing said configurations. 248 /// 249 /// ```toml 250 /// [channel."#memes"] 251 /// secret = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM" 252 /// topic = "Dank Memes" 253 /// ``` 254 pub fn parse_configured_channels(data: &toml::Value) -> Result<HashMap<String, IrcChannel>> { 255 let mut ret = HashMap::new(); 256 257 let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) }; 258 let Some(chans) = table.get("channel") else { return Ok(ret) }; 259 let Some(chans) = chans.as_table() else { return Err(ParseFailed("`channel` not a map")) }; 260 261 for (name, items) in chans { 262 let mut chan = IrcChannel { topic: String::new(), nicks: HashSet::new(), saltbox: None }; 263 264 if let Some(topic) = items.get("topic") { 265 if let Some(topic) = topic.as_str() { 266 info!("Found configured topic for {name}: {topic}"); 267 chan.topic = topic.to_string(); 268 } else { 269 return Err(ParseFailed("Channel topic not a string")) 270 } 271 } 272 273 if let Some(secret) = items.get("secret") { 274 if let Some(secret) = secret.as_str() { 275 let Ok(secret_bytes) = bs58::decode(secret).into_vec() else { 276 return Err(ParseFailed("Channel secret not valid base58")) 277 }; 278 279 if secret_bytes.len() != 32 { 280 return Err(ParseFailed("Channel secret not 32 bytes long")) 281 } 282 283 let secret_bytes: [u8; 32] = secret_bytes.try_into().unwrap(); 284 let secret = crypto_box::SecretKey::from(secret_bytes); 285 let public = secret.public_key(); 286 chan.saltbox = Some(Arc::new(crypto_box::ChaChaBox::new(&public, &secret))); 287 info!("Configured NaCl box for channel {name}"); 288 } else { 289 return Err(ParseFailed("Channel secret not a string")) 290 } 291 } 292 293 info!("Configured channel {name}"); 294 ret.insert(name.to_string(), chan); 295 } 296 297 Ok(ret) 298 }