Skip to content

Commit

Permalink
Add keyring-util
Browse files Browse the repository at this point in the history
Fixes: #48
  • Loading branch information
A6GibKm committed Nov 25, 2023
1 parent 008fc56 commit dd66e26
Show file tree
Hide file tree
Showing 3 changed files with 301 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
Cargo.lock
keyring-util/target
11 changes: 11 additions & 0 deletions keyring-util/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "keyring-util"
version = "0.1.0"
edition = "2021"

[dependencies]
async-std = { version = "1.12.0", features = [ "attributes" ] }
chrono = "0.4.31"
clap = { version = "4.4.6", features = [ "cargo", "derive" ] }
oo7 = { path = "../" }
rpassword = "7.2.0"
289 changes: 289 additions & 0 deletions keyring-util/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
use std::collections::HashMap;
use std::fmt;
use std::io::Write;
use std::process::{ExitCode, Termination};

use clap::parser::ValuesRef;
use clap::CommandFactory;
use clap::Parser;
use clap::{arg, value_parser, Command};
use oo7::dbus::{Algorithm, Collection, Service};

struct Error(String);

impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

impl From<oo7::dbus::Error> for Error {
fn from(err: oo7::dbus::Error) -> Error {
Error(err.to_string())
}
}

impl Error {
fn new(s: &str) -> Self {
Self(String::from(s))
}
}

impl Termination for Error {
fn report(self) -> ExitCode {
ExitCode::FAILURE
}
}

#[derive(Parser)]
#[command(
name = "store",
about = "Store a secret",
after_help = "The contents of the secret will be asked afterwards.\n\nExample:\n keyring-util store 'My Personal Mail' smtp-port 1025 imap-port 143"
)]
struct StoreArgs {
#[clap(help = "Description for the secret")]
label: String,
#[clap(
short,
long,
help = "List of attributes. This is a space separated list of pairs of key value"
)]
#[arg(num_args(1..))]
attributes: Vec<String>,
}

#[derive(Parser)]
#[command(
name = "search",
about = "Search entries with matching attributes",
after_help = "Example:\n keyring-util search --all smtp-port 1025"
)]
struct SearchArgs {
#[clap(
short,
long,
help = "List of attributes. This is a space separated list of pairs of key value"
)]
#[arg(num_args(1..))]
attributes: Vec<String>,
#[clap(
long,
help = "Whether to list all possible matches or only the first result"
)]
all: bool,
}

#[derive(Parser)]
#[command(
name = "lookup",
about = "Retrieve a secret",
after_help = "Example:\n keyring-util lookup smtp-port 1025"
)]
struct LookupArgs {
#[clap(
short,
long,
help = "List of attributes. This is a space separated list of pairs of key value"
)]
#[arg(num_args(1..))]
attributes: Vec<String>,
}

#[derive(Parser)]
#[command(
name = "delete",
about = "Delete a secret",
after_help = "Will delete all secrets with matching attributes.\n\nExample:\n keyring-util delete smtp-port 1025"
)]
struct DeleteArgs {
#[clap(
short,
long,
help = "List of attributes. This is a space separated list of pairs of key value"
)]
#[arg(num_args(1..))]
attributes: Vec<String>,
}

#[async_std::main]
async fn main() -> Result<(), Error> {
let cmd = Command::new("keyring-util")
.bin_name("keyring-util")
.subcommand_required(true)
.subcommand(StoreArgs::command())
.subcommand(LookupArgs::command())
.subcommand(DeleteArgs::command())
.subcommand(SearchArgs::command())
.subcommand(Command::new("lock").about("Lock the keyring"))
.subcommand(Command::new("unlock").about("Unlock the keyring"));
let matches = cmd.get_matches();
match matches.subcommand() {
Some(("store", _matches)) => {
let args = StoreArgs::parse();
let attributes = parse_attributes(&args.attributes)?;

store(&args.label, attributes).await
}
Some(("lookup", _matches)) => {
let args = LookupArgs::parse();
let attributes = parse_attributes(&args.attributes)?;

lookup(attributes).await
}
Some(("search", _matches)) => {
let args = SearchArgs::parse();
let attributes = parse_attributes(&args.attributes)?;

search(attributes, args.all).await
}
Some(("delete", _matches)) => {
let args = LookupArgs::parse();
let attributes = parse_attributes(&args.attributes)?;

delete(attributes).await
}
Some(("lock", _matches)) => lock().await,
Some(("unlock", _matches)) => unlock().await,
_ => unreachable!("clap should ensure we don't get here"),
}
}

