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 17, 2023
1 parent 008fc56 commit 0e190a6
Show file tree
Hide file tree
Showing 3 changed files with 269 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" ] }
oo7 = { path = "../" }
rpassword = "7.2.0"
257 changes: 257 additions & 0 deletions keyring-util/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
use std::collections::HashMap;
use std::fmt;
use std::io::Write;
use std::process::{ExitCode, Termination};

use clap::parser::ValuesRef;
use clap::{arg, value_parser, Command};
use oo7::Keyring;

struct Error(String);

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

impl From<oo7::Error> for Error {
fn from(_err: oo7::Error) -> Error {
// TODO
Error(String::new())
}
}

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

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

#[async_std::main]
async fn main() -> Result<(), Error> {
let cmd = Command::new("keyring-util")
.bin_name("keyring-util")
.subcommand_required(true)
.subcommand(
Command::new("store")
.arg(
arg!(label: <LABEL>)
.required(true)
.help("Description for the secret")
.value_parser(value_parser!(String)),
)
.arg(
arg!(attributes: [ATTRIBUTES] ...)
.help("List of attributes. This is a space separated list of pairs of key value")
.value_parser(value_parser!(String))
)
.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")
.about("Store a secret"),
)
.subcommand(
Command::new("lookup")
.arg(
arg!(attributes: [ATTRIBUTES] ...)
.help("List of attributes. This is a space separated list of pairs of key value")
.value_parser(value_parser!(String))
)
.after_help("Example:\n keyring-util lookup smtp-port 1025")
.about("Retrieve a secret"),
)
.subcommand(
Command::new("delete")
.arg(
arg!(attributes: [ATTRIBUTES] ...)
.help("List of attributes. This is a space separated list of pairs of key value")
.value_parser(value_parser!(String))
)
.after_help("Will delete all secrets with matching attributes.\n\nExample:\n keyring-util delete smtp-port 1025")
.about("Delete a secret"),
)
.subcommand(
Command::new("search")
.about("Search entries with matching attributes")
.after_help("Example:\n keyring-util search --all smtp-port 1025")
.arg(
arg!(--"all")
.help("Whether to list all possible matches or only the first result")
)
.arg(
arg!(attributes: [ATTRIBUTES] ...)
.help("List of attributes. This is a space separated list of pairs of key value")
.value_parser(value_parser!(String))
),
)
.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 label = matches.get_one::<String>("label").unwrap();
let attributes = matches.get_many::<String>("attributes").unwrap_or_default();
let attributes = parse_attributes(attributes)?;

store(label, attributes).await
}
Some(("lookup", matches)) => {
let attributes = matches.get_many::<String>("attributes").unwrap();
let attributes = parse_attributes(attributes)?;

lookup(attributes).await
}
Some(("search", matches)) => {
let all = matches.get_one::<bool>("all").unwrap();
let attributes = matches.get_many::<String>("attributes").unwrap();
let attributes = parse_attributes(attributes)?;

search(attributes, *all).await
}
Some(("delete", matches)) => {
let attributes = matches.get_many::<String>("attributes").unwrap();
let attributes = parse_attributes(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(
mut attributes: ValuesRef<'_, String>,
) -> Result<HashMap<&str, &str>, Error> {
// Should this allow attribute-less secrets?
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 keyring = Keyring::new().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"))?;

keyring
.create_item(label, attributes, &secret, true)
.await?;

Ok(())
}

async fn lookup(attributes: HashMap<&str, &str>) -> Result<(), Error> {
let keyring = Keyring::new().await?;

let items = keyring.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 keyring = Keyring::new().await?;

let items = keyring.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 keyring = Keyring::new().await?;
keyring.delete(attributes).await?;

Ok(())
}

async fn lock() -> Result<(), Error> {
let keyring = Keyring::new().await?;
keyring.lock().await?;

Ok(())
}

async fn unlock() -> Result<(), Error> {
let keyring = Keyring::new().await?;
keyring.unlock().await?;

Ok(())
}

async fn print_item(item: &oo7::Item) -> 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(())
}

0 comments on commit 0e190a6

Please sign in to comment.