Skip to content

Commit e6b4008

Browse files
committed
Add keyring-util
Fixes: #48
1 parent 85ce1bf commit e6b4008

File tree

5 files changed

+329
-3
lines changed

5 files changed

+329
-3
lines changed

.github/workflows/CI.yml

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@ jobs:
1616
profile: minimal
1717
toolchain: stable
1818
override: true
19-
- uses: actions-rs/cargo@v1
19+
- name: Check
20+
uses: actions-rs/cargo@v1
2021
with:
2122
command: check
23+
- name: Check (keyring-util)
24+
uses: actions-rs/cargo@v1
25+
with:
26+
command: check
27+
args: -p keyring-util
2228

2329
test:
2430
name: Test Suite
@@ -40,6 +46,9 @@ jobs:
4046
- name: Build and test (OpenSSL)
4147
run: |
4248
dbus-run-session -- cargo test --no-default-features --features async-std --features openssl_crypto
49+
- name: Build and test (keyring-util)
50+
run: |
51+
cargo test -p keyring-util
4352
4453
fmt:
4554
name: Rustfmt
@@ -52,10 +61,16 @@ jobs:
5261
toolchain: nightly
5362
override: true
5463
- run: rustup component add rustfmt
55-
- uses: actions-rs/cargo@v1
64+
- name: Rust Format
65+
uses: actions-rs/cargo@v1
5666
with:
5767
command: fmt
5868
args: --all -- --check
69+
- name: Rust Format (keyring-util)
70+
uses: actions-rs/cargo@v1
71+
with:
72+
command: fmt
73+
args: --all -p keyring-util -- --check
5974

6075
clippy:
6176
name: Clippy
@@ -68,7 +83,13 @@ jobs:
6883
toolchain: stable
6984
override: true
7085
- run: rustup component add clippy
71-
- uses: actions-rs/cargo@v1
86+
- name: Clippy
87+
uses: actions-rs/cargo@v1
7288
with:
7389
command: clippy
7490
args: -- -D warnings
91+
- name: Clippy (keyring-util)
92+
uses: actions-rs/cargo@v1
93+
with:
94+
command: clippy
95+
args: -p keyring-util -- -D warnings

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/target
22
Cargo.lock
3+
keyring-util/target

keyring-util/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "keyring-util"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
async-std = { version = "1.12.0", features = [ "attributes" ] }
8+
chrono = { version = "0.4.31", default-features = false, features = ["alloc", "clock"] }
9+
clap = { version = "4.4.6", features = [ "cargo", "derive" ] }
10+
oo7 = { path = "../" }
11+
rpassword = "7.2.0"

