Browse Source

app: working send/recv private DMs

jkds 2 weeks ago
parent
commit
9f5a2dad48
2 changed files with 96 additions and 44 deletions
  1. 3 0
      bin/app/src/error.rs
  2. 93 44
      bin/app/src/plugin/darkirc2.rs

+ 3 - 0
bin/app/src/error.rs

@@ -139,6 +139,9 @@ pub enum Error {
 
 
     #[error("Unknown anim ID")]
     #[error("Unknown anim ID")]
     GfxUnknownAnimID = 46,
     GfxUnknownAnimID = 46,
+
+    #[error("Contact not found")]
+    ContactNotFound = 47,
 }
 }
 
 
 impl From<sled::Error> for Error {
 impl From<sled::Error> for Error {

+ 93 - 44
bin/app/src/plugin/darkirc2.rs

@@ -451,9 +451,17 @@ impl DarkIrc2 {
                 }
                 }
             };
             };
 
 
-            // Try to decrypt messages (will decrypt encrypted channels/contacts in place)
+            // Route the message. An already-decrypted (plaintext) message names a
+            // channel we hold directly: encrypted channels arrive as base58
+            // ciphertext, so a channel key we recognise is plaintext by definition
+            // and is accepted as-is. Anything else must decrypt as a channel or DM;
+            // undecryptable traffic (base58 garbage in neither map) is silently dropped.
             let mut privmsg = privmsg;
             let mut privmsg = privmsg;
-            self.try_decrypt(&mut privmsg, &self.nick.get()).await;
+            // Is this a plaintext channel?
+            let is_plaintext = self.channels.read().await.contains_key(&privmsg.channel);
+            if !is_plaintext && !self.try_decrypt(&mut privmsg, &self.nick.get()).await {
+                continue;
+            }
 
 
             let mut timest = ev.header.timestamp;
             let mut timest = ev.header.timestamp;
             let msg_id = msg_id(&privmsg, timest);
             let msg_id = msg_id(&privmsg, timest);
@@ -488,24 +496,13 @@ impl DarkIrc2 {
                 timest = now_timest;
                 timest = now_timest;
             }
             }
 
 
-            // Strip off starting #
-            let mut channel = privmsg.channel;
-            if channel.is_empty() {
-                w!("Received privmsg with empty channel!");
-                continue
-            }
-            if channel.chars().next().unwrap() != '#' {
-                w!("Skipping encrypted channel: {channel}");
-                continue
-            }
-
             // Workaround for the chatview hack. This nick is off limits!
             // Workaround for the chatview hack. This nick is off limits!
             let mut nick = privmsg.nick;
             let mut nick = privmsg.nick;
             if nick == "NOTICE" {
             if nick == "NOTICE" {
                 nick = "noticer".to_string();
                 nick = "noticer".to_string();
             }
             }
 
 
-            self.notify_recv(channel, timest, msg_id, nick, privmsg.msg).await;
+            self.notify_recv(privmsg.channel, timest, msg_id, nick, privmsg.msg).await;
         }
         }
     }
     }
 
 