fn parse_attributes(attributes: &[String]) -> Result<HashMap<&str, &str>, Error> {
// Should this allow attribute-less secrets?
let mut attributes = attributes.iter();
if attributes.len() == 0 {
return Err(Error(String::from(
"Need to specify at least one attribute",
)));
}
let mut result = HashMap::new();
while let (Some(k), Some(v)) = (attributes.next(), attributes.next()) {
result.insert(k.as_str(), v.as_str());
}
match attributes.next() {
None => Ok(result),
Some(k) => Err(Error(String::from(&format!(
"Key '{k}' is missing a value"
)))),
}
}

async fn store(label: &str, attributes: HashMap<&str, &str>) -> Result<(), Error> {
let collection = collection().await?;

print!("Type a secret: ");
std::io::stdout()
.flush()
.map_err(|_| Error::new("Could not flush stdout"))?;
let secret = rpassword::read_password().map_err(|_| Error::new("Can't read password"))?;

collection
.create_item(label, attributes, &secret, true, "text/plain")
.await?;

Ok(())
}

async fn lookup(attributes: HashMap<&str, &str>) -> Result<(), Error> {
let collection = collection().await?;
let items = collection.search_items(attributes).await?;

if let Some(item) = items.first() {
let bytes = item.secret().await?;
let secret =
std::str::from_utf8(&bytes).map_err(|_| Error::new("Secret is not valid utf-8"))?;
println!("{secret}");
}

Ok(())
}

async fn search(attributes: HashMap<&str, &str>, all: bool) -> Result<(), Error> {
let collection = collection().await?;
let items = collection.search_items(attributes).await?;

if all {
for item in items {
print_item(&item).await?;
}
} else if let Some(item) = items.first() {
print_item(item).await?;
}

Ok(())
}

async fn delete(attributes: HashMap<&str, &str>) -> Result<(), Error> {
let collection = collection().await?;
let items = collection.search_items(attributes).await?;

for item in items {
item.delete().await?;
}

Ok(())
}

async fn lock() -> Result<(), Error> {
let collection = collection().await?;
collection.lock().await?;

Ok(())
}

async fn unlock() -> Result<(), Error> {
let collection = collection().await?;
collection.unlock().await?;

Ok(())
}

async fn print_item<'a>(item: &oo7::dbus::Item<'a>) -> Result<(), Error> {
use std::fmt::Write;

let label = item.label().await?;
let bytes = item.secret().await?;
// TODO Maybe show bytes in hex instead of failing?
let secret =
std::str::from_utf8(&bytes).map_err(|_| Error::new("Secret is not valid utf-8"))?;
let mut attributes = item.attributes().await?;
let created = item.created().await?;
let modified = item.modified().await?;

let created = chrono::DateTime::<chrono::Utc>::from_timestamp(created.as_secs() as i64, 0)
.unwrap()
.with_timezone(&chrono::Local);
let modified = chrono::DateTime::<chrono::Utc>::from_timestamp(modified.as_secs() as i64, 0)
.unwrap()
.with_timezone(&chrono::Local);

let mut result = format!("[{label}]\n");
writeln!(&mut result, "secret = {secret}").unwrap();
writeln!(
&mut result,
"created = {}",
created.format("%Y-%m-%d %H:%M:%S")
)
.unwrap();
writeln!(
&mut result,
"modified = {}",
modified.format("%Y-%m-%d %H:%M:%S")
)
.unwrap();
if let Some(schema) = attributes.remove("xdg:schema") {
writeln!(&mut result, "schema = {schema} ").unwrap();
}
writeln!(&mut result, "attributes = {attributes:?} ").unwrap();
print!("{result}");

Ok(())
}

async fn collection<'a>() -> Result<Collection<'a>, Error> {
let service = Service::new(Algorithm::Encrypted)
.await
.or(Service::new(Algorithm::Plain).await)?;
Ok(service.default_collection().await?)
}

0 comments on commit dd66e26

Please sign in to comment.