runnel-rs 0.2.0

A Rust proxy and tunnel toolbox with WireGuard-style, TUN, SOCKS, and TLS-based transports.
Documentation
use anyhow::Result;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use boringtun::x25519::{PublicKey, StaticSecret};
use clap::Args;
use rand::rngs::OsRng;
use serde::Serialize;

use super::parse_key;

#[derive(Clone, Debug, Args)]
pub struct WgKeygenArgs {
    #[arg(long)]
    pub json: bool,
}

#[derive(Debug, Serialize, PartialEq, Eq)]
pub(crate) struct WgKeyMaterial {
    pub(crate) private_key: String,
    pub(crate) public_key: String,
}

pub fn run_keygen(args: WgKeygenArgs) -> Result<()> {
    let material = generate_key_material();
    print_key_material(&material, args.json)?;
    Ok(())
}

pub(crate) fn generate_key_material() -> WgKeyMaterial {
    let private_key = StaticSecret::random_from_rng(OsRng).to_bytes();
    let public_key = *PublicKey::from(&StaticSecret::from(private_key)).as_bytes();
    WgKeyMaterial {
        private_key: STANDARD.encode(private_key),
        public_key: STANDARD.encode(public_key),
    }
}

pub(crate) fn public_key_from_private_key(encoded_private_key: &str) -> Result<String> {
    let private_key = parse_key("wg private_key", encoded_private_key)?;
    let public_key = *PublicKey::from(&StaticSecret::from(private_key)).as_bytes();
    Ok(STANDARD.encode(public_key))
}

fn print_key_material(material: &WgKeyMaterial, as_json: bool) -> Result<()> {
    if as_json {
        println!("{}", serde_json::to_string_pretty(material)?);
    } else {
        println!("private_key={}", material.private_key);
        println!("public_key={}", material.public_key);
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{generate_key_material, public_key_from_private_key};
    use base64::{Engine as _, engine::general_purpose::STANDARD};
    use boringtun::x25519::{PublicKey, StaticSecret};

    #[test]
    fn keygen_outputs_parseable_base64_keypair() {
        let material = generate_key_material();
        let private_key = STANDARD
            .decode(&material.private_key)
            .expect("private key should decode");
        let public_key = STANDARD
            .decode(&material.public_key)
            .expect("public key should decode");

        assert_eq!(private_key.len(), 32);
        assert_eq!(public_key.len(), 32);
        assert_eq!(
            material.public_key,
            public_key_from_private_key(&material.private_key).unwrap()
        );
    }

    #[test]
    fn pubkey_derives_expected_public_key() {
        let private_key = STANDARD.encode([7u8; 32]);
        let expected = STANDARD.encode(PublicKey::from(&StaticSecret::from([7u8; 32])).as_bytes());
        assert_eq!(public_key_from_private_key(&private_key).unwrap(), expected);
    }

    #[test]
    fn pubkey_rejects_invalid_private_key() {
        let err = public_key_from_private_key("not-base64")
            .unwrap_err()
            .to_string();
        assert!(err.contains("failed to decode"), "{err}");
    }
}