@@ -518,6 +515,11 @@ impl DarkIrc2 {
         nick: String,
         nick: String,
         msg: String,
         msg: String,
     ) {
     ) {
+        assert!(
+            channel.starts_with('#') || channel.starts_with('@'),
+            "notify_recv channel must be a \"#name\" channel or \"@name\" DM, got: {channel}"
+        );
+
         let mut arg_data = vec![];
         let mut arg_data = vec![];
         channel.encode(&mut arg_data).unwrap();
         channel.encode(&mut arg_data).unwrap();
         timestamp.encode(&mut arg_data).unwrap();
         timestamp.encode(&mut arg_data).unwrap();
@@ -568,7 +570,19 @@ impl DarkIrc2 {
         // Send text to channel
         // Send text to channel
         d!("Sending privmsg: {timest} {channel}: <{nick}> {msg}");
         d!("Sending privmsg: {timest} {channel}: <{nick}> {msg}");
         let mut msg = Privmsg { version: 0, msg_type: 0, channel, nick, msg };
         let mut msg = Privmsg { version: 0, msg_type: 0, channel, nick, msg };
-        self.try_encrypt(&mut msg).await;
+
+        // DM layers use the "@name" UI id; strip it to the bare contact key and
+        // require the contact to exist, else refuse to broadcast.
+        if let Some(bare) = msg.channel.strip_prefix('@').map(str::to_string) {
+            msg.channel = bare;
+            if self.try_encrypt_dm(&mut msg).await.is_err() {
+                e!("Refusing to send DM to unknown contact");
+                return;
+            }
+        } else {
+            assert!(msg.channel.starts_with('#'), "channel name must start with #");
+            self.try_encrypt_channel(&mut msg).await;
+        }
         let evgr = self.event_graph.clone();
         let evgr = self.event_graph.clone();
         let event = event_graph::Event::with_timestamp(timest, serialize_async(&msg).await, &evgr)
         let event = event_graph::Event::with_timestamp(timest, serialize_async(&msg).await, &evgr)
             .await
             .await
@@ -655,9 +669,8 @@ impl DarkIrc2 {
             let self_saltbox =
             let self_saltbox =
                 Arc::new(ChaChaBox::new(&self.dm_secret.public_key(), &self.dm_secret));
                 Arc::new(ChaChaBox::new(&self.dm_secret.public_key(), &self.dm_secret));
 
 
-            let full_name = format!("@{}", name);
-            contacts.insert(full_name, IrcContact { saltbox, self_saltbox });
-            i!("Loaded contact: @{name}");
+            contacts.insert(name.clone(), IrcContact { saltbox, self_saltbox });
+            i!("Loaded contact: {name}");
         }
         }
     }
     }
 
 
@@ -870,31 +883,47 @@ impl DarkIrc2 {
         *self.tasks.lock() = tasks;
         *self.tasks.lock() = tasks;
     }
     }
 
 
-    /// 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 {
-                privmsg.channel = saltbox::encrypt(saltbox, &[0x00; MAX_NICK_LEN]);
-                privmsg.nick = saltbox::encrypt(saltbox, &pad(&privmsg.nick));
-                privmsg.msg = saltbox::encrypt(saltbox, privmsg.msg.as_bytes());
-                d!("Successfully encrypted message for {name}");
-                return
-            }
+    /// Encrypt a channel `Privmsg` in place if the channel has a shared key.
+    /// Open channels with no key are left plaintext.
+    pub async fn try_encrypt_channel(&self, privmsg: &mut Privmsg) {
+        let guard = self.channels.read().await;
+        let Some((name, channel)) = guard.get_key_value(&privmsg.channel) else {
+            return;
         };
         };
-
-        if let Some((name, contact)) = self.contacts.read().await.get_key_value(&privmsg.channel) {
-            privmsg.channel = saltbox::encrypt(&contact.saltbox, &[0x00; MAX_NICK_LEN]);
-            privmsg.nick = saltbox::encrypt(&contact.self_saltbox, &[0x00; MAX_NICK_LEN]);
-            privmsg.msg = saltbox::encrypt(&contact.saltbox, privmsg.msg.as_bytes());
-            d!("Successfully encrypted message for {name}");
+        let Some(saltbox) = &channel.saltbox else {
+            return;
         };
         };