keyring-util/src/main.rs

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
use std::{
2+
collections::HashMap,
3+
fmt,
4+
io::Write,
5+
process::{ExitCode, Termination},
6+
};
7+
8+
use clap::{Command, CommandFactory, FromArgMatches, Parser};
9+
use oo7::{
10+
dbus::{Algorithm, Collection, Service},
11+
zbus,
12+
};
13+
14+
struct Error(String);
15+
16+
impl fmt::Debug for Error {
17+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18+
write!(f, "{}", self.0)
19+
}
20+
}
21+
22+
impl From<oo7::dbus::Error> for Error {
23+
fn from(err: oo7::dbus::Error) -> Error {
24+
Error(err.to_string())
25+
}
26+
}
27+
28+
impl Error {
29+
fn new(s: &str) -> Self {
30+
Self(String::from(s))
31+
}
32+
}
33+
34+
impl Termination for Error {
35+
fn report(self) -> ExitCode {
36+
ExitCode::FAILURE
37+
}
38+
}
39+
40+
#[derive(Parser)]
41+
#[command(
42+
name = "store",
43+
about = "Store a secret",
44+
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"
45+
)]
46+
struct StoreArgs {
47+
#[clap(help = "Description for the secret")]
48+
label: String,
49+
#[clap(help = "List of attributes. This is a space separated list of pairs of key value")]
50+
attributes: Vec<String>,
51+
}
52+
53+
#[derive(Parser)]
54+
#[command(
55+
name = "search",
56+
about = "Search entries with matching attributes",
57+
after_help = "Example:\n keyring-util search --all smtp-port 1025"
58+
)]
59+
struct SearchArgs {
60+
#[clap(help = "List of attributes. This is a space separated list of pairs of key value")]
61+
attributes: Vec<String>,
62+
#[clap(
63+
short,
64+
long,
65+
help = "Whether to list all possible matches or only the first result"
66+
)]
67+
all: bool,
68+
}
69+
70+
#[derive(Parser)]
71+
#[command(
72+
name = "lookup",
73+
about = "Retrieve a secret",
74+
after_help = "Example:\n keyring-util lookup smtp-port 1025"
75+
)]
76+
struct LookupArgs {
77+
#[clap(help = "List of attributes. This is a space separated list of pairs of key value")]
78+
attributes: Vec<String>,
79+
}
80+
81+
#[derive(Parser)]
82+
#[command(
83+
name = "delete",
84+
about = "Delete a secret",
85+
after_help = "Will delete all secrets with matching attributes.\n\nExample:\n keyring-util delete smtp-port 1025"
86+
)]
87+
struct DeleteArgs {
88+
#[clap(help = "List of attributes. This is a space separated list of pairs of key value")]
89+
attributes: Vec<String>,
90+
}
91+
92+
#[async_std::main]
93+
async fn main() -> Result<(), Error> {
94+
let cmd = Command::new("keyring-util")
95+
.bin_name("keyring-util")
96+
.subcommand_required(true)
97+
.subcommand(StoreArgs::command())
98+
.subcommand(LookupArgs::command())
99+
.subcommand(DeleteArgs::command())
100+
.subcommand(SearchArgs::command())
101+
.subcommand(Command::new("lock").about("Lock the keyring"))
102+
.subcommand(Command::new("unlock").about("Unlock the keyring"));
103+
let matches = cmd.get_matches();
104+
match matches.subcommand() {
105+
Some(("store", matches)) => {
106+
let args =
107+
StoreArgs::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
108+
let attributes = parse_attributes(&args.attributes)?;
109+
110+
store(&args.label, attributes).await
111+
}
112+
Some(("lookup", matches)) => {
113+
let args =
114+
LookupArgs::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
115+
let attributes = parse_attributes(&args.attributes)?;
116+
117+
lookup(attributes).await
118+
}
119+
Some(("search", matches)) => {
120+
let args =
121+
SearchArgs::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
122+
let attributes = parse_attributes(&args.attributes)?;
123+
124+
search(attributes, args.all).await
125+
}
126+
Some(("delete", matches)) => {
127+
let args =
128+
LookupArgs::from_arg_matches(matches).map_err(|e| Error::new(&e.to_string()))?;
129+
let attributes = parse_attributes(&args.attributes)?;
130+
131+
delete(attributes).await
132+
}
133+
Some(("lock", _matches)) => lock().await,
134+
Some(("unlock", _matches)) => unlock().await,
135+
_ => unreachable!("clap should ensure we don't get here"),
136+
}
137+
}
138+
139+
fn parse_attributes(attributes: &[String]) -> Result<HashMap<&str, &str>, Error> {
140+
// Should this allow attribute-less secrets?
141+
let mut attributes = attributes.iter();
142+
if attributes.len() == 0 {
143+
return Err(Error(String::from(
144+
"Need to specify at least one attribute",
145+
)));
146+
}
147+
let mut result = HashMap::new();
148+
while let (Some(k), Some(v)) = (attributes.next(), attributes.next()) {
149+
result.insert(k.as_str(), v.as_str());
150+
}
151+
match attributes.next() {
152+
None => Ok(result),
153+
Some(k) => Err(Error(String::from(&format!(
154+
"Key '{k}' is missing a value"
155+
)))),
156+
}
157+
}
158+
159+
async fn store(label: &str, attributes: HashMap<&str, &str>) -> Result<(), Error> {
160+
let collection = collection().await?;
161+
162+
print!("Type a secret: ");
163+
std::io::stdout()
164+
.flush()
165+
.map_err(|_| Error::new("Could not flush stdout"))?;
166+
let secret = rpassword::read_password().map_err(|_| Error::new("Can't read password"))?;
167+
168+
collection
169+
.create_item(label, attributes, &secret, true, "text/plain")
170+
.await?;
171+
172+
Ok(())
173+
}
174+
175+
async fn lookup(attributes: HashMap<&str, &str>) -> Result<(), Error> {
176+
let collection = collection().await?;
177+
let items = collection.search_items(attributes).await?;
178+
179+
if let Some(item) = items.first() {
180+
let bytes = item.secret().await?;
181+
let secret =
182+
std::str::from_utf8(&bytes).map_err(|_| Error::new("Secret is not valid utf-8"))?;
183+
println!("{secret}");
184+
}
185+
186+
Ok(())
187+
}
188+
189+
async fn search(attributes: HashMap<&str, &str>, all: bool) -> Result<(), Error> {
190+
let collection = collection().await?;
191+
let items = collection.search_items(attributes).await?;
192+
193+
if all {
194+
for item in items {
195+
print_item(&item).await?;
196+
}
197+
} else if let Some(item) = items.first() {
198+
print_item(item).await?;
199+
}
200+
201+
Ok(())
202+
}
203+
204+
async fn delete(attributes: HashMap<&str, &str>) -> Result<(), Error> {
205+
let collection = collection().await?;
206+
let items = collection.search_items(attributes).await?;
207+
208+
for item in items {
209+
item.delete().await?;
210+
}
211+
212+
Ok(())
213+
}
214+
215+
async fn lock() -> Result<(), Error> {
216+
let collection = collection().await?;
217+
collection.lock().await?;
218+
219+
Ok(())
220+
}
221+
222+
async fn unlock() -> Result<(), Error> {
223+
let collection = collection().await?;
224+
collection.unlock().await?;
225+
226+
Ok(())
227+
}
228+
229+
async fn print_item<'a>(item: &oo7::dbus::Item<'a>) -> Result<(), Error> {
230+
use std::fmt::Write;
231+
232+
let label = item.label().await?;
233+
let bytes = item.secret().await?;
234+
// TODO Maybe show bytes in hex instead of failing?
235+
let secret =
236+
std::str::from_utf8(&bytes).map_err(|_| Error::new("Secret is not valid utf-8"))?;
237+
let mut attributes = item.attributes().await?;
238+
let created = item.created().await?;
239+
let modified = item.modified().await?;
240+
241+
let created = chrono::DateTime::<chrono::Utc>::from_timestamp(created.as_secs() as i64, 0)
242+
.unwrap()
243+
.with_timezone(&chrono::Local);
244+
let modified = chrono::DateTime::<chrono::Utc>::from_timestamp(modified.as_secs() as i64, 0)
245+
.unwrap()
246+
.with_timezone(&chrono::Local);
247+
248+
let mut result = format!("[{label}]\n");
249+
writeln!(&mut result, "secret = {secret}").unwrap();
250+
writeln!(
251+
&mut result,
252+
"created = {}",
253+
created.format("%Y-%m-%d %H:%M:%S")
254+
)
255+
.unwrap();
256+
writeln!(
257+
&mut result,
258+
"modified = {}",
259+
modified.format("%Y-%m-%d %H:%M:%S")
260+
)
261+
.unwrap();
262+
if let Some(schema) = attributes.remove("xdg:schema") {
263+
writeln!(&mut result, "schema = {schema} ").unwrap();
264+
}
265+
writeln!(&mut result, "attributes = {attributes:?} ").unwrap();
266+
print!("{result}");
267+
268+
Ok(())
269+
}
270+
271+
async fn collection<'a>() -> Result<Collection<'a>, Error> {
272+
let service = match Service::new(Algorithm::Encrypted).await {
273+
Ok(service) => Ok(service),
274+
Err(oo7::dbus::Error::Zbus(zbus::Error::MethodError(_, _, _))) => {
275+
Service::new(Algorithm::Plain).await
276+
}
277+
Err(e) => Err(e),
278+
}?;
279+
280+
let collection = match service.default_collection().await {
281+
Ok(c) => Ok(c),
282+
Err(oo7::dbus::Error::NotFound(_)) => {
283+
service
284+
.create_collection("Login", Some(oo7::dbus::DEFAULT_COLLECTION))
285+
.await
286+
}
287+
Err(e) => Err(e),
288+
}?;
289+
290+
Ok(collection)
291+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub use error::{Error, Result};
2525
pub use keyring::{Item, Keyring};
2626
pub use migration::migrate;
2727

28+
pub use zbus;
29+
2830
/// Checks whether the application is sandboxed or not.
2931
pub async fn is_sandboxed() -> bool {
3032
helpers::is_flatpak().await || helpers::is_snap().await

0 commit comments

Comments
 (0)