Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ fn print_item_common(
if let Some(schema) = attributes.remove(oo7::XDG_SCHEMA_ATTRIBUTE) {
writeln!(&mut result, "schema = {schema} ").unwrap();
}
if let Some(content_type) = attributes.remove(oo7::CONTENT_TYPE_ATTRIBUTE) {
writeln!(&mut result, "content_type = {content_type} ").unwrap();
}
writeln!(&mut result, "attributes = {attributes:?} ").unwrap();
print!("{result}");
}
Expand Down
42 changes: 14 additions & 28 deletions client/src/dbus/api/secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ use zbus::zvariant::{OwnedObjectPath, Type};
use zeroize::{Zeroize, ZeroizeOnDrop};

use super::Session;
use crate::{
crypto,
dbus::Error,
secret::{BLOB_CONTENT_TYPE, TEXT_CONTENT_TYPE},
Key, Secret,
};
use crate::{crypto, dbus::Error, secret::ContentType, Key, Secret};

#[derive(Debug, Serialize, Deserialize, Type)]
#[zvariant(signature = "(oayays)")]
/// Same as [`DBusSecret`] without tying the session path to a [`Session`] type.
pub struct DBusSecretInner(pub OwnedObjectPath, pub Vec<u8>, pub Vec<u8>, pub String);
pub struct DBusSecretInner(
pub OwnedObjectPath,
pub Vec<u8>,
pub Vec<u8>,
pub ContentType,
);

#[derive(Debug, Type, Zeroize, ZeroizeOnDrop)]
#[zvariant(signature = "(oayays)")]
Expand All @@ -25,7 +25,7 @@ pub struct DBusSecret<'a> {
pub(crate) parameters: Vec<u8>,
pub(crate) value: Vec<u8>,
#[zeroize(skip)]
pub(crate) content_type: String,
pub(crate) content_type: ContentType,
}

impl<'a> DBusSecret<'a> {
Expand All @@ -35,7 +35,7 @@ impl<'a> DBusSecret<'a> {
session,
parameters: vec![],
value: secret.as_bytes().to_vec(),
content_type: secret.content_type().to_owned(),
content_type: secret.content_type(),
}
}

Expand All @@ -50,7 +50,7 @@ impl<'a> DBusSecret<'a> {
session,
value: crypto::encrypt(secret.as_bytes(), aes_key, &iv)?,
parameters: iv,
content_type: secret.content_type().to_owned(),
content_type: secret.content_type(),
})
}

Expand All @@ -71,21 +71,7 @@ impl<'a> DBusSecret<'a> {
Some(key) => &crypto::decrypt(&self.value, key, &self.parameters)?,
None => &self.value,
};
match self.content_type.as_str() {
TEXT_CONTENT_TYPE => {
match String::from_utf8(value.to_vec()) {
Ok(text) => Ok(Secret::Text(text)),
// Workaround gnome-keyring always reporting text/plain even if it is not one.
Err(_) => Ok(Secret::blob(value)),
}
}
BLOB_CONTENT_TYPE => Ok(Secret::blob(value)),
e => {
#[cfg(feature = "tracing")]
tracing::warn!("Unsupported content-type {e}, falling back to blob");
Ok(Secret::blob(value))
}
}
Ok(Secret::with_content_type(self.content_type, value))
}

/// Session used to encode the secret
Expand All @@ -104,8 +90,8 @@ impl<'a> DBusSecret<'a> {
}

/// Content type of the secret
pub fn content_type(&self) -> &str {
&self.content_type
pub fn content_type(&self) -> ContentType {
self.content_type
}
}

