Browse Source

Merge pull request #1259 from gsnoff/cjdnstool

Implement `cjdnstool util *` commands
Caleb James DeLisle 10 months ago
parent
commit
c7e4f078cf

+ 1 - 0
Cargo.lock

@@ -394,6 +394,7 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2",
+ "sodiumoxide",
  "tokio",
 ]
 

+ 1 - 0
rust/cjdnstool/Cargo.toml

@@ -17,4 +17,5 @@ lazy_static = "1"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 sha2 = "0.10"
+sodiumoxide = { git = "https://github.com/cjdelisle/sodiumoxide", rev = "f79b6656b84f02b049470f9ad556a6a0c9980bd7", version = "0.2.6" }
 tokio = { version = "1.27", features = ["fs", "net", "macros", "time", "rt-multi-thread", "sync"] }

+ 4 - 4
rust/cjdnstool/src/cexec.rs

@@ -1,6 +1,6 @@
-use crate::{
-    common::CommonArgs,
-    util::{self, PushField},
+use crate::common::{
+    args::CommonArgs,
+    utils::{self, PushField},
 };
 use anyhow::{bail, Result};
 use cjdns_admin::{ArgType, ArgValue, ArgValues, Func, ReturnValue};
@@ -37,7 +37,7 @@ pub async fn cexec(common: CommonArgs, rpc: Option<String>, rpc_args: Vec<String
             bail!("{} is not an RPC in cjdns", rpc);
         }
     } else {
-        let exe = util::exe_name();
+        let exe = utils::exe_name();
         println!("see: {FUNCTION_DOCS}");
         for func in cjdns.functions.iter() {
             let mut line = format!("{exe} cexec {}", func.name);

+ 0 - 0
rust/cjdnstool/src/common.rs → rust/cjdnstool/src/common/args.rs


+ 30 - 0
rust/cjdnstool/src/common/base32.rs

@@ -0,0 +1,30 @@
+use data_encoding::{BitOrder, DecodeError, Encoding, Specification, Translate, Wrap};
+use lazy_static::lazy_static;
+
+lazy_static! {
+    static ref BASE32: Encoding = Specification {
+        symbols: "0123456789bcdfghjklmnpqrstuvwxyz".to_owned(),
+        bit_order: BitOrder::LeastSignificantFirst,
+        check_trailing_bits: true,
+        padding: None,
+        ignore: String::new(),
+        wrap: Wrap {
+            width: 0,
+            separator: String::new()
+        },
+        translate: Translate {
+            from: "BCDFGHJKLMNPQRSTUVWXYZ".to_owned(),
+            to: "bcdfghjklmnpqrstuvwxyz".to_owned(),
+        },
+    }
+    .encoding()
+    .expect("invalid encoding specification");
+}
+
+pub fn decode(input: &[u8]) -> Result<Vec<u8>, DecodeError> {
+    BASE32.decode(input)
+}
+
+pub fn encode(input: &[u8]) -> String {
+    BASE32.encode(input)
+}

+ 4 - 0
rust/cjdnstool/src/common/mod.rs

@@ -0,0 +1,4 @@
+pub mod args;
+pub mod base32;
+pub mod utils;
+pub mod wire;

+ 49 - 55
rust/cjdnstool/src/util.rs → rust/cjdnstool/src/common/utils.rs

@@ -1,8 +1,10 @@
+use super::base32;
 use anyhow::{anyhow, bail, Result};
-use data_encoding::{BitOrder, Encoding, Specification, Translate, Wrap};
-use lazy_static::lazy_static;
-use sha2::{Digest, Sha512};
-use std::{env, iter, path::MAIN_SEPARATOR};
+use sha2::{
+    digest::{Digest, Output},
+    Sha512,
+};
+use std::{env, fmt::Write, path::MAIN_SEPARATOR};
 
 pub fn exe_name() -> String {
     env::args()
@@ -61,56 +63,24 @@ pub fn print_padded<const N: usize>(lines: Vec<[String; N]>) {
     }
 }
 
-pub fn key_to_ip6(with_key: &str) -> Result<String> {
-    lazy_static! {
-        static ref BASE32: Encoding = Specification {
-            symbols: "0123456789bcdfghjklmnpqrstuvwxyz".to_owned(),
-            bit_order: BitOrder::LeastSignificantFirst,
-            check_trailing_bits: true,
-            padding: None,
-            ignore: String::new(),
-            wrap: Wrap {
-                width: 0,
-                separator: String::new()
-            },
-            translate: Translate {
-                from: "BCDFGHJKLMNPQRSTUVWXYZ".to_owned(),
-                to: "bcdfghjklmnpqrstuvwxyz".to_owned(),
-            },
-        }
-        .encoding()
-        .expect("invalid encoding specification");
-    }
-
+pub fn key_to_ip6(with_key: &str, with_prefix: bool) -> Result<String> {
     if with_key.ends_with(".k") {
         let mut key = &with_key[..(with_key.len() - 2)];
-        let left = match key.rsplit_once('.') {
-            Some((l, r)) => {
-                key = r;
-                Some(l)
-            }
-            _ => None,
-        };
-        let bytes = BASE32
-            .decode(key.as_bytes())
-            .map_err(|e| anyhow!("invalid key format: {}", e))?;
-        let hash = format!("{:x}", Sha512::digest(Sha512::digest(bytes)));
-        let ipv6 = hash
-            .chars()
-            .take(32)
-            .enumerate()
-            .flat_map(|(i, c)| {
-                if i != 0 && i % 4 == 0 {
-                    Some(':')
-                } else {
-                    None
-                }
-                .into_iter()
-                .chain(iter::once(c))
-            })
-            .collect();
-        Ok(if let Some(left) = left {
-            format!("{left}.{ipv6}")
+        let prefix;
+        if with_prefix {
+            let (l, r) = key
+                .rsplit_once('.')
+                .ok_or_else(|| anyhow!("expected prefix before key"))?;
+            prefix = Some(l);
+            key = r;
+        } else {
+            prefix = None;
+        }
+        let raw_key =
+            base32::decode(key.as_bytes()).map_err(|e| anyhow!("invalid key format: {}", e))?;
+        let ipv6 = hash_to_ip6(Sha512::digest(Sha512::digest(raw_key)));
+        Ok(if let Some(prefix) = prefix {
+            format!("{prefix}.{ipv6}")
         } else {
             ipv6
         })
@@ -119,8 +89,27 @@ pub fn key_to_ip6(with_key: &str) -> Result<String> {
     }
 }
 
+pub fn hash_to_ip6(hash: Output<Sha512>) -> String {
+    const IP6_LEN: usize = 39;
+
+    let mut ip6 = String::with_capacity(IP6_LEN);
+    for chunk in hash.chunks(2).take(8) {
+        if !ip6.is_empty() {
+            ip6.push(':');
+        }
+        write!(ip6, "{:02x}{:02x}", chunk[0], chunk[1]).unwrap();
+    }
+    ip6
+}
+
 #[cfg(test)]
 mod test {
+    fn test_key_to_ip6_samples(samples: &[(&str, &str)], with_prefix: bool) {
+        for (&ref key, &ref ip6) in samples {
+            assert_eq!(super::key_to_ip6(key, with_prefix).unwrap(), ip6);
+        }
+    }
+
     #[test]
     fn test_key_to_ip6() {
         const SAMPLES: &[(&str, &str)] = &[
@@ -132,6 +121,13 @@ mod test {
                 "RJNDC8RVG194DDF2J5V679CFJCPMSMHV8P022Q3LVPYM21CQWYH0.k",
                 "fc50:47a8:2ef5:1c82:952e:10fc:dbad:dba9",
             ),
+        ];
+        test_key_to_ip6_samples(SAMPLES, false)
+    }
+
+    #[test]
+    fn test_key_to_ip6_with_prefix() {
+        const SAMPLES: &[(&str, &str)] = &[
             (
                 "v21.0000.0000.0000.001d.08bz912l989nzqc21q9x5qr96ns465nd71f290hb9q40z94jjw60.k",
                 "v21.0000.0000.0000.001d.fc8d:56ed:a8f3:237e:e586:2447:9966:9be1",
@@ -145,8 +141,6 @@ mod test {
                 "v20.0000.0000.0000.0019.fc02:2735:e595:bb70:8ffc:5293:8af8:c4b7",
             ),
         ];
-        for (&ref key, &ref ip6) in SAMPLES {
-            assert_eq!(super::key_to_ip6(key).unwrap(), ip6);
-        }
+        test_key_to_ip6_samples(SAMPLES, true)
     }
 }

+ 0 - 0
rust/cjdnstool/src/wire.rs → rust/cjdnstool/src/common/wire.rs


+ 9 - 3
rust/cjdnstool/src/main.rs

@@ -3,7 +3,6 @@ mod common;
 mod peers;
 mod session;
 mod util;
-mod wire;
 
 use clap::{Parser, Subcommand};
 use std::process::{ExitCode, Termination};
@@ -12,7 +11,7 @@ use std::process::{ExitCode, Termination};
 #[command(author, version, about, long_about = None)]
 struct Args {
     #[command(flatten)]
-    common: common::CommonArgs,
+    common: common::args::CommonArgs,
 
     #[command(subcommand)]
     command: Command,
@@ -42,6 +41,12 @@ enum Command {
         #[command(subcommand)]
         command: Option<session::Command>,
     },
+
+    /// Locally perform utility functions over data (public and private keys).
+    Util {
+        #[command(subcommand)]
+        command: util::Command,
+    },
 }
 
 #[tokio::main]
@@ -59,6 +64,7 @@ async fn main() -> MainResult {
             Session { command } => session::session(args.common, command.unwrap_or_default())
                 .await
                 .into(),
+            Util { command } => util::util(command).await.into(),
         },
         Err(err) => err.into(),
     }
@@ -100,7 +106,7 @@ impl Termination for MainResult {
                 ExitCode::from(err.exit_code() as u8)
             }
             Self::AnyhowError(err) => {
-                let exe = util::exe_name();
+                let exe = common::utils::exe_name();
                 eprintln!("{exe}: {err}");
                 ExitCode::FAILURE
             }

+ 1 - 1
rust/cjdnstool/src/peers/mod.rs

@@ -1,6 +1,6 @@
 mod show;
 
-use crate::common::CommonArgs;
+use crate::common::args::CommonArgs;
 use anyhow::Result;
 use clap::Subcommand;
 

+ 5 - 5
rust/cjdnstool/src/peers/show.rs

@@ -1,6 +1,6 @@
-use crate::{
-    common::CommonArgs,
-    util::{self, PushField},
+use crate::common::{
+    args::CommonArgs,
+    utils::{self, PushField},
     wire,
 };
 use anyhow::Result;
@@ -16,7 +16,7 @@ pub async fn show(common: CommonArgs, ip6: bool) -> Result<()> {
             .await?;
         for peer in resp.peers {
             let addr = if ip6 {
-                util::key_to_ip6(&peer.addr)?
+                utils::key_to_ip6(&peer.addr, true)?
             } else {
                 peer.addr
             };
@@ -50,7 +50,7 @@ pub async fn show(common: CommonArgs, ip6: bool) -> Result<()> {
         }
     }
 
-    util::print_padded(lines);
+    utils::print_padded(lines);
 
     Ok(())
 }

+ 1 - 1
rust/cjdnstool/src/session/mod.rs

@@ -1,6 +1,6 @@
 mod show;
 
-use crate::common::CommonArgs;
+use crate::common::args::CommonArgs;
 use anyhow::Result;
 use clap::Subcommand;
 

+ 5 - 5
rust/cjdnstool/src/session/show.rs

@@ -1,6 +1,6 @@
-use crate::{
-    common::CommonArgs,
-    util::{self, PushField},
+use crate::common::{
+    args::CommonArgs,
+    utils::{self, PushField},
     wire,
 };
 use anyhow::Result;
@@ -39,7 +39,7 @@ pub async fn show(common: CommonArgs, ip6: bool) -> Result<()> {
     let mut lines = Vec::with_capacity(sessions.len());
     for session in sessions {
         let addr = if ip6 {
-            util::key_to_ip6(&session.addr)?
+            utils::key_to_ip6(&session.addr, true)?
         } else {
             session.addr
         };
@@ -62,7 +62,7 @@ pub async fn show(common: CommonArgs, ip6: bool) -> Result<()> {
             last,
         ]);
     }
-    util::print_padded(lines);
+    utils::print_padded(lines);
     Ok(())
 }
 

+ 31 - 0
rust/cjdnstool/src/util/key2ip6.rs

@@ -0,0 +1,31 @@
+use crate::common::utils;
+use anyhow::{anyhow, bail, Result};
+use std::fmt::Write;
+
+pub async fn key2ip6(pubkeys: Vec<String>) -> Result<()> {
+    const KEY_LEN: usize = 54;
+
+    let mut output = String::new();
+    for (index, pubkey) in pubkeys.into_iter().enumerate() {
+        use std::cmp::Ordering::*;
+        let adj = match pubkey.chars().count().cmp(&KEY_LEN) {
+            Less => Some("short"),
+            Equal => None,
+            Greater => Some("long"),
+        };
+        if let Some(adj) = adj {
+            bail!(
+                "argument {} [{}] is too {} to be a valid public key",
+                index,
+                pubkey,
+                adj
+            );
+        }
+        let ip6 = utils::key_to_ip6(&pubkey, false)
+            .map_err(|e| anyhow!("argument {} [{}]: {}", index, pubkey, e))?;
+        writeln!(output, "{pubkey} {ip6}")?;
+    }
+
+    print!("{output}");
+    Ok(())
+}

+ 26 - 0
rust/cjdnstool/src/util/keygen.rs

@@ -0,0 +1,26 @@
+use crate::common::{base32, utils};
+use anyhow::{bail, Result};
+use sha2::{Digest, Sha512};
+use sodiumoxide::{crypto::box_, hex};
+
+pub async fn keygen() -> Result<()> {
+    if sodiumoxide::init().is_err() {
+        bail!("failed to initialize sodium library")
+    }
+
+    let (pubkey, privkey, hash) = loop {
+        let pair = box_::gen_keypair();
+        let hash = Sha512::digest(Sha512::digest(&pair.0));
+        if hash[0] == 0xfc {
+            break (pair.0, pair.1, hash);
+        }
+    };
+
+    println!(
+        "{} {}.k {}",
+        hex::encode(privkey.0),
+        base32::encode(&pubkey.0),
+        utils::hash_to_ip6(hash)
+    );
+    Ok(())
+}

+ 36 - 0
rust/cjdnstool/src/util/mod.rs

@@ -0,0 +1,36 @@
+mod key2ip6;
+mod keygen;
+mod priv2pub;
+
+use anyhow::Result;
+use clap::Subcommand;
+
+pub async fn util(command: Command) -> Result<()> {
+    use Command::*;
+    match command {
+        Key2Ip6 { pubkeys } => key2ip6::key2ip6(pubkeys).await,
+        Priv2Pub { privkeys } => priv2pub::priv2pub(privkeys).await,
+        Keygen => keygen::keygen().await,
+    }
+}
+
+#[derive(Subcommand)]
+#[command(rename_all = "lower")]
+pub enum Command {
+    /// Calculate IPv6 address(es) to be assigned to peer(s) depending on their public key(s).
+    Key2Ip6 {
+        /// One or more public keys to calculate IPv6 addresses from.
+        #[arg(id = "PUBKEY", required = true)]
+        pubkeys: Vec<String>,
+    },
+
+    /// Calculate the public key(s) which correspond to the given private key(s).
+    Priv2Pub {
+        /// One or more private keys to calculate their corresponding public keys.
+        #[arg(id = "PRIVKEY", required = true)]
+        privkeys: Vec<String>,
+    },
+
+    /// Generate a private-public key pair, and display it along with its IPv6 address.
+    Keygen,
+}

+ 41 - 0
rust/cjdnstool/src/util/priv2pub.rs

@@ -0,0 +1,41 @@
+use crate::common::base32;
+use anyhow::{bail, Result};
+use sodiumoxide::{crypto::box_::SecretKey, hex};
+use std::fmt::Write;
+
+pub async fn priv2pub(privkeys: Vec<String>) -> Result<()> {
+    const KEY_LEN: usize = 64;
+
+    let mut output = String::new();
+    for (index, privkey) in privkeys.into_iter().enumerate() {
+        use std::cmp::Ordering::*;
+        let adj = match privkey.chars().count().cmp(&KEY_LEN) {
+            Less => Some("short"),
+            Equal => None,
+            Greater => Some("long"),
+        };
+        if let Some(adj) = adj {
+            bail!(
+                "argument {} [{}] is too {} to be a valid private key",
+                index,
+                privkey,
+                adj
+            );
+        }
+        if let Ok(raw) = hex::decode(&privkey) {
+            let pubkey = SecretKey::from_slice(&raw)
+                .unwrap() // already checked the length
+                .public_key();
+            writeln!(output, "{}.k", base32::encode(&pubkey.0))?;
+        } else {
+            bail!(
+                "argument {} [{}] is not a valid hexadecimal string",
+                index,
+                privkey
+            );
+        }
+    }
+
+    print!("{output}");
+    Ok(())
+}