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 Dec 19, 2023
1 parent 85ce1bf commit e8c9a47
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 3 deletions.
27 changes: 24 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ jobs:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
- name: Check
uses: actions-rs/cargo@v1
with:
command: check
- name: Check (keyring-util)
uses: actions-rs/cargo@v1
with:
command: check
args: -p keyring-util

test:
name: Test Suite
Expand All @@ -40,6 +46,9 @@ jobs:
- name: Build and test (OpenSSL)
run: |
dbus-run-session -- cargo test --no-default-features --features async-std --features openssl_crypto
- name: Build and test keyring-util
run: |
cargo test -p keyring-util
fmt:
name: Rustfmt
Expand All @@ -52,10 +61,16 @@ jobs:
toolchain: nightly
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
- name: Rust Format
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Rust Format (keyring-util)
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -p keyring-util -- --check

clippy:
name: Clippy
Expand All @@ -68,7 +83,13 @@ jobs:
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
- name: Clippy (keyring-util)
uses: actions-rs/cargo@v1
with:
command: clippy
args: -p keyring-util -- -D warnings
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 = { version = "0.4.31", default-features = false, features = ["alloc", "clock"] }
clap = { version = "4.4.6", features = [ "cargo", "derive" ] }
oo7 = { path = "../" }
rpassword = "7.2.0"
291 changes: 291 additions & 0 deletions keyring-util/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
use std::{
collections::HashMap,
fmt,
io::Write,
process::{ExitCode, Termination},
};

use clap::{Command, CommandFactory, FromArgMatches, Parser};
use oo7::{
dbus::{Algorithm, Collection, Service},
zbus,
};

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(help = "List of attributes. This is a space separated list of pairs of key value")]
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(help = "List of attributes. This is a space separated list of pairs of key value")]
attributes: Vec<String>,
#[clap(
short,
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(help = "List of attributes. This is a space separated list of pairs of key value")]
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(help = "List of attributes. This is a space separated list of pairs of key value")]
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::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
let attributes = parse_attributes(&args.attributes)?;

store(&args.label, attributes).await
}
Some(("lookup", matches)) => {
let args =
LookupArgs::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
let attributes = parse_attributes(&args.attributes)?;

lookup(attributes).await
}
Some(("search", matches)) => {
let args =
SearchArgs::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
let attributes = parse_attributes(&args.attributes)?;

search(attributes, args.all).await
}
Some(("delete", matches)) => {
let args =
LookupArgs::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
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 = match Service::new(Algorithm::Encrypted).await {
Ok(service) => Ok(service),
Err(oo7::dbus::Error::Zbus(zbus::Error::MethodError(_, _, _))) => {
Service::new(Algorithm::Plain).await
}
Err(e) => Err(e),
}?;

let collection = match service.default_collection().await {
Ok(c) => Ok(c),
Err(oo7::dbus::Error::NotFound(_)) => {
service
.create_collection("Login", Some(oo7::dbus::DEFAULT_COLLECTION))
.await
}
Err(e) => Err(e),
}?;

Ok(collection)
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub use error::{Error, Result};
pub use keyring::{Item, Keyring};
pub use migration::migrate;

pub use zbus;

/// Checks whether the application is sandboxed or not.
pub async fn is_sandboxed() -> bool {
helpers::is_flatpak().await || helpers::is_snap().await
Expand Down

0 comments on commit e8c9a47

Please sign in to comment.