~core/sage

78315f80a4134c20eba8bd551c8f0191053ed90f — core 21 days ago f11e865
chore: please thy lord clippy
M src/cmds/createid.rs => src/cmds/createid.rs +21 -10
@@ 1,9 1,9 @@
use std::path::Path;
use std::process::exit;
use age::secrecy::ExposeSecret;
use regex::Regex;
use crate::db::Database;
use crate::identity::{IdKeyData, Identity};
use age::secrecy::ExposeSecret;
use regex::Regex;
use std::path::Path;
use std::process::exit;

pub fn create_id(db_path: &Path, name: String, email: String) -> anyhow::Result<()> {
    let mut db = Database::load_or_create(db_path)?;


@@ 11,7 11,11 @@ pub fn create_id(db_path: &Path, name: String, email: String) -> anyhow::Result<
    // sanity check to ensure this id does not already exist
    for key in &db.keys {
        if key.name == name && key.email == email {
            eprintln!("key '{:?}' (keyid {}) already exists  in database", key, key.keys.keyid()?);
            eprintln!(
                "key '{:?}' (keyid {}) already exists  in database",
                key,
                key.keys.keyid()?
            );
            eprintln!("if you want to recreate it, remove it first");
            exit(1);
        }


@@ 19,11 23,14 @@ pub fn create_id(db_path: &Path, name: String, email: String) -> anyhow::Result<

    // Email is a fucking nightmare
    // RFC5322, I hate you
    let email_re = Regex::new(r#"(?x)
    let email_re = Regex::new(
        r#"(?x)
            ^(?P<login>[^@\s]+)@
            ([[:word:]]+\.)*
            [[:word:]]+$
            "#).unwrap();
            "#,
    )
    .unwrap();

    if !email_re.is_match(&email) {
        eprintln!("`{email}` is not a valid RFC5322 email address");


@@ 37,14 44,18 @@ pub fn create_id(db_path: &Path, name: String, email: String) -> anyhow::Result<
    let identity = Identity {
        name,
        email,
        keys: IdKeyData::Local(ss.to_string())
        keys: IdKeyData::Local(ss.to_string()),
    };

    db.keys.push(identity.clone());
    // save database
    db.write(db_path)?;

    println!("wrote new key {:?} (keyid {}) to database", identity, identity.keys.keyid()?);
    println!(
        "wrote new key {:?} (keyid {}) to database",
        identity,
        identity.keys.keyid()?
    );

    Ok(())
}
\ No newline at end of file
}

M src/cmds/import.rs => src/cmds/import.rs +18 -18
@@ 1,8 1,7 @@
use std::convert::identity;
use crate::db::Database;
use crate::identity::{IdKeyData, Identity, unescape};
use std::path::Path;
use std::process::exit;
use crate::db::Database;
use crate::identity::{unescape, IdKeyData, Identity};

pub fn import_key(db_path: &Path, key: String, insert_anyway: bool) -> anyhow::Result<()> {
    // decode the key


@@ 13,35 12,32 @@ pub fn import_key(db_path: &Path, key: String, insert_anyway: bool) -> anyhow::R
        exit(1);
    }

    let [_sentinel, key, name, email] = components.as_slice() else { unreachable!() };
    let [_sentinel, key, name, email] = components.as_slice() else {
        unreachable!()
    };

    let name = unescape(name.to_string())?;
    let email = unescape(email.to_string())?;

    let keys = match &key[0..3] {
        "age" => {
            IdKeyData::Peer(key.to_string())
        },
        "AGE" => {
            IdKeyData::Local(key.to_string())
        },
        "age" => IdKeyData::Peer(key.to_string()),
        "AGE" => IdKeyData::Local(key.to_string()),
        _ => {
            eprintln!("unsupported age key type; is this a sage key?");
            exit(1);
        }
    };

    let identity = Identity {
        name,
        email,
        keys
    };
    let identity = Identity { name, email, keys };

    let mut db = Database::load_or_create(db_path)?;

    for key in &db.keys {
        if key.keys.keyid()? == identity.keys.keyid()? {
            println!("key {} already exists in database, skipping", key.keys.keyid()?);
            println!(
                "key {} already exists in database, skipping",
                key.keys.keyid()?
            );
            return Ok(());
        }



@@ 56,7 52,11 @@ pub fn import_key(db_path: &Path, key: String, insert_anyway: bool) -> anyhow::R

    db.write(db_path)?;

    println!("imported {:?} (keyid {}) successfully", identity, identity.keys.keyid()?);
    println!(
        "imported {:?} (keyid {}) successfully",
        identity,
        identity.keys.keyid()?
    );

    Ok(())
}
\ No newline at end of file
}

M src/cmds/ls.rs => src/cmds/ls.rs +21 -8
@@ 1,10 1,14 @@
use crate::db::Database;
use crate::identity::{IdKeyData, escape};
use std::path::Path;
use std::process::exit;
use urlencoding::encode;
use crate::db::Database;
use crate::identity::{escape, IdKeyData};

pub fn list_keys(db_path: &Path, local_only: bool, peer_only: bool, full_pks: bool) -> anyhow::Result<()> {
pub fn list_keys(
    db_path: &Path,
    local_only: bool,
    peer_only: bool,
    full_pks: bool,
) -> anyhow::Result<()> {
    if local_only && peer_only {
        eprintln!("cannot show only local and only peer keys, pick one");
        exit(1);


@@ 19,19 23,28 @@ pub fn list_keys(db_path: &Path, local_only: bool, peer_only: bool, full_pks: bo
    for key in &db.keys {
        match key.keys {
            IdKeyData::Local(_) => {
                if !show_local { continue };
                if !show_local {
                    continue;
                };
                displayed += 1;
                println!("local: {:?}", key);
            }
            IdKeyData::Peer(_) => {
                if !show_peer { continue };
                if !show_peer {
                    continue;
                };
                displayed += 1;
                println!("peer: {:?}", key);
            }
        }

        if full_pks {
            println!("   pk: sage/{}/{}/{}", key.keys.pk()?, escape(key.name.clone()), escape(key.email.clone()));
            println!(
                "   pk: sage/{}/{}/{}",
                key.keys.pk()?,
                escape(key.name.clone()),
                escape(key.email.clone())
            );
        } else {
            println!("   id: {}", key.keys.keyid()?);
        }


@@ 42,4 55,4 @@ pub fn list_keys(db_path: &Path, local_only: bool, peer_only: bool, full_pks: bo
    }

    Ok(())
}
\ No newline at end of file
}

M src/cmds/mod.rs => src/cmds/mod.rs +1 -1
@@ 1,4 1,4 @@
pub mod createid;
pub mod import;
pub mod ls;
pub mod show;
pub mod import;
\ No newline at end of file

M src/cmds/show.rs => src/cmds/show.rs +18 -6
@@ 1,7 1,7 @@
use crate::db::Database;
use crate::identity::{IdKeyData, escape};
use std::path::Path;
use std::process::exit;
use crate::db::Database;
use crate::identity::{escape, IdKeyData};

pub fn show_key(db_path: &Path, search: String, expose_secret: bool) -> anyhow::Result<()> {
    let db = Database::load_or_create(db_path)?;


@@ 21,12 21,24 @@ pub fn show_key(db_path: &Path, search: String, expose_secret: bool) -> anyhow::
        let show_sk = expose_secret && matches!(each_match.keys, IdKeyData::Local(..));

        if !show_sk {
            println!("   pk: sage/{}/{}/{}", each_match.keys.pk()?, escape(each_match.name.clone()), escape(each_match.email.clone()));
            println!(
                "   pk: sage/{}/{}/{}",
                each_match.keys.pk()?,
                escape(each_match.name.clone()),
                escape(each_match.email.clone())
            );
        } else {
            let IdKeyData::Local(k) = &each_match.keys else { continue };
            println!("   sk: sage/{}/{}/{}", k, escape(each_match.name.clone()), escape(each_match.email.clone()));
            let IdKeyData::Local(k) = &each_match.keys else {
                continue;
            };
            println!(
                "   sk: sage/{}/{}/{}",
                k,
                escape(each_match.name.clone()),
                escape(each_match.email.clone())
            );
        }
    }

    Ok(())
}
\ No newline at end of file
}

M src/db.rs => src/db.rs +35 -17
@@ 1,19 1,17 @@
use std::path::{Path, PathBuf};
use crate::identity::Identity;
use dirs::config_dir;
use nucleo::{Config, Matcher, Utf32Str};
use serde::{Deserialize, Serialize};
use crate::identity::Identity;
use std::path::{Path, PathBuf};

#[derive(Serialize, Deserialize)]
pub struct Database {
    pub keys: Vec<Identity>
    pub keys: Vec<Identity>,
}
impl Database {
    pub fn load_or_create(path: &Path) -> anyhow::Result<Self> {
        if !database_exists(path) {
            let d = Database {
                keys: vec![]
            };
            let d = Database { keys: vec![] };
            write_database(d, path)?;
        }
        load_database(path)


@@ 22,19 20,39 @@ impl Database {
        write_database(self, path)
    }
    pub fn fuzzy_search(&self, term: String) -> Vec<Identity> {
        if self.keys.is_empty() { return vec![] }
        if self.keys.is_empty() {
            return vec![];
        }
        let mut m = Matcher::new(Config::DEFAULT);

        let haystacks = self.keys.iter().map(|u| {
            (u.clone(), format!("{:?} {}", u, u.keys.pk().unwrap_or("<pk_unknown>".to_string())))
        }).collect::<Vec<_>>();
        let haystacks = self
            .keys
            .iter()
            .map(|u| {
                (
                    u.clone(),
                    format!(
                        "{:?} {}",
                        u,
                        u.keys.pk().unwrap_or("<pk_unknown>".to_string())
                    ),
                )
            })
            .collect::<Vec<_>>();

        let mut scores = haystacks.iter().map(|h| {
            (
                m.fuzzy_match(Utf32Str::Ascii(h.1.as_bytes()), Utf32Str::Ascii(term.as_bytes())).unwrap_or(0),
                h.0.clone()
            )
        }).collect::<Vec<_>>();
        let mut scores = haystacks
            .iter()
            .map(|h| {
                (
                    m.fuzzy_match(
                        Utf32Str::Ascii(h.1.as_bytes()),
                        Utf32Str::Ascii(term.as_bytes()),
                    )
                    .unwrap_or(0),
                    h.0.clone(),
                )
            })
            .collect::<Vec<_>>();

        scores.sort_by_key(|u| u.0);
        scores.reverse();


@@ 71,4 89,4 @@ fn write_database(d: Database, path: &Path) -> anyhow::Result<()> {

pub fn db_default() -> PathBuf {
    config_dir().unwrap().join("sage.toml")
}
\ No newline at end of file
}

M src/identity.rs => src/identity.rs +7 -9
@@ 1,18 1,18 @@
use std::fmt::{Debug, Formatter};
use std::str::FromStr;
use anyhow::bail;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Formatter};
use std::str::FromStr;

#[derive(Serialize, Deserialize, Clone)]
pub struct Identity {
    pub name: String,
    pub email: String,
    pub keys: IdKeyData
    pub keys: IdKeyData,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum IdKeyData {
    Local(String),
    Peer(String)
    Peer(String),
}

impl Debug for Identity {


@@ 31,16 31,14 @@ impl IdKeyData {
                    }
                };
                Ok(k.to_public().to_string())
            },
            IdKeyData::Peer(pk) => {
                Ok(pk.clone())
            }
            IdKeyData::Peer(pk) => Ok(pk.clone()),
        }
    }
    pub fn keyid(&self) -> anyhow::Result<String> {
        let pk = self.pk()?;
        let s0 = &pk[4..12];
        Ok(format!("{s0}"))
        Ok(s0.to_string())
    }
}



@@ 49,4 47,4 @@ pub fn escape(i: String) -> String {
}
pub fn unescape(i: String) -> anyhow::Result<String> {
    Ok(urlencoding::decode(&i)?.to_string())
}
\ No newline at end of file
}

