git-remote-iroh 0.2.1

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,
};

use crate::keys_dir;

/// Loads or generates a persistent iroh secret key.
pub fn load_or_create_key(config_dir: &PathBuf, git_dir: &PathBuf) -> Result<(PathBuf, SecretKey)> {
    let key_file = resolve_key_path(config_dir, git_dir)?;
    let key = if !key_file.exists() {
        create_key(&key_file).context(format!("create key: {:?}", key_file))
    } else {
        load_key(&key_file).context(format!("load key: {:?}", key_file))
    }?;
    Ok((key_file, key))
}

/// 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 ony used if the user config dir cannot be determined.
fn resolve_key_path(config_dir: &PathBuf, git_dir: &PathBuf) -> Result<PathBuf> {
    let git_config = if git_dir.exists() {
        File::from_git_dir(git_dir)?
    } else {
        File::from_globals()?
    };
    let key_file = match git_config.path_by_key("iroh.secretKey") {
        Some(key_file) => key_file.value.to_string().into(),
        None => keys_dir(&config_dir).join("default"),
    };

    let key_dir = key_file.parent().ok_or(anyhow::anyhow!(
        "failed to resolve parent directory: {:?}",
        key_file
    ))?;
    DirBuilder::new()
        .recursive(true)
        .mode(0o700)
        .create(key_dir)?;
    Ok(key_file)
}

/// 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)
}