voynich 0.1.1

Library for creating anonymous, end-to-end encrypted and authenticated chat applications
Documentation
use crate::onion_service::OnionService;
use anyhow::{anyhow, Result};
use clap::crate_name;
use lazy_static::lazy_static;
use std::env;
use std::fs::{create_dir, read, read_to_string, set_permissions, write, Permissions};
use std::net::SocketAddr;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tokio_socks::tcp::Socks5Stream;
use tor_client_lib::{
    control_connection::{
        OnionAddress, OnionService as TorClientOnionService, OnionServiceListener,
        OnionServiceMapping, TorSocketAddr,
    },
    TorEd25519SigningKey,
};

lazy_static! {
    pub static ref HOME: String = match env::var("HOME") {
        Ok(value) => value,
        Err(error) => {
            panic!("Error finding home directory: {}", error);
        }
    };
    pub static ref CONFIG_HOME: String = match env::var("XDG_CONFIG_HOME") {
        Ok(value) => value,
        Err(env::VarError::NotPresent) => format!("{}/.config", *HOME),
        Err(error) => {
            panic!("Error getting value of XDG_DATA_HOME: {}", error);
        }
    };
    pub static ref DATA_DIR: String = format!("{}/.{}", *HOME, crate_name!());
}

fn create_secure_dir(path_string: &str) -> Result<()> {
    let path = Path::new(path_string);
    if path.exists() {
        if !path.is_dir() {
            return Err(anyhow!("{} is not a directory", *DATA_DIR));
        }
    } else {
        create_dir(path_string)?;
        set_permissions(path_string, Permissions::from_mode(0o700))?;
    }
    Ok(())
}

fn check_directory(dir: &str) -> Result<()> {
    let dir_path = Path::new(dir);
    if !dir_path.exists() {
        return Err(anyhow!("Directory {} doesn't exist", dir));
    }
    if !dir_path.is_dir() {
        return Err(anyhow!("{} is not a directory", dir));
    }
    let metadata = dir_path.metadata()?;
    let mode = metadata.permissions().mode();
    if mode & 0o777 != 0o700 {
        return Err(anyhow!("Permissions on {} are too permissive!", *DATA_DIR));
    }

    Ok(())
}

fn check_file(filename: &str) -> Result<PathBuf> {
    let path = Path::new(filename);
    if !path.exists() {
        return Err(anyhow!("{} doesn't exist", filename));
    }
    let metadata = path.metadata()?;
    let mode = metadata.permissions().mode();
    if mode & 0o777 != 0o600 {
        return Err(anyhow!("Permissions on {} are too permissive!", filename));
    }

    Ok(path.to_path_buf())
}

fn read_secret_key_file(dir: &str) -> Result<TorEd25519SigningKey> {
    let filename = format!("{}/ed25519_secret_key", dir);
    let path = check_file(&filename)?;
    match read(path) {
        Ok(data) => {
            let data: [u8; 64] = match data.try_into() {
                Ok(data) => data,
                Err(_) => {
                    return Err(anyhow!(
                        "Error reading {}: Data not an Ed25519 key",
                        filename
                    ));
                }
            };
            Ok(TorEd25519SigningKey::from_bytes(data))
        }
        Err(error) => Err(anyhow!("{}", error)),
    }
}

fn write_secret_key_file(dir: &str, key: &TorEd25519SigningKey) -> Result<()> {
    let filename = format!("{}/ed25519_secret_key", dir);
    let path = Path::new(&filename);
    write(path, key.to_bytes())?;
    set_permissions(path, Permissions::from_mode(0o600))?;
    Ok(())
}

fn read_onion_address_file(dir: &str) -> Result<OnionAddress> {
    let filename = format!("{}/onion_address", dir);
    let path = check_file(&filename)?;
    match read_to_string(path) {
        Ok(data) => Ok(OnionAddress::from_str(&data)?),
        Err(error) => Err(anyhow!("{}", error)),
    }
}

fn write_onion_address_file(dir: &str, address: &OnionAddress) -> Result<()> {
    let filename = format!("{}/onion_address", dir);
    let path = Path::new(&filename);
    write(path, address.to_string())?;
    set_permissions(path, Permissions::from_mode(0o600))?;
    Ok(())
}

pub fn get_onion_address(name: &str) -> Result<OnionAddress> {
    let onion_service_dir = get_onion_service_dir(name)?;
    read_onion_address_file(&onion_service_dir)
}

pub fn save_onion_address(name: &str, address: &OnionAddress) -> Result<()> {
    let dir_name = create_onion_service_dir(name)?;
    write_onion_address_file(&dir_name, address)
}

pub fn get_onion_service_key(name: &str) -> Result<TorEd25519SigningKey> {
    let onion_service_dir = get_onion_service_dir(name)?;
    read_secret_key_file(&onion_service_dir)
}

pub fn save_onion_service_key(name: &str, key: &TorEd25519SigningKey) -> Result<()> {
    let dir_name = create_onion_service_dir(name)?;
    write_secret_key_file(&dir_name, key)
}

fn get_onion_service_dir(name: &str) -> Result<String> {
    check_directory(&DATA_DIR)?;
    let onion_service_dir = format!("{}/{}", *DATA_DIR, name);
    check_directory(&onion_service_dir)?;
    Ok(onion_service_dir)
}

fn create_onion_service_dir(name: &str) -> Result<String> {
    create_secure_dir(&DATA_DIR)?;
    let path_string = format!("{}/{}", *DATA_DIR, name);
    create_secure_dir(&path_string)?;
    Ok(path_string)
}

pub fn get_onion_service(
    name: &str,
    onion_address: &OnionAddress,
    listen_address: &TorSocketAddr,
) -> Result<OnionService, anyhow::Error> {
    let onion_service_key = get_onion_service_key(name)?;
    let onion_service_mapping =
        OnionServiceMapping::new(onion_address.service_port(), Some(listen_address.clone()));
    Ok(OnionService::new(
        name,
        TorClientOnionService::new(
            onion_address.service_id().clone(),
            onion_service_key,
            &[onion_service_mapping],
        ),
    ))
}

pub fn save_onion_service(onion_service: &OnionService, service_port: u16) -> Result<()> {
    save_onion_address(
        onion_service.name(),
        &onion_service.onion_address(service_port)?,
    )?;
    save_onion_service_key(onion_service.name(), onion_service.signing_key())
}

pub async fn test_onion_service_connection(
    listener: OnionServiceListener,
    tor_proxy_address: &SocketAddr,
    onion_address: &OnionAddress,
) -> Result<OnionServiceListener, anyhow::Error> {
    println!(
        "Testing onion service connection to {}. Please be patient, this may take a few moments...",
        onion_address
    );
    let handle = tokio::spawn(async move {
        match listener.accept().await {
            Ok(_) => Ok(listener),
            Err(error) => Err(error),
        }
    });
    Socks5Stream::connect(tor_proxy_address, onion_address.to_string()).await?;

    Ok(handle.await??)
}