M src/main.rs => src/main.rs +13 -21
@@ 1,13 1,11 @@
use std::path::PathBuf;
use std::process::exit;
use dirs::config_dir;
use crate::cmds::createid::create_id;
use crate::db::db_default;
use crate::wrapper::wrapper;
use std::process::exit;

mod identity;
mod db;
mod cmds;
mod db;
mod identity;
mod wrapper;

fn main() -> anyhow::Result<()> {


@@ 18,7 16,9 @@ fn main() -> anyhow::Result<()> {
        exit(0);
    }

    let database = pargs.opt_value_from_str(["-D", "--database"])?.unwrap_or(db_default());
    let database = pargs
        .opt_value_from_str(["-D", "--database"])?
        .unwrap_or(db_default());

    let Some(subcommand) = pargs.subcommand()? else {
        // run the wrapper


@@ 37,7 37,7 @@ fn main() -> anyhow::Result<()> {
            };

            create_id(&database, name, email)?;
        },
        }
        "ls" => {
            cmds::ls::list_keys(
                &database,


@@ 45,29 45,21 @@ fn main() -> anyhow::Result<()> {
                pargs.contains(["-p", "--peer"]),
                pargs.contains(["-f", "--full"]),
            )?;
        },
        }
        "show" => {
            let Ok(search) = pargs.free_from_str() else {
                eprintln!("search term is required, --help for help");
                exit(1);
            };
            cmds::show::show_key(
                &database,
                search,
                pargs.contains(["-e", "--expose-secret"])
            )?;
        },
            cmds::show::show_key(&database, search, pargs.contains(["-e", "--expose-secret"]))?;
        }
        "import" => {
            let Ok(key) = pargs.free_from_str() else {
                eprintln!("key is required, --help for help");
                exit(1);
            };
            cmds::import::import_key(
                &database,
                key,
                pargs.contains(["-i", "--import-anyway"])
            )?;
        },
            cmds::import::import_key(&database, key, pargs.contains(["-i", "--import-anyway"]))?;
        }
        unknown => {
            eprintln!("Unknown subcommand: {}", unknown);
            print_help();


@@ 158,4 150,4 @@ import [OPTIONS] <key>

    OPTIONS:
    -i, --insert-anyway     Insert even if a matching identity is already present
";
\ No newline at end of file
";

M src/wrapper.rs => src/wrapper.rs +38 -25
@@ 1,19 1,17 @@
use crate::db::Database;
use crate::identity::IdKeyData;
use anyhow::bail;
use pico_args::Arguments;
use std::os::unix::prelude::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{exit, Command, Stdio};
use std::process::{Command, exit};
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
use anyhow::bail;
use pico_args::Arguments;
use tempfile::NamedTempFile;
use crate::db::Database;
use crate::identity::IdKeyData;

#[derive(Debug)]
enum Mode {
    Encrypt,
    Decrypt
    Decrypt,
}

pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {


@@ 25,7 23,11 @@ pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
        bail!("cannot specify both --encrypt and --decrypt, pick one");
    }

    let mode = if contains_decrypt { Mode::Decrypt } else { Mode::Encrypt };
    let mode = if contains_decrypt {
        Mode::Decrypt
    } else {
        Mode::Encrypt
    };
    let output: Option<PathBuf> = pargs.opt_value_from_str(["-o", "--output"])?;
    let armor = pargs.contains(["-a", "--armor"]);
    let passphrase = pargs.contains(["-p", "--passphrase"]);


@@ 37,11 39,12 @@ pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
        if let Some(r) = maybe_r {
            recipients.push(r);
        } else {
            break
            break;
        }
    }

    if let Ok(Some(f)) = pargs.opt_value_from_str::<[&str; 2], PathBuf>(["-R", "--recipients-file"]) {
    if let Ok(Some(f)) = pargs.opt_value_from_str::<[&str; 2], PathBuf>(["-R", "--recipients-file"])
    {
        recipients.extend(load_keyfile(&f)?);
    }



@@ 50,32 53,37 @@ pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
        let maybe_i: Option<String> = pargs.opt_value_from_str(["-i", "--identity"])?;

        if let Some(i) = maybe_i {
            if let Ok(path) = PathBuf::from_str(&i) && path.exists() {
            let Ok(path) = PathBuf::from_str(&i);
            if path.exists() {
                identities.extend(load_keyfile(&path)?);
            } else {
                identities.push(i);
            }
        } else {
            break
            break;
        }
    }

    let input: Option<PathBuf> = pargs.opt_free_from_str()?;

    if verbose {
        println!("resolved args: {:?} output={:?} armor?{:?} passphrase?{:?} recipients=>{:?} identities=>{:?} input={:?}", mode, output, armor, passphrase, recipients, identities, input);
        println!(
            "resolved args: {:?} output={:?} armor?{:?} passphrase?{:?} recipients=>{:?} identities=>{:?} input={:?}",
            mode, output, armor, passphrase, recipients, identities, input
        );
    }

    // Resolve recipients and identities to the database
    let db = Database::load_or_create(db_path)?;

    // resolve recipients
    let resolved_recipients = recipients.iter()
    let resolved_recipients = recipients
        .iter()
        .map(|u| {
            // is this already a valid key?
            if u.starts_with("age1") {
                // regular ol' pubkey
                return u.clone()
                return u.clone();
            }
            // otherwise, resolve it in the database
            let results = db.fuzzy_search(u.clone());


@@ 90,7 98,11 @@ pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
            let r = &results[0];

            if verbose {
                eprintln!("resolved rcpt {u} => {:?} (keyid {})", r, r.keys.keyid().unwrap());
                eprintln!(
                    "resolved rcpt {u} => {:?} (keyid {})",
                    r,
                    r.keys.keyid().unwrap()
                );
            }

            r.keys.pk().unwrap()


@@ 142,10 154,13 @@ pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {

    // execute age
    let mut args = vec![];
    args.push(match mode {
        Mode::Encrypt => "--encrypt",
        Mode::Decrypt => "--decrypt"
    }.to_string());
    args.push(
        match mode {
            Mode::Encrypt => "--encrypt",
            Mode::Decrypt => "--decrypt",
        }
        .to_string(),
    );
    if let Some(o) = output {
        args.push("--output".to_string());
        args.push(o.display().to_string());


@@ 172,9 187,7 @@ pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
        eprintln!("exec age {:?}", args);
    }

    Err(Command::new("age")
        .args(args)
        .exec())?;
    Err(Command::new("age").args(args).exec())?;
    Ok(())
}



@@ 186,4 199,4 @@ fn load_keyfile(path: &Path) -> anyhow::Result<Vec<String>> {
        .filter(|u| !u.starts_with("#"))
        .map(|u| u.to_string())
        .collect())
}
\ No newline at end of file
}