git-remote-iroh 0.1.0

Git remote protocol support for https://www.iroh.computer
Documentation
use anyhow::{Context, Result};
use gix_config::File;
use iroh::SecretKey;
use std::{
    fs::{DirBuilder, OpenOptions},
    io::Write,
    os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt},
    path::PathBuf,
};

/// Loads or generates a persistent iroh secret key.
pub fn load_or_create_key(git_dir: &str) -> Result<SecretKey> {
    let key_file = resolve_key_path(git_dir)?;
    if !key_file.exists() {
        create_key(key_file)
    } else {
        load_key(key_file)
    }
}

/// Resolve the configured iroh secret key.
/// Key file location in order of preference:
/// - $(git config get iroh.secretKey)
/// - dirs::config_local_dir()/git-remote-iroh/keys/default
/// - $HOME/.git-remote-iroh/keys/default
///
/// The last location is only used if the user config dir cannot be determined.
fn resolve_key_path(git_dir: &str) -> Result<PathBuf> {
    let git_config = File::from_git_dir(git_dir)?;
    if let Some(key_file) = git_config.path_by_key("iroh.secretKey") {
        Ok(key_file.value.to_string().into())
    } else {
        let key_dir = dirs::config_local_dir()
            .unwrap_or(PathBuf::from(
                std::env::var("HOME").context("HOME not set")?,
            ))
            .join("git-remote-iroh")
            .join("keys");
        DirBuilder::new()
            .recursive(true)
            .mode(0o700)
            .create(key_dir.as_path())?;
        Ok(key_dir.join("default"))
    }
}

/// Load the secret key at the given path.
///
/// The file must only be accessible by the current user.
pub fn load_key(key_path: PathBuf) -> Result<SecretKey> {
    if key_path.metadata()?.permissions().mode() & 0o077 != 0 {
        anyhow::bail!("unsafe permissions on {}", key_path.to_string_lossy());
    }
    let bytes = std::fs::read(&key_path).context("failed to read iroh key")?;
    let arr: [u8; 32] = bytes
        .try_into()
        .map_err(|_| anyhow::anyhow!("invalid key length"))?;
    Ok(SecretKey::from(arr))
}

/// Generate and store a new secret key at given path.
pub fn create_key(key_path: PathBuf) -> Result<SecretKey> {
    let key = SecretKey::generate();
    let mut key_file = OpenOptions::new()
        .mode(0o600)
        .create(true)
        .write(true)
        .open(&key_path)?;
    key_file
        .write(key.to_bytes().as_slice())
        .context("failed to write iroh key")?;
    key_file.flush()?;
    Ok(key)
}