Expand All @@ -118,7 +104,7 @@ impl Serialize for DBusSecret<'_> {
tuple_serializer.serialize_element(self.session().inner().path())?;
tuple_serializer.serialize_element(self.parameters())?;
tuple_serializer.serialize_element(self.value())?;
tuple_serializer.serialize_element(self.content_type())?;
tuple_serializer.serialize_element(self.content_type().as_str())?;
tuple_serializer.end()
}
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/file/api/legacy_keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ mod tests {
assert_eq!(items[0].label(), "foo");
assert_eq!(items[0].secret(), Secret::blob("foo"));
let attributes = items[0].attributes();
assert_eq!(attributes.len(), 1);
assert_eq!(attributes.len(), 2); // also content-type
assert_eq!(
attributes
.get(crate::XDG_SCHEMA_ATTRIBUTE)
Expand Down
4 changes: 3 additions & 1 deletion client/src/file/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ mod tests {
use std::collections::HashMap;

use super::*;
use crate::secret::ContentType;

const SECRET: [u8; 64] = [
44, 173, 251, 20, 203, 56, 241, 169, 91, 54, 51, 244, 40, 40, 202, 92, 71, 233, 174, 17,
Expand Down Expand Up @@ -386,7 +387,8 @@ mod tests {
let loaded_items =
loaded_keyring.search_items(&HashMap::from([("my-tag", "my tag value")]), &key)?;

assert_eq!(loaded_items[0].secret(), Secret::blob("A Password"));
assert_eq!(loaded_items[0].secret(), Secret::text("A Password"));
assert_eq!(loaded_items[0].secret().content_type(), ContentType::Text);

let _silent = std::fs::remove_file("/tmp/test.keyring");

Expand Down
72 changes: 60 additions & 12 deletions client/src/file/item.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::HashMap, time::Duration};
use std::{collections::HashMap, str::FromStr, time::Duration};

use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
Expand All @@ -7,7 +7,7 @@ use super::{
api::{AttributeValue, EncryptedItem, GVARIANT_ENCODING},
Error,
};
use crate::{crypto, AsAttributes, Key, Secret};
use crate::{crypto, secret::ContentType, AsAttributes, Key, Secret, CONTENT_TYPE_ATTRIBUTE};

/// An item stored in the file backend.
#[derive(Deserialize, Serialize, zvariant::Type, Clone, Debug, Zeroize, ZeroizeOnDrop)]
Expand All @@ -34,16 +34,27 @@ impl Item {
.unwrap()
.as_secs();

let mut item_attributes: HashMap<String, AttributeValue> = attributes
.as_attributes()
.into_iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect();

let secret = secret.into();
// Set default MIME type if not provided
if !item_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
item_attributes.insert(
CONTENT_TYPE_ATTRIBUTE.to_owned(),
secret.content_type().as_str().into(),
);
}

Self {
attributes: attributes
.as_attributes()
.into_iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect(),
attributes: item_attributes,
label: label.to_string(),
created: now,
modified: now,
secret: secret.into().as_bytes().to_vec(),
secret: secret.as_bytes().to_vec(),
}
}

Expand All @@ -54,11 +65,32 @@ impl Item {

/// Update the item attributes.
pub fn set_attributes(&mut self, attributes: &impl AsAttributes) {
self.attributes = attributes
let mut new_attributes: HashMap<String, AttributeValue> = attributes
.as_attributes()
.into_iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect();

// Preserve MIME type if not explicitly set in new attributes
if !new_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
if let Some(existing_mime_type) = self.attributes.get(CONTENT_TYPE_ATTRIBUTE) {
new_attributes.insert(
CONTENT_TYPE_ATTRIBUTE.to_string(),
existing_mime_type.clone(),
);
} else {
new_attributes.insert(
CONTENT_TYPE_ATTRIBUTE.to_owned(),
ContentType::default().as_str().into(),
);
}
}

self.attributes = new_attributes;
self.modified = std::time::SystemTime::UNIX_EPOCH
.elapsed()
.unwrap()
.as_secs();
}

/// The item label.
Expand All @@ -77,7 +109,13 @@ impl Item {

/// Retrieve the currently stored secret.
pub fn secret(&self) -> Secret {
Secret::blob(&self.secret)
let content_type = self
.attributes
.get(CONTENT_TYPE_ATTRIBUTE)
.and_then(|c| ContentType::from_str(c).ok())
.unwrap_or_default();

Secret::with_content_type(content_type, &self.secret)
}

/// Store a new secret.
Expand Down Expand Up @@ -130,8 +168,18 @@ impl TryFrom<&[u8]> for Item {
type Error = Error;

fn try_from(value: &[u8]) -> Result<Self, Error> {
Ok(zvariant::serialized::Data::new(value, *GVARIANT_ENCODING)
let mut item: Item = zvariant::serialized::Data::new(value, *GVARIANT_ENCODING)
.deserialize()?
.0)
.0;

// Ensure MIME type attribute exists for backward compatibility
if !item.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) {
item.attributes.insert(
CONTENT_TYPE_ATTRIBUTE.to_owned(),
ContentType::default().as_str().into(),
);
}

Ok(item)
}
}
42 changes: 41 additions & 1 deletion client/src/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ mod tests {
assert_eq!(items[0].label(), "foo");
assert_eq!(items[0].secret(), Secret::blob("foo"));
let attributes = items[0].attributes();
assert_eq!(attributes.len(), 1);
assert_eq!(attributes.len(), 2);
assert_eq!(
attributes
.get(crate::XDG_SCHEMA_ATTRIBUTE)
Expand Down Expand Up @@ -913,4 +913,44 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn content_type() -> Result<(), Error> {
use crate::secret::ContentType;

let keyring = Keyring::temporary(Secret::blob("test_password")).await?;

// Add items with different MIME types
keyring
.create_item(
"Text",
&HashMap::from([("type", "text")]),
Secret::text("Hello, World!"),
false,
)
.await?;

keyring
.create_item(
"Password",
&HashMap::from([("type", "password")]),
Secret::blob("super_secret_password"),
false,
)
.await?;

let items = keyring
.search_items(&HashMap::from([("type", "text")]))
.await?;
assert_eq!(items.len(), 1);
assert_eq!(items[0].secret().content_type(), ContentType::Text);

let items = keyring
.search_items(&HashMap::from([("type", "password")]))
.await?;
assert_eq!(items.len(), 1);
assert_eq!(items[0].secret().content_type(), ContentType::Blob);

Ok(())
}
}
8 changes: 4 additions & 4 deletions client/src/keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,9 @@ mod tests {
assert_eq!(items.len(), 1);
let item = items.remove(0);
assert_eq!(item.label().await?, "my item");
assert_eq!(item.secret().await?, Secret::blob("my_secret"));
assert_eq!(item.secret().await?, Secret::text("my_secret"));
let attrs = item.attributes().await?;
assert_eq!(attrs.len(), 1);
assert_eq!(attrs.len(), 2);
assert_eq!(attrs.get("key").unwrap(), "value");

item.set_attributes(&vec![("key", "changed_value"), ("new_key", "new_value")])
Expand All @@ -415,9 +415,9 @@ mod tests {
assert_eq!(items.len(), 1);
let item = items.remove(0);
assert_eq!(item.label().await?, "my item");
assert_eq!(item.secret().await?, Secret::blob("my_secret"));
assert_eq!(item.secret().await?, Secret::text("my_secret"));
let attrs = item.attributes().await?;
assert_eq!(attrs.len(), 2);
assert_eq!(attrs.len(), 3);
assert_eq!(attrs.get("key").unwrap(), "changed_value");
assert_eq!(attrs.get("new_key").unwrap(), "new_value");

Expand Down
5 changes: 5 additions & 0 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ pub use zbus;
/// to map a Rust struct of simple types to an item attributes with type check.
pub const XDG_SCHEMA_ATTRIBUTE: &str = "xdg:schema";

/// A content type attribute.
///
/// Defines the type of the secret stored in the item.
pub const CONTENT_TYPE_ATTRIBUTE: &str = "xdg:content-type";

/// An item/collection attributes.
pub trait AsAttributes {
fn as_attributes(&self) -> HashMap<&str, &str>;
Expand Down
Loading