Skip to content

Commit 78446c3

Browse files
client: Don't over-abstract locked/unlocked items
Fixes #416
1 parent acb06ca commit 78446c3

11 files changed

Lines changed: 150 additions & 148 deletions

File tree

cli/src/main.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,16 @@ impl ItemOutput {
119119
}
120120
}
121121

122-
fn from_file_item(item: &oo7::file::Item, as_hex: bool) -> Self {
123-
let unlocked = item.as_unlocked();
122+
fn from_file_item(item: &oo7::file::UnlockedItem, as_hex: bool) -> Self {
124123
Self::new(
125-
&unlocked.secret(),
126-
unlocked.label(),
127-
unlocked
128-
.attributes()
124+
&item.secret(),
125+
item.label(),
126+
item.attributes()
129127
.iter()
130128
.map(|(k, v)| (k.to_string(), v.to_string()))
131129
.collect(),
132-
unlocked.created(),
133-
unlocked.modified(),
130+
item.created(),
131+
item.modified(),
134132
as_hex,
135133
)
136134
}
@@ -378,7 +376,7 @@ impl Commands {
378376
let items = keyring.search_items(&attributes).await?;
379377
if let Some(item) = items.first() {
380378
if secret_only {
381-
Output::SecretOnly(vec![item.as_unlocked().secret().clone()], hex)
379+
Output::SecretOnly(vec![item.secret().clone()], hex)
382380
} else {
383381
Output::Items(vec![ItemOutput::from_file_item(item, hex)], json)
384382
}
@@ -405,7 +403,7 @@ impl Commands {
405403
if secret_only {
406404
let secrets = items_to_print
407405
.into_iter()
408-
.map(|item| item.as_unlocked().secret().clone())
406+
.map(|item| item.secret().clone())
409407
.collect();
410408

411409
Output::SecretOnly(secrets, hex)
@@ -472,14 +470,15 @@ impl Commands {
472470
Commands::List { hex, json } => {
473471
let items = match keyring {
474472
Keyring::File(keyring) => {
475-
let items = keyring.items().await?;
473+
let items = keyring.all_items().await?;
476474
let mut outputs = Vec::new();
477475
for item in items {
478-
if let Ok(item) = item {
479-
outputs.push(ItemOutput::from_file_item(&item, hex));
480-
} else if !json {
481-
// Only print error message in text mode, skip in JSON mode
482-
println!("Item is not valid and cannot be decrypted");
476+
match item {
477+
Ok(item) => outputs.push(ItemOutput::from_file_item(&item, hex)),
478+
Err(_) if !json => {
479+
println!("Item is not valid and cannot be decrypted");
480+
}
481+
Err(_) => {} // Skip invalid items in JSON mode
483482
}
484483
}
485484
outputs

client/examples/file_tracing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ async fn test_search_performance() -> oo7::Result<()> {
294294
let start = Instant::now();
295295
let items = keyring.items().await?;
296296
let all_items_time = start.elapsed();
297-
let valid_items = items.iter().filter(|r| r.is_ok()).count();
297+
let valid_items = items.iter().count();
298298
info!(
299299
"Get all items: {:?} (found {} valid items)",
300300
all_items_time, valid_items

client/examples/schema.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,14 @@ async fn main() -> oo7::Result<()> {
5252
println!("Found {} item(s)", items.len());
5353

5454
for item in &items {
55-
let unlocked = item.as_unlocked();
56-
println!(" Label: {}", unlocked.label());
57-
println!(" Secret: {:?}", unlocked.secret());
55+
println!(" Label: {}", item.label());
56+
println!(" Secret: {:?}", item.secret());
5857
}
5958

6059
println!("\n=== Typed attributes ===");
6160

6261
if let Some(item) = items.first() {
63-
let schema = item.as_unlocked().attributes_as::<PasswordSchema>()?;
62+
let schema = item.attributes_as::<PasswordSchema>()?;
6463
println!("Username: {}", schema.username);
6564
println!("Server: {}", schema.server);
6665
println!("Port: {:?}", schema.port);

client/src/file/locked_keyring.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use tokio::{
1818
sync::{Mutex, RwLock},
1919
};
2020

21-
use super::{Error, Item, LockedItem, UnlockedKeyring, api};
22-
use crate::{Secret, file::InvalidItemError};
21+
use super::{Error, LockedItem, UnlockedKeyring, api};
22+
use crate::Secret;
2323

2424
/// A locked keyring that requires a secret to unlock.
2525
#[derive(Debug)]
@@ -56,16 +56,14 @@ impl LockedKeyring {
5656

5757
/// Retrieve the list of available [`LockedItem`]s without decrypting them.
5858
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
59-
pub async fn items(&self) -> Result<Vec<Result<Item, InvalidItemError>>, Error> {
59+
pub async fn items(&self) -> Result<Vec<LockedItem>, Error> {
6060
let keyring = self.keyring.read().await;
6161

6262
Ok(keyring
6363
.items
6464
.iter()
65-
.map(|encrypted_item| {
66-
Ok(Item::Locked(LockedItem {
67-
inner: encrypted_item.clone(),
68-
}))
65+
.map(|encrypted_item| LockedItem {
66+
inner: encrypted_item.clone(),
6967
})
7068
.collect())
7169
}

client/src/file/mod.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010
//! .await?;
1111
//!
1212
//! let items = keyring.search_items(&[("account", "alice")]).await?;
13-
//! assert_eq!(
14-
//! items[0].as_unlocked().secret(),
15-
//! oo7::Secret::blob("My Password")
16-
//! );
13+
//! assert_eq!(items[0].secret(), oo7::Secret::blob("My Password"));
1714
//!
1815
//! keyring.delete(&[("account", "alice")]).await?;
1916
//! # Ok(())
@@ -46,6 +43,18 @@ pub enum Item {
4643
Unlocked(UnlockedItem),
4744
}
4845

46+
impl From<UnlockedItem> for Item {
47+
fn from(item: UnlockedItem) -> Self {
48+
Self::Unlocked(item)
49+
}
50+
}
51+
52+
impl From<LockedItem> for Item {
53+
fn from(item: LockedItem) -> Self {
54+
Self::Locked(item)
55+
}
56+
}
57+
4958
impl Item {
5059
pub const fn is_locked(&self) -> bool {
5160
matches!(self, Self::Locked(_))
@@ -102,6 +111,18 @@ pub enum Keyring {
102111
Unlocked(UnlockedKeyring),
103112
}
104113

114+
impl From<LockedKeyring> for Keyring {
115+
fn from(keyring: LockedKeyring) -> Self {
116+
Self::Locked(keyring)
117+
}
118+
}
119+
120+
impl From<UnlockedKeyring> for Keyring {
121+
fn from(keyring: UnlockedKeyring) -> Self {
122+
Self::Unlocked(keyring)
123+
}
124+
}
125+
105126
impl Keyring {
106127
/// Validate that a secret can decrypt the items in this keyring.
107128
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, secret)))]
@@ -136,10 +157,20 @@ impl Keyring {
136157
.and_then(|time| time.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
137158
}
138159

139-
pub async fn items(&self) -> Result<Vec<Result<Item, InvalidItemError>>, Error> {
160+
pub async fn items(&self) -> Result<Vec<Item>, Error> {
140161
match self {
141-
Self::Locked(keyring) => keyring.items().await,
142-
Self::Unlocked(keyring) => keyring.items().await,
162+
Self::Locked(keyring) => Ok(keyring
163+
.items()
164+
.await?
165+
.into_iter()
166+
.map(Item::Locked)
167+
.collect()),
168+
Self::Unlocked(keyring) => Ok(keyring
169+
.items()
170+
.await?
171+
.into_iter()
172+
.map(Item::Unlocked)
173+
.collect()),
143174
}
144175
}
145176

client/src/file/unlocked_keyring.rs

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use tokio::{
1919

2020
use crate::{
2121
AsAttributes, Key, Secret,
22-
file::{Error, InvalidItemError, Item, LockedItem, LockedKeyring, UnlockedItem, api},
22+
file::{Error, InvalidItemError, LockedItem, LockedKeyring, UnlockedItem, api},
2323
};
2424

2525
/// Definition for batch item creation: (label, attributes, secret, replace)
@@ -243,45 +243,55 @@ impl UnlockedKeyring {
243243
self.keyring.read().await.items.len()
244244
}
245245

246-
/// Retrieve the list of available [`UnlockedItem`]s.
246+
/// Retrieve all items including those that cannot be decrypted.
247+
///
248+
/// Returns a [`Vec`] where each element is either an [`UnlockedItem`] or an
249+
/// [`InvalidItemError`] for items that failed to decrypt.
247250
///
248-
/// If items cannot be decrypted, [`InvalidItemError`]s are returned for
249-
/// them instead of [`UnlockedItem`]s.
251+
/// Use this method when you need to know about or handle decryption
252+
/// failures. For most use cases, [`items()`](Self::items) is more
253+
/// convenient as it only returns successfully decrypted items.
250254
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
251-
pub async fn items(&self) -> Result<Vec<Result<Item, InvalidItemError>>, Error> {
255+
pub async fn all_items(&self) -> Result<Vec<Result<UnlockedItem, InvalidItemError>>, Error> {
252256
let key = self.derive_key().await?;
253257
let keyring = self.keyring.read().await;
254258

255259
#[cfg(feature = "tracing")]
256-
let _span = tracing::debug_span!("decrypt", total_items = keyring.items.len());
260+
let _span = tracing::debug_span!("decrypt_all", total_items = keyring.items.len());
257261

258262
Ok(keyring
259263
.items
260264
.iter()
261265
.map(|e| {
262-
(*e).clone()
263-
.decrypt(&key)
264-
.map_err(|err| {
265-
InvalidItemError::new(
266-
err,
267-
e.hashed_attributes.keys().map(|x| x.to_string()).collect(),
268-
)
269-
})
270-
.map(Item::Unlocked)
266+
(*e).clone().decrypt(&key).map_err(|err| {
267+
InvalidItemError::new(
268+
err,
269+
e.hashed_attributes.keys().map(|x| x.to_string()).collect(),
270+
)
271+
})
271272
})
272273
.collect())
273274
}
274275

276+
/// Retrieve the list of available [`UnlockedItem`]s.
277+
///
278+
/// Items that cannot be decrypted are silently skipped. Use
279+
/// [`all_items()`](Self::all_items) if you need access to decryption
280+
/// errors.
281+
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
282+
pub async fn items(&self) -> Result<Vec<UnlockedItem>, Error> {
283+
Ok(self.all_items().await?.into_iter().flatten().collect())
284+
}
285+
275286
/// Search items matching the attributes.
276287
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
277-
pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result<Vec<Item>, Error> {
288+
pub async fn search_items(
289+
&self,
290+
attributes: &impl AsAttributes,
291+
) -> Result<Vec<UnlockedItem>, Error> {
278292
let key = self.derive_key().await?;
279293
let keyring = self.keyring.read().await;
280-
let results = keyring
281-
.search_items(attributes, &key)?
282-
.into_iter()
283-
.map(Item::Unlocked)
284-
.collect::<Vec<Item>>();
294+
let results = keyring.search_items(attributes, &key)?;
285295

286296
#[cfg(feature = "tracing")]
287297
tracing::debug!("Found {} matching items", results.len());
@@ -291,13 +301,14 @@ impl UnlockedKeyring {
291301

292302
/// Find the first item matching the attributes.
293303
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, attributes)))]
294-
pub async fn lookup_item(&self, attributes: &impl AsAttributes) -> Result<Option<Item>, Error> {
304+
pub async fn lookup_item(
305+
&self,
306+
attributes: &impl AsAttributes,
307+
) -> Result<Option<UnlockedItem>, Error> {
295308
let key = self.derive_key().await?;
296309
let keyring = self.keyring.read().await;
297310

298-
keyring
299-
.lookup_item(attributes, &key)
300-
.map(|maybe_item| maybe_item.map(Item::Unlocked))
311+
keyring.lookup_item(attributes, &key)
301312
}
302313

303314
/// Find the index in the list of items of the first item matching the
@@ -354,7 +365,7 @@ impl UnlockedKeyring {
354365
attributes: &impl AsAttributes,
355366
secret: impl Into<Secret>,
356367
replace: bool,
357-
) -> Result<Item, Error> {
368+
) -> Result<UnlockedItem, Error> {
358369
let item = {
359370
let key = self.derive_key().await?;
360371
let mut keyring = self.keyring.write().await;
@@ -375,7 +386,7 @@ impl UnlockedKeyring {
375386
Ok(_) => {
376387
#[cfg(feature = "tracing")]
377388
tracing::info!("Successfully created item");
378-
Ok(Item::Unlocked(item))
389+
Ok(item)
379390
}
380391
}
381392
}

client/src/keyring.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,7 @@ impl Keyring {
174174
let items = backend.items().await.map_err(crate::Error::File)?;
175175
items
176176
.into_iter()
177-
// Ignore invalid items
178-
.flatten()
179-
.map(|i| Item::for_file(i, Arc::clone(keyring)))
177+
.map(|i| Item::for_file(i.into(), Arc::clone(keyring)))
180178
.collect::<Vec<_>>()
181179
}
182180
Some(file::Keyring::Locked(_)) => {
@@ -239,7 +237,7 @@ impl Keyring {
239237
.map_err(crate::Error::File)?;
240238
items
241239
.into_iter()
242-
.map(|i| Item::for_file(i, Arc::clone(keyring)))
240+
.map(|i| Item::for_file(i.into(), Arc::clone(keyring)))
243241
.collect::<Vec<_>>()
244242
}
245243
Some(file::Keyring::Locked(_)) => {

0 commit comments

Comments
 (0)