nickserv.rs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 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. //! NickServ - account management for DarkIRC.
  19. //!
  20. //! Each registered account is one RLN identity stored under its own
  21. //! kvdb tree, named `darkirc_account_<name>`. A separate kvdb tree
  22. //! `darkirc_account_default` mirrors whichever identity is currently
  23. //! active; on startup, `IrcServer::new` reads from that tree to
  24. //! populate `IrcServer::rln_identity`, which is the field every
  25. //! outbound signal proof reads from.
  26. //!
  27. //! Commands:
  28. //!
  29. //! - `REGISTER <name> <nullifier> <trapdoor> <user_msg_limit>` -
  30. //! create a new account, build a registration proof, broadcast it.
  31. //! If no account was previously active, the new one becomes
  32. //! active.
  33. //! - `INFO` (no args) - list all registered accounts, mark the
  34. //! active one with `*`, show each account's RLN commitment.
  35. //! - `INFO <name>` - dump the secrets for `<name>` so they can be
  36. //! copied to a different machine or a config file. Output
  37. //! includes a warning that secrets are appearing in scrollback.
  38. //! - `SET <name>` - swap the active identity to `<name>`. The
  39. //! change is persisted (next restart will load the same one) and
  40. //! takes effect for the next outbound message.
  41. //! - `DEREGISTER <name>` - drop the account's kvdb tree. Refuses
  42. //! if `<name>` is currently active (the user must SET away first
  43. //! so they don't accidentally orphan their session). Local-only:
  44. //! the on-network registration is unaffected, so the same
  45. //! identity can be re-registered locally on another machine
  46. //! that holds the same secrets.
  47. //! - `SLASH <name> CONFIRM` - permanently burn the account on the
  48. //! network. Publishes a slash event into the static DAG; once
  49. //! accepted by peers, the identity is removed from the SMT
  50. //! network-wide and CANNOT be re-registered. The slash blob
  51. //! contains the identity_secret_hash in plaintext, so the
  52. //! secret becomes world-readable on the wire. This is intended
  53. //! for retiring an account whose secret has leaked, or for
  54. //! formally giving up an identity. The literal `CONFIRM` token
  55. //! is required to suppress accidents.
  56. //! - `HELP` - usage.
  57. use std::{str::SplitAsciiWhitespace, sync::Arc};
  58. use darkfi::{
  59. event_graph::{
  60. rln::{prepare_slash_proof_request, RLNNode, RlnProver, SlashBlob, GENESIS_USER_MSG_LIMIT},
  61. Event,
  62. },
  63. util::memory::log_memory,
  64. Result,
  65. };
  66. use darkfi_sdk::{crypto::pasta_prelude::PrimeField, pasta::pallas};
  67. use darkfi_serial::{deserialize_async, serialize_async};
  68. use smol::lock::RwLock;
  69. use super::super::{client::ReplyType, rpl::*, server::MAX_NICK_LEN};
  70. use crate::{crypto::rln::RlnIdentity, genesis_commits::is_pregenerated_commitment, IrcServer};
  71. pub const ACCOUNTS_DB_PREFIX: &str = "darkirc_account_";
  72. pub const ACCOUNTS_KEY_RLN_IDENTITY: &[u8] = b"rln_identity";
  73. const MAX_ACCOUNT_NAME_LEN: usize = MAX_NICK_LEN;
  74. /// Name of the kvdb tree that mirrors the currently-active identity.
  75. /// `IrcServer::new` reads this on startup.
  76. pub const ACCOUNTS_DEFAULT_TREE: &str = "darkirc_account_default";
  77. // /// Outcome of the `static_broadcast` call inside REGISTER. Used to
  78. // /// pick which "what just happened" notice we send back to the user.
  79. // #[derive(PartialEq, Eq)]
  80. // enum BroadcastStatus {
  81. // /// Local node was synced; broadcast was issued immediately.
  82. // Sent,
  83. // /// Local node was unsynced; the (event, blob) pair was queued
  84. // /// in `IrcServer::pending_static_broadcasts` and a watcher task
  85. // /// will broadcast it once sync completes.
  86. // Deferred,
  87. // }
  88. const NICKSERV_USAGE: &str = r#"***** NickServ Help *****
  89. NickServ allows a client to perform account management on DarkIRC.
  90. The following commands are available:
  91. INFO Display information on registrations.
  92. REGISTER Register an account.
  93. DEREGISTER Deregister an account locally.
  94. SET Select an account to use.
  95. SLASH Permanently retire an account network-wide.
  96. For more information on a NickServ command, type:
  97. /msg NickServ HELP <command>
  98. ***** End of Help *****
  99. "#;
  100. const NICKSERV_INFO_HELP: &str = r#"***** NickServ Help: INFO *****
  101. INFO with no arguments lists every registered account, marks the
  102. currently-active one with an asterisk, and shows each account's
  103. RLN commitment (a public identifier).
  104. INFO <account_name> dumps that account's secrets in a form ready to
  105. paste back into REGISTER on another machine. Be aware that this
  106. prints the secrets to your IRC client where they may end up in
  107. scrollback or logs.
  108. INFO
  109. INFO <account_name>
  110. ***** End of Help *****
  111. "#;
  112. const NICKSERV_REGISTER_HELP: &str = r#"***** NickServ Help: REGISTER *****
  113. REGISTER stores one of this network's pregenerated RLN identities
  114. under a local account name. Pregenerated identities are already
  115. bootstrapped into the static DAG; this command does not broadcast a
  116. public free-tier registration proof. The first account registered
  117. also becomes the active one.
  118. Use the nullifier/trapdoor pair from the network's pregenerated
  119. identity bundle. A freshly generated identity is rejected unless its
  120. commitment is already present in the configured pregenerated set.
  121. REGISTER <account_name> <nullifier> <trapdoor> <user_msg_limit>
  122. account_name - any local label, e.g. "alice" or "throwaway"
  123. nullifier - base58-encoded pallas::Base scalar
  124. trapdoor - base58-encoded pallas::Base scalar
  125. user_msg_limit - pregenerated account budget; must match the
  126. configured genesis limit
  127. ***** End of Help *****
  128. "#;
  129. const NICKSERV_SET_HELP: &str = r#"***** NickServ Help: SET *****
  130. SET swaps the active identity to the named account. Outbound
  131. messages from now on use that account's commitment and per-epoch
  132. budget. The choice is persisted across restarts.
  133. If you have used this identity recently from another node, wait
  134. one RLN epoch (10 minutes) before sending - the in-memory message
  135. counter resets on swap, and a clash with another node could cause
  136. a slash.
  137. SET <account_name>
  138. ***** End of Help *****
  139. "#;
  140. const NICKSERV_DEREGISTER_HELP: &str = r#"***** NickServ Help: DEREGISTER *****
  141. DEREGISTER removes an account from local storage. The on-network
  142. RLN registration is permanent and CANNOT be undone by this command;
  143. DEREGISTER only forgets the account locally. If you registered this
  144. identity on the network and care about reusing it, save its INFO
  145. output first.
  146. You cannot DEREGISTER the active account; SET to a different one
  147. first.
  148. If you want to permanently retire the identity NETWORK-WIDE so that
  149. no one (including you) can ever use it again, see SLASH.
  150. DEREGISTER <account_name>
  151. ***** End of Help *****
  152. "#;
  153. const NICKSERV_SLASH_HELP: &str = r#"***** NickServ Help: SLASH *****
  154. SLASH publishes a slash event for the named account into the static
  155. DAG. Once accepted by peers, the identity is removed from the
  156. network's identity tree and CANNOT be re-registered, by you or by
  157. anyone else. The slash blob contains the identity_secret_hash in
  158. plaintext, so any observer of the static DAG can see your secret
  159. after a SLASH - treat the secret as compromised after this command.
  160. Use cases:
  161. - Your identity's secrets leaked and you want to retire the
  162. account so an attacker can't impersonate you any longer.
  163. - You want to formally give up an identity (e.g. before disposing
  164. of a machine).
  165. This is irreversible and visible to the entire network. The
  166. literal `CONFIRM` token is required to proceed.
  167. You cannot SLASH the active account; SET to a different one first
  168. (if you have one). SLASH refuses while the local DAG is unsynced
  169. because the slash proof must be built against a canonical SMT root
  170. that peers will recognize.
  171. After a successful SLASH the local account tree is also dropped
  172. (equivalent to a DEREGISTER on top of the network slash).
  173. SLASH <account_name> CONFIRM
  174. ***** End of Help *****
  175. "#;
  176. /// NickServ implementation used for IRC account management
  177. pub struct NickServ {
  178. /// Client username
  179. pub _username: Arc<RwLock<String>>,
  180. /// Client nickname
  181. pub nickname: Arc<RwLock<String>>,
  182. /// Pointer to parent `IrcServer`
  183. pub server: Arc<IrcServer>,
  184. }
  185. /// Convenience helper - build a NickServ NOTICE reply.
  186. fn notice(nick: &str, body: impl Into<String>) -> ReplyType {
  187. ReplyType::Notice(("NickServ".to_string(), nick.to_string(), body.into()))
  188. }
  189. /// Convenience helper - build several NOTICE replies from an iterator
  190. /// of strings, one per line.
  191. fn notices<I, S>(nick: &str, lines: I) -> Vec<ReplyType>
  192. where
  193. I: IntoIterator<Item = S>,
  194. S: Into<String>,
  195. {
  196. lines.into_iter().map(|s| notice(nick, s)).collect()
  197. }
  198. impl NickServ {
  199. /// Instantiate a new `NickServ` for a client. This should be called after
  200. /// the user/nick are successfully registered.
  201. pub async fn new(
  202. _username: Arc<RwLock<String>>,
  203. nickname: Arc<RwLock<String>>,
  204. server: Arc<IrcServer>,
  205. ) -> Result<Self> {
  206. Ok(Self { _username, nickname, server })
  207. }
  208. /// Handle a `NickServ` query. This is the main command handler.
  209. /// Called from `command::handle_cmd_privmsg`.
  210. pub async fn handle_query(&self, query: &str) -> Result<Vec<ReplyType>> {
  211. let nick = self.nickname.read().await.to_string();
  212. let Some((command, mut tokens)) = parse_nickserv_command(query) else {
  213. return Ok(vec![ReplyType::Server((
  214. ERR_NOTEXTTOSEND,
  215. format!("{nick} :No text to send"),
  216. ))])
  217. };
  218. match command.to_uppercase().as_str() {
  219. "INFO" => self.handle_info(&nick, &mut tokens).await,
  220. "REGISTER" => self.handle_register(&nick, &mut tokens).await,
  221. "DEREGISTER" => self.handle_deregister(&nick, &mut tokens).await,
  222. "SET" => self.handle_set(&nick, &mut tokens).await,
  223. "SLASH" => self.handle_slash(&nick, &mut tokens).await,
  224. "HELP" => self.handle_help(&nick, &mut tokens).await,
  225. _ => self.handle_invalid(&nick).await,
  226. }
  227. }
  228. /// Handle the INFO command.
  229. ///
  230. /// `INFO` (no args) -> account list with active marker
  231. /// `INFO <account_name>` -> secrets dump for that account
  232. pub async fn handle_info(
  233. &self,
  234. nick: &str,
  235. tokens: &mut SplitAsciiWhitespace<'_>,
  236. ) -> Result<Vec<ReplyType>> {
  237. match tokens.next() {
  238. None => self.handle_info_list(nick).await,
  239. Some(account_name) => self.handle_info_account(nick, account_name).await,
  240. }
  241. }
  242. /// `INFO` with no args. Walks every `darkirc_account_*` tree,
  243. /// loads the identity to recover its commitment, and marks the
  244. /// one whose commitment matches the in-memory active identity.
  245. async fn handle_info_list(&self, nick: &str) -> Result<Vec<ReplyType>> {
  246. // The active identity's commitment is what we compare
  247. // against. We deliberately do NOT compare account names,
  248. // because the default tree stores the identity blob, not
  249. // a name. Comparing commitments means we get the right
  250. // answer even if the user renamed trees by hand.
  251. let active_commitment =
  252. self.server.rln_identity.read().await.as_ref().map(|id| id.commitment());
  253. let mut accounts: Vec<(String, RlnIdentity)> = Vec::new();
  254. for name in self.server.darkirc.kvdb.tree_names()? {
  255. // Skip the `default` mirror tree and anything that
  256. // isn't an account tree. Note we strip the prefix once
  257. // and reject the literal "default" suffix - we do NOT
  258. // want to list the mirror as if it were a separate
  259. // account.
  260. let Some(account_name) = name.strip_prefix(ACCOUNTS_DB_PREFIX) else { continue };
  261. if !is_valid_account_name(account_name) {
  262. continue
  263. }
  264. let tree = self.server.darkirc.kvdb.open_tree_default(&name)?;
  265. let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else { continue };
  266. // If a tree exists but the blob is malformed, skip
  267. // rather than failing the whole listing.
  268. let Ok(identity): std::result::Result<RlnIdentity, _> = deserialize_async(&blob).await
  269. else {
  270. continue
  271. };
  272. accounts.push((account_name.to_string(), identity));
  273. }
  274. if accounts.is_empty() {
  275. return Ok(vec![notice(nick, "No registered accounts. Use REGISTER to create one.")])
  276. }
  277. // Stable ordering for predictable output.
  278. accounts.sort_by(|a, b| a.0.cmp(&b.0));
  279. let mut lines: Vec<String> = Vec::with_capacity(accounts.len() + 2);
  280. lines.push("Registered accounts (* = active):".to_string());
  281. for (name, id) in &accounts {
  282. let active_mark = if Some(id.commitment()) == active_commitment { "*" } else { " " };
  283. // The commitment is a public value (it lives in the
  284. // SMT) so showing it here doesn't leak anything.
  285. let commitment_b58 = bs58::encode(id.commitment().to_repr()).into_string();
  286. lines.push(format!(
  287. " {active_mark} {name} limit={limit} commitment={commitment_b58}",
  288. limit = id.user_message_limit,
  289. ));
  290. }
  291. lines.push(
  292. "Use `INFO <account_name>` to show that account's secrets (REGISTER args).".to_string(),
  293. );
  294. Ok(notices(nick, lines))
  295. }
  296. /// `INFO <account_name>`. Dumps the secrets so the user can
  297. /// reconstruct the identity elsewhere.
  298. async fn handle_info_account(&self, nick: &str, account_name: &str) -> Result<Vec<ReplyType>> {
  299. let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
  300. if !is_valid_account_name(account_name) {
  301. return Ok(vec![notice(nick, "Invalid account name.")])
  302. }
  303. let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
  304. let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
  305. return Ok(vec![notice(nick, format!("No such account: \"{account_name}\""))])
  306. };
  307. let identity: RlnIdentity = match deserialize_async(&blob).await {
  308. Ok(v) => v,
  309. Err(_) => {
  310. return Ok(vec![notice(
  311. nick,
  312. format!("Account \"{account_name}\" exists but its data is corrupted."),
  313. )])
  314. }
  315. };
  316. let nullifier_b58 = bs58::encode(identity.nullifier.to_repr()).into_string();
  317. let trapdoor_b58 = bs58::encode(identity.trapdoor.to_repr()).into_string();
  318. let commitment_b58 = bs58::encode(identity.commitment().to_repr()).into_string();
  319. // Active marker.
  320. let active_commitment =
  321. self.server.rln_identity.read().await.as_ref().map(|id| id.commitment());
  322. let is_active = Some(identity.commitment()) == active_commitment;
  323. let lines = vec![
  324. format!("Account \"{account_name}\"{}:", if is_active { " (ACTIVE)" } else { "" }),
  325. format!(" commitment = {commitment_b58}"),
  326. format!(" user_msg_limit = {}", identity.user_message_limit),
  327. " --- secrets below; treat as a password ---".to_string(),
  328. format!(" nullifier = {nullifier_b58}"),
  329. format!(" trapdoor = {trapdoor_b58}"),
  330. "To re-register on another node, run:".to_string(),
  331. format!(
  332. " /msg NickServ REGISTER {account_name} {nullifier_b58} {trapdoor_b58} {limit}",
  333. limit = identity.user_message_limit,
  334. ),
  335. ];
  336. Ok(notices(nick, lines))
  337. }
  338. /// Handle the REGISTER command.
  339. ///
  340. /// `REGISTER <account_name> <nullifier> <trapdoor> <user_msg_limit>`
  341. pub async fn handle_register(
  342. &self,
  343. nick: &str,
  344. tokens: &mut SplitAsciiWhitespace<'_>,
  345. ) -> Result<Vec<ReplyType>> {
  346. if !self.server.darkirc.event_graph.rln_enabled() {
  347. return Ok(vec![notice(nick, "RLN is disabled; registration is not required.")])
  348. }
  349. // Gather the tokens
  350. let (
  351. Some(account_name),
  352. Some(identity_nullifier),
  353. Some(identity_trapdoor),
  354. Some(user_msg_limit),
  355. ) = (tokens.next(), tokens.next(), tokens.next(), tokens.next())
  356. else {
  357. return Ok(notices(
  358. nick,
  359. [
  360. "Invalid syntax.",
  361. "Use `REGISTER <account_name> <identity_nullifier> \
  362. <identity_trapdoor> <user_msg_limit>`.",
  363. "Run `darkirc --gen-rln-identity` to mint fresh secrets.",
  364. ],
  365. ))
  366. };
  367. // Reserved name. We use `default` for the mirror tree.
  368. if !is_valid_account_name(account_name) {
  369. return Ok(vec![notice(nick, "Invalid account name.")])
  370. }
  371. // Parse user_msg_limit defensively. The original code
  372. // panicked here, which would tear down the whole IRC
  373. // session on a typo.
  374. let user_msg_limit: u64 = match user_msg_limit.parse() {
  375. Ok(v) => v,
  376. Err(_) => {
  377. return Ok(vec![notice(nick, "Invalid user_msg_limit: must be a positive integer.")])
  378. }
  379. };
  380. if user_msg_limit == 0 {
  381. return Ok(vec![notice(nick, "Invalid user_msg_limit: must be at least 1.")])
  382. }
  383. // Parse the secrets. The original code used `.unwrap()` on
  384. // the `try_into` for the byte-length check, which would
  385. // panic on any input that wasn't exactly 32 bytes. Convert
  386. // to a graceful error instead.
  387. let identity_nullifier = match parse_pallas_b58(identity_nullifier) {
  388. Some(v) => v,
  389. None => return Ok(vec![notice(nick, "Invalid identity_nullifier.")]),
  390. };
  391. let identity_trapdoor = match parse_pallas_b58(identity_trapdoor) {
  392. Some(v) => v,
  393. None => return Ok(vec![notice(nick, "Invalid identity_trapdoor.")]),
  394. };
  395. // Create a new RLN identity. `last_epoch` is initialised to
  396. // 0 deterministically - the first persisted send reservation
  397. // will detect the rollover to the current wall-clock epoch.
  398. let new_rln_identity = RlnIdentity {
  399. nullifier: identity_nullifier,
  400. trapdoor: identity_trapdoor,
  401. user_message_limit: user_msg_limit,
  402. message_id: 0,
  403. last_epoch: 0,
  404. };
  405. let is_genesis = is_pregenerated_commitment(&new_rln_identity.commitment());
  406. if !is_genesis {
  407. return Ok(vec![notice(
  408. nick,
  409. "Registration is currently limited to pregenerated identities.",
  410. )])
  411. }
  412. if user_msg_limit != GENESIS_USER_MSG_LIMIT {
  413. return Ok(vec![notice(
  414. nick,
  415. format!("Genesis account must use user_msg_limit={}", GENESIS_USER_MSG_LIMIT),
  416. )])
  417. }
  418. // Open the per-account kvdb tree only after the identity has
  419. // passed the pregenerated-admission checks. Rejected identities
  420. // must not leave account state behind or become active locally.
  421. let db = self
  422. .server
  423. .darkirc
  424. .kvdb
  425. .open_tree_default(&format!("{ACCOUNTS_DB_PREFIX}{account_name}"))?;
  426. if !db.is_empty()? {
  427. return Ok(vec![notice(nick, "This account name is already registered.")])
  428. }
  429. // Store account.
  430. db.insert(ACCOUNTS_KEY_RLN_IDENTITY, &serialize_async(&new_rln_identity).await)?;
  431. // First-ever registration also becomes the active one. We
  432. // check the in-memory active identity (not the default
  433. // tree) because that's the source of truth at runtime.
  434. let became_active = self.server.rln_identity.read().await.is_none();
  435. if became_active {
  436. let db_default = self.server.darkirc.kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
  437. db_default
  438. .insert(ACCOUNTS_KEY_RLN_IDENTITY, &serialize_async(&new_rln_identity).await)?;
  439. *self.server.rln_identity.write().await = Some(new_rln_identity);
  440. }
  441. let mut replies =
  442. vec![notice(nick, format!("Successfully registered account \"{account_name}\""))];
  443. if became_active {
  444. replies.push(notice(nick, format!("\"{account_name}\" is now the active identity.")));
  445. } else {
  446. replies.push(notice(
  447. nick,
  448. format!("Use `SET {account_name}` to make this the active identity."),
  449. ));
  450. }
  451. // Pregenerated identities are already bootstrapped into
  452. // the static DAG. Future staked registration must add a
  453. // contract-backed network broadcast path here, after event
  454. // graph can verify the DarkFi attestation.
  455. Ok(replies)
  456. }
  457. /// Handle the DEREGISTER command.
  458. ///
  459. /// Refuses to drop the active account so the user doesn't
  460. /// orphan their session into a state where outbound messages
  461. /// would still reference an identity whose tree is gone. The
  462. /// network-side registration is permanent regardless - this
  463. /// only clears local state.
  464. pub async fn handle_deregister(
  465. &self,
  466. nick: &str,
  467. tokens: &mut SplitAsciiWhitespace<'_>,
  468. ) -> Result<Vec<ReplyType>> {
  469. let Some(account_name) = tokens.next() else {
  470. return Ok(vec![notice(nick, "Invalid syntax. Use `DEREGISTER <account_name>`.")])
  471. };
  472. if !is_valid_account_name(account_name) {
  473. return Ok(vec![notice(nick, "Invalid account name.")])
  474. }
  475. // Look up the account's commitment so we can compare
  476. // against the in-memory active identity.
  477. let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
  478. let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
  479. let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
  480. return Ok(vec![notice(nick, format!("No such account: \"{account_name}\""))])
  481. };
  482. let identity: RlnIdentity = match deserialize_async(&blob).await {
  483. Ok(v) => v,
  484. Err(_) => {
  485. // Corrupted account: allow the user to reclaim the
  486. // tree name. We can't tell whether it's active, so
  487. // err on the safe side and refuse if there IS an
  488. // active one. The only way out from a corrupted
  489. // active account is to manually surgery kvdb.
  490. if self.server.rln_identity.read().await.is_some() {
  491. return Ok(vec![notice(
  492. nick,
  493. format!(
  494. "Account \"{account_name}\" data is corrupted; refusing to \
  495. auto-deregister while another identity is active. SET to \
  496. a clean account first, then retry."
  497. ),
  498. )])
  499. }
  500. self.server.darkirc.kvdb.drop_tree(&tree_name)?;
  501. return Ok(vec![notice(
  502. nick,
  503. format!("Dropped corrupted account \"{account_name}\"."),
  504. )])
  505. }
  506. };
  507. // Refuse if active.
  508. if let Some(active) = self.server.rln_identity.read().await.as_ref() {
  509. if active.commitment() == identity.commitment() {
  510. return Ok(notices(
  511. nick,
  512. [
  513. format!(
  514. "\"{account_name}\" is the active identity; refusing to deregister."
  515. ),
  516. "Use `SET <other_account>` first to switch away.".to_string(),
  517. ],
  518. ))
  519. }
  520. }
  521. // Drop the tree.
  522. self.server.darkirc.kvdb.drop_tree(&tree_name)?;
  523. Ok(vec![notice(nick, format!("Successfully deregistered account \"{account_name}\""))])
  524. }
  525. /// Handle the SET command. Swaps the active identity and
  526. /// persists the choice to the default-mirror tree so it
  527. /// survives a restart.
  528. pub async fn handle_set(
  529. &self,
  530. nick: &str,
  531. tokens: &mut SplitAsciiWhitespace<'_>,
  532. ) -> Result<Vec<ReplyType>> {
  533. if !self.server.darkirc.event_graph.rln_enabled() {
  534. return Ok(vec![notice(nick, "RLN is disabled; SET has no effect.")])
  535. }
  536. let Some(account_name) = tokens.next() else {
  537. return Ok(vec![notice(nick, "Invalid syntax. Use `SET <account_name>`.")])
  538. };
  539. if !is_valid_account_name(account_name) {
  540. return Ok(vec![notice(nick, "Invalid account name.")])
  541. }
  542. let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
  543. let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
  544. let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
  545. return Ok(notices(
  546. nick,
  547. [
  548. format!("No such account: \"{account_name}\""),
  549. "Use INFO to list registered accounts.".to_string(),
  550. ],
  551. ))
  552. };
  553. let identity: RlnIdentity = match deserialize_async(&blob).await {
  554. Ok(v) => v,
  555. Err(_) => {
  556. return Ok(vec![notice(
  557. nick,
  558. format!("Account \"{account_name}\" data is corrupted."),
  559. )])
  560. }
  561. };
  562. // No-op if it's already active. Cheaper than rewriting the
  563. // default tree, and silences a confusing "now active" line
  564. // for users who SET twice in a row.
  565. let already_active = match self.server.rln_identity.read().await.as_ref() {
  566. Some(active) => active.commitment() == identity.commitment(),
  567. None => false,
  568. };
  569. if already_active {
  570. return Ok(vec![notice(
  571. nick,
  572. format!("\"{account_name}\" is already the active identity."),
  573. )])
  574. }
  575. // Persist the choice. We write the freshly-loaded blob
  576. // (not the in-memory identity, which would have stale
  577. // counter state if it were the previously-active one)
  578. // because the default tree is meant to mirror an account
  579. // tree exactly.
  580. let db_default = self.server.darkirc.kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
  581. db_default.insert(ACCOUNTS_KEY_RLN_IDENTITY, blob.as_ref())?;
  582. // Swap in-memory. The loaded identity includes any persisted
  583. // counter state from its account tree; future sends reserve and
  584. // flush the next slot before proof creation.
  585. *self.server.rln_identity.write().await = Some(identity);
  586. Ok(notices(
  587. nick,
  588. [
  589. format!("Active identity is now \"{account_name}\"."),
  590. "If you have used this identity recently from another node, wait one \
  591. RLN epoch (10 minutes) before sending to avoid a counter clash."
  592. .to_string(),
  593. ],
  594. ))
  595. }
  596. /// Handle the SLASH command. Publishes a network-wide slash for
  597. /// the named account, which permanently retires the identity
  598. /// from the SMT. Unlike DEREGISTER (local only), this affects
  599. /// the entire network and cannot be undone.
  600. ///
  601. /// Refusal rules:
  602. ///
  603. /// - The literal `CONFIRM` token is required as the second arg
  604. /// to suppress accidents.
  605. /// - The named account must exist locally (we need its secrets
  606. /// to construct the slash proof).
  607. /// - The account must NOT be the active identity. The user has
  608. /// to SET away first - if the goal is to slash the active
  609. /// identity the user has to acknowledge they're losing it.
  610. /// - The local DAG must be synced. The slash proof bakes in
  611. /// the current SMT root as a public input; if we built it
  612. /// while unsynced, the root might be one no peer recognizes,
  613. /// and the slash would be silently rejected. Better to fail
  614. /// loudly here than to broadcast a no-op event.
  615. ///
  616. /// On success: build proof, broadcast the slash event through
  617. /// the same canonical pipeline as REGISTER, and drop the local
  618. /// account tree. Network state and local state both reflect
  619. /// the retirement after this returns.
  620. pub async fn handle_slash(
  621. &self,
  622. nick: &str,
  623. tokens: &mut SplitAsciiWhitespace<'_>,
  624. ) -> Result<Vec<ReplyType>> {
  625. if !self.server.darkirc.event_graph.rln_enabled() {
  626. return Ok(vec![notice(nick, "RLN is disabled; SLASH is unavailable.")])
  627. }
  628. let Some(account_name) = tokens.next() else {
  629. return Ok(notices(
  630. nick,
  631. [
  632. "Invalid syntax. Use `SLASH <account_name> CONFIRM`.",
  633. "WARNING: SLASH is permanent and network-wide. See HELP SLASH.",
  634. ],
  635. ))
  636. };
  637. if !is_valid_account_name(account_name) {
  638. return Ok(vec![notice(nick, "Invalid account name.")])
  639. }
  640. // The CONFIRM token is required to be a literal, not just
  641. // any non-empty arg, so a fat-fingered "SLASH alice yes"
  642. // doesn't go through.
  643. match tokens.next() {
  644. Some("CONFIRM") => {}
  645. _ => {
  646. return Ok(notices(
  647. nick,
  648. [
  649. format!(
  650. "SLASH requires explicit confirmation. Type: \
  651. SLASH {account_name} CONFIRM"
  652. ),
  653. "This is permanent and network-wide. See HELP SLASH for the \
  654. full warning."
  655. .to_string(),
  656. ],
  657. ))
  658. }
  659. }
  660. // Load the account.
  661. let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
  662. let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
  663. let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
  664. return Ok(vec![notice(nick, format!("No such account: \"{account_name}\""))])
  665. };
  666. let identity: RlnIdentity = match deserialize_async(&blob).await {
  667. Ok(v) => v,
  668. Err(_) => {
  669. return Ok(vec![notice(
  670. nick,
  671. format!("Account \"{account_name}\" data is corrupted."),
  672. )])
  673. }
  674. };
  675. // Refuse if active. Same rule as DEREGISTER, with stronger
  676. // wording because the consequence is harsher.
  677. if let Some(active) = self.server.rln_identity.read().await.as_ref() {
  678. if active.commitment() == identity.commitment() {
  679. return Ok(notices(
  680. nick,
  681. [
  682. format!("\"{account_name}\" is the active identity; refusing to slash."),
  683. "Use `SET <other_account>` first if you genuinely want to slash \
  684. this identity."
  685. .to_string(),
  686. ],
  687. ))
  688. }
  689. }
  690. // Refuse while unsynced. The slash proof's public input
  691. // includes the current SMT root, which peers verify against
  692. // their own historical-roots table. A pre-sync local root
  693. // is unlikely to match anything peers know about, so the
  694. // slash would be silently dropped on the receive side.
  695. let evgr = &self.server.darkirc.event_graph;
  696. if !evgr.is_synced() {
  697. return Ok(notices(
  698. nick,
  699. [
  700. "Cannot SLASH while the local DAG is unsynced.",
  701. "Wait for sync to complete and try again.",
  702. ],
  703. ))
  704. }
  705. // Build the slash proof. The request contains
  706. // identity_secret_hash (NOT the raw nullifier+trapdoor pair)
  707. // because that's what SSS would recover in the misbehavior path.
  708. // The expensive proof work runs through the RLN prover boundary so a
  709. // future trusted remote prover can implement the same API.
  710. let identity_secret_hash = identity.identity_secret_hash();
  711. let request = {
  712. let id_state = evgr.rln_identity_state()?.read().await;
  713. prepare_slash_proof_request(identity_secret_hash, &id_state)
  714. };
  715. let root = request.merkle_root;
  716. log_memory("before slash proving");
  717. let proof = evgr.rln_zk_keys()?.prove_slash(request).await?.proof;
  718. log_memory("after slash proving");
  719. let slash_blob = SlashBlob { proof, identity_secret_hash, merkle_root: root };
  720. let blob_bytes = serialize_async(&slash_blob).await;
  721. let rln_node = RLNNode::Slashing(identity.commitment());
  722. let event = Event::new_static(serialize_async(&rln_node).await, evgr).await?;
  723. // Commit through the verified static-event pipeline so durable event
  724. // storage stays ahead of RLN side tables, while subscribers still see
  725. // the event only after the local RLN state has been updated.
  726. evgr.commit_verified_static_event(&event, &blob_bytes, &rln_node).await?;
  727. evgr.static_broadcast(event, blob_bytes).await?;
  728. // Drop the local account tree. The on-network slash makes
  729. // the account unusable anyway, so keeping the tree around
  730. // would be misleading (it would show up in INFO as if it
  731. // were still registered).
  732. self.server.darkirc.kvdb.drop_tree(&tree_name)?;
  733. Ok(notices(
  734. nick,
  735. [
  736. format!("SLASHED \"{account_name}\". The identity is permanently retired."),
  737. "The slash event has been broadcast to peers; once propagated, the \
  738. commitment is removed from the network's identity tree."
  739. .to_string(),
  740. "Local account state has also been dropped.".to_string(),
  741. ],
  742. ))
  743. }
  744. /// Reply to the HELP command.
  745. ///
  746. /// `HELP` (no args) -> top-level usage
  747. /// `HELP <command_name>` -> per-command help block
  748. pub async fn handle_help(
  749. &self,
  750. nick: &str,
  751. tokens: &mut SplitAsciiWhitespace<'_>,
  752. ) -> Result<Vec<ReplyType>> {
  753. let body = match tokens.next() {
  754. None => NICKSERV_USAGE,
  755. Some(sub) => match sub.to_uppercase().as_str() {
  756. "INFO" => NICKSERV_INFO_HELP,
  757. "REGISTER" => NICKSERV_REGISTER_HELP,
  758. "SET" => NICKSERV_SET_HELP,
  759. "DEREGISTER" => NICKSERV_DEREGISTER_HELP,
  760. "SLASH" => NICKSERV_SLASH_HELP,
  761. "HELP" => NICKSERV_USAGE,
  762. _ => {
  763. return Ok(vec![notice(
  764. nick,
  765. format!("No help available for \"{sub}\". Try `HELP`."),
  766. )])
  767. }
  768. },
  769. };
  770. Ok(notices(nick, body.lines().map(str::to_string)))
  771. }
  772. /// Reply to an invalid command
  773. pub async fn handle_invalid(&self, nick: &str) -> Result<Vec<ReplyType>> {
  774. Ok(notices(
  775. nick,
  776. ["Invalid NickServ command.", "Use /msg NickServ HELP for a NickServ command listing."],
  777. ))
  778. }
  779. }
  780. fn is_account_name_char(byte: u8) -> bool {
  781. byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')
  782. }
  783. /// Return true when a local account name is safe as a kvdb tree suffix.
  784. fn is_valid_account_name(account_name: &str) -> bool {
  785. account_name != "default" &&
  786. !account_name.is_empty() &&
  787. account_name.len() <= MAX_ACCOUNT_NAME_LEN &&
  788. account_name.bytes().all(is_account_name_char)
  789. }
  790. /// Parse a NickServ PRIVMSG body into the service command and remaining arguments.
  791. fn parse_nickserv_command(query: &str) -> Option<(&str, SplitAsciiWhitespace<'_>)> {
  792. let mut tokens = query.split_ascii_whitespace();
  793. tokens.next()?;
  794. let command = tokens.next()?.strip_prefix(':')?;
  795. if command.is_empty() {
  796. return None
  797. }
  798. Some((command, tokens))
  799. }
  800. /// Decode a base58-encoded `pallas::Base` scalar. Returns `None`
  801. /// for any malformed input rather than panicking - this is called
  802. /// on user-supplied IRC tokens.
  803. fn parse_pallas_b58(s: &str) -> Option<pallas::Base> {
  804. let bytes = bs58::decode(s).into_vec().ok()?;
  805. let arr: [u8; 32] = bytes.try_into().ok()?;
  806. pallas::Base::from_repr(arr).into_option()
  807. }
  808. #[cfg(test)]
  809. mod tests {
  810. use super::parse_nickserv_command;
  811. #[test]
  812. fn parse_nickserv_command_accepts_colon_prefixed_command() {
  813. let (command, mut tokens) =
  814. parse_nickserv_command("NickServ :REGISTER alice n t 100").unwrap();
  815. assert_eq!(command, "REGISTER");
  816. assert_eq!(tokens.next(), Some("alice"));
  817. assert_eq!(tokens.next(), Some("n"));
  818. }
  819. #[test]
  820. fn parse_nickserv_command_rejects_bare_command() {
  821. assert!(parse_nickserv_command("NickServ REGISTER alice").is_none());
  822. }
  823. #[test]
  824. fn parse_nickserv_command_rejects_empty_command() {
  825. assert!(parse_nickserv_command("NickServ :").is_none());
  826. }
  827. #[test]
  828. fn account_name_validation_rejects_reserved_and_unsafe_names() {
  829. assert!(super::is_valid_account_name("alice_1"));
  830. assert!(!super::is_valid_account_name("default"));
  831. assert!(!super::is_valid_account_name(""));
  832. assert!(!super::is_valid_account_name("../alice"));
  833. assert!(!super::is_valid_account_name(&"a".repeat(super::MAX_ACCOUNT_NAME_LEN + 1)));
  834. }
  835. }