-    }
-
-    /// Try decrypting a given potentially encrypted `Privmsg` object.
-    pub async fn try_decrypt(&self, privmsg: &mut Privmsg, self_nickname: &str) {
-        let Ok(channel_ciphertext) = bs58::decode(&privmsg.channel).into_vec() else { return };
-        let Ok(nick_ciphertext) = bs58::decode(&privmsg.nick).into_vec() else { return };
-        let Ok(msg_ciphertext) = bs58::decode(&privmsg.msg).into_vec() else { return };
+        privmsg.channel = saltbox::encrypt(saltbox, &[0x00; MAX_NICK_LEN]);
+        privmsg.nick = saltbox::encrypt(saltbox, &pad(&privmsg.nick));
+        privmsg.msg = saltbox::encrypt(saltbox, privmsg.msg.as_bytes());
+        d!("Successfully encrypted message for {name}");
+    }
+
+    /// Encrypt a DM `Privmsg` in place for the contact named by `privmsg.channel`
+    /// (the bare key, with no leading "@"). Fails if the contact is unknown so
+    /// the caller can refuse to broadcast a message no one could decrypt.
+    pub async fn try_encrypt_dm(&self, privmsg: &mut Privmsg) -> Result<()> {
+        let guard = self.contacts.read().await;
+        let Some((name, contact)) = guard.get_key_value(&privmsg.channel) else {
+            return Err(Error::ContactNotFound);
+        };
+        privmsg.channel = saltbox::encrypt(&contact.saltbox, &[0x00; MAX_NICK_LEN]);
+        privmsg.nick = saltbox::encrypt(&contact.self_saltbox, &[0x00; MAX_NICK_LEN]);
+        privmsg.msg = saltbox::encrypt(&contact.saltbox, privmsg.msg.as_bytes());
+        d!("Successfully encrypted DM for {name}");
+        Ok(())
+    }
+
+    /// Try decrypting a `Privmsg` as a channel message in place. Returns true on
+    /// success. Plaintext messages for a known keyless channel are accepted
+    /// as-is; everything else returns false.
+    pub async fn try_decrypt_channel(&self, privmsg: &mut Privmsg) -> bool {
+        let Ok(channel_ciphertext) = bs58::decode(&privmsg.channel).into_vec() else {
+            // Not encrypted: accept only if it names a channel we hold.
+            return self.channels.read().await.contains_key(&privmsg.channel);
+        };
+        let Ok(nick_ciphertext) = bs58::decode(&privmsg.nick).into_vec() else { return false };
+        let Ok(msg_ciphertext) = bs58::decode(&privmsg.msg).into_vec() else { return false };
 
 
         for (name, channel) in self.channels.read().await.iter() {
         for (name, channel) in self.channels.read().await.iter() {
             let Some(saltbox) = &channel.saltbox else { continue };
             let Some(saltbox) = &channel.saltbox else { continue };
@@ -919,9 +948,20 @@ impl DarkIrc2 {
             privmsg.nick = String::from_utf8_lossy(&nick_dec).into();
             privmsg.nick = String::from_utf8_lossy(&nick_dec).into();
             privmsg.msg = String::from_utf8_lossy(&msg_dec).into();
             privmsg.msg = String::from_utf8_lossy(&msg_dec).into();
             d!("Successfully decrypted message for {name}");
             d!("Successfully decrypted message for {name}");
-            return
+            return true
         }
         }
 
 
+        false
+    }
+
+    /// Try decrypting a `Privmsg` as a DM in place. Returns true on success.
+    pub async fn try_decrypt_contact(&self, privmsg: &mut Privmsg, self_nickname: &str) -> bool {
+        let Ok(channel_ciphertext) = bs58::decode(&privmsg.channel).into_vec() else {
+            return false
+        };
+        let Ok(nick_ciphertext) = bs58::decode(&privmsg.nick).into_vec() else { return false };
+        let Ok(msg_ciphertext) = bs58::decode(&privmsg.msg).into_vec() else { return false };
+
         for (name, contact) in self.contacts.read().await.iter() {
         for (name, contact) in self.contacts.read().await.iter() {
             if saltbox::try_decrypt(&contact.saltbox, &channel_ciphertext).is_none() {
             if saltbox::try_decrypt(&contact.saltbox, &channel_ciphertext).is_none() {
                 continue
                 continue
@@ -938,11 +978,20 @@ impl DarkIrc2 {
                 continue
                 continue
             };
             };
 
 
-            privmsg.channel = name.to_string();
+            privmsg.channel = format!("@{}", name);
             privmsg.nick = nick;
             privmsg.nick = nick;
             privmsg.msg = String::from_utf8_lossy(&msg_dec).into();
             privmsg.msg = String::from_utf8_lossy(&msg_dec).into();
-            return
+            return true
         }
         }
+
+        false
+    }
+
+    /// Try decrypting a given potentially encrypted `Privmsg` object as a channel
+    /// and then as a DM. Returns true if either succeeded.
+    pub async fn try_decrypt(&self, privmsg: &mut Privmsg, self_nickname: &str) -> bool {
+        self.try_decrypt_channel(privmsg).await ||
+            self.try_decrypt_contact(privmsg, self_nickname).await
     }
     }
 }
 }