Implement a simple password manager

This commit is contained in:
Mattias Eriksson 2024-02-09 09:35:24 +01:00
parent 10fd396ea3
commit 71b9fb5226
8 changed files with 720 additions and 27 deletions

93
Cargo.lock generated
View file

@ -785,6 +785,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.5.1"
@ -962,6 +971,15 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.2.37"
@ -1544,8 +1562,11 @@ dependencies = [
"paste",
"ron",
"rust-embed",
"secret-service",
"secstr",
"serde",
"shlex",
"thiserror 2.0.16",
"tokio",
"url",
]
@ -2866,6 +2887,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
@ -3758,6 +3788,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
]
@ -4749,6 +4780,20 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
@ -4759,6 +4804,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@ -4785,6 +4839,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
@ -6204,6 +6269,34 @@ dependencies = [
"tiny-skia",
]
[[package]]
name = "secret-service"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
dependencies = [
"aes",
"cbc",
"futures-util",
"generic-array",
"getrandom 0.2.16",
"hkdf",
"num",
"once_cell",
"serde",
"sha2",
"zbus 5.11.0",
]
[[package]]
name = "secstr"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04f657244f605c4cf38f6de5993e8bd050c8a303f86aeabff142d5c7c113e12"
dependencies = [
"libc",
]
[[package]]
name = "self_cell"
version = "1.2.0"

View file

@ -29,6 +29,9 @@ i18n-embed-fl = "0.10"
icu = { version = "2.0.0", features = ["compiled_data"] }
rust-embed = "8"
url = "2.5"
secret-service = { version = "5.0.0", features = ["rt-tokio-crypto-rust"], optional = true }
thiserror = { version = "2.0", optional = true }
secstr = { version = "0.5", optional = true }
[dependencies.cosmic-files]
git = "https://github.com/pop-os/cosmic-files.git"
@ -48,10 +51,11 @@ features = ["about", "multi-window", "tokio", "winit", "surface-message"]
fork = "0.2"
[features]
default = ["dbus-config", "wgpu", "wayland"]
default = ["dbus-config", "wgpu", "wayland", "password_manager"]
dbus-config = ["libcosmic/dbus-config"]
wgpu = ["libcosmic/wgpu", "cosmic-files/wgpu"]
wayland = ["libcosmic/wayland", "cosmic-files/wayland"]
password_manager = [ "secret-service", "thiserror", "secstr" ]
[profile.release-with-debug]
inherits = "release"

View file

@ -99,3 +99,10 @@ pane-toggle-maximize = Toggle maximized
menu-color-schemes = Color schemes...
menu-settings = Settings...
menu-about = About COSMIC Terminal...
# Password Manager
menu-password-manager = Passwords...
passwords-title = Passwords
add-password = Add Password
password-input = Password
password-input-description = Description

View file

@ -111,3 +111,10 @@ menu-settings = Inställningar…
menu-about = Om COSMIC Terminal…
repository = Källkod
support = Support
# Lösenordshanterare
menu-password-manager = Lösenord…
passwords-title = Lösenord
add-password = Lägg till lösenord
password-input = Lösenord
password-input-description = Beskrivning

View file

@ -45,6 +45,8 @@ pub fn key_binds() -> HashMap<KeyBind, Action> {
Key::Character("X".into()),
PaneToggleMaximized
);
#[cfg(feature = "password_manager")]
bind!([Ctrl, Alt], Key::Character("p".into()), PasswordManager);
// Ctrl+Tab and Ctrl+Shift+Tab cycle through tabs
// Ctrl+Tab is not a special key for terminals and is free to use

View file

@ -65,6 +65,8 @@ use terminal_box::terminal_box;
use crate::dnd::DndDrop;
mod terminal_box;
#[cfg(feature = "password_manager")]
mod password_manager;
mod terminal_theme;
mod dnd;
@ -236,6 +238,8 @@ pub enum Action {
Profiles,
SelectAll,
Settings,
#[cfg(feature = "password_manager")]
PasswordManager,
ShowHeaderBar(bool),
TabActivate0,
TabActivate1,
@ -278,6 +282,8 @@ impl Action {
Self::PaneSplitHorizontal => Message::PaneSplit(pane_grid::Axis::Horizontal),
Self::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical),
Self::PaneToggleMaximized => Message::PaneToggleMaximized,
#[cfg(feature = "password_manager")]
Self::PasswordManager => Message::ToggleContextPage(ContextPage::PasswordManager),
Self::Paste => Message::Paste(entity_opt),
Self::PastePrimary => Message::PastePrimary(entity_opt),
Self::ProfileOpen(profile_id) => Message::ProfileOpen(*profile_id),
@ -362,6 +368,10 @@ pub enum Message {
PaneResized(pane_grid::ResizeEvent),
PaneSplit(pane_grid::Axis),
PaneToggleMaximized,
#[cfg(feature = "password_manager")]
PasswordManager(password_manager::PasswordManagerMessage),
#[cfg(feature = "password_manager")]
PasswordPaste(secstr::SecUtf8, pane_grid::Pane),
Paste(Option<segmented_button::Entity>),
PastePrimary(Option<segmented_button::Entity>),
PasteValue(Option<segmented_button::Entity>, String),
@ -412,6 +422,8 @@ pub enum ContextPage {
ColorSchemes(ColorSchemeKind),
Profiles,
Settings,
#[cfg(feature = "password_manager")]
PasswordManager,
}
/// The [`App`] stores application-specific state.
@ -456,6 +468,8 @@ pub struct App {
profile_expanded: Option<ProfileId>,
show_advanced_font_settings: bool,
modifiers: Modifiers,
#[cfg(feature = "password_manager")]
password_mgr: password_manager::PasswordManager,
}
impl App {
@ -1569,6 +1583,8 @@ impl Application for App {
profile_expanded: None,
show_advanced_font_settings: false,
modifiers: Modifiers::empty(),
#[cfg(feature = "password_manager")]
password_mgr: Default::default(),
};
app.set_curr_font_weights_and_stretches();
@ -1582,6 +1598,10 @@ impl Application for App {
if self.core.window.show_context {
// Close context drawer if open
self.core.window.show_context = false;
#[cfg(feature = "password_manager")]
if self.context_page == ContextPage::PasswordManager {
self.password_mgr.clear();
}
} else if self.find {
// Close find if open
self.find = false;
@ -1596,6 +1616,10 @@ impl Application for App {
if self.core.window.show_context {
Task::none()
} else {
#[cfg(feature = "password_manager")]
if self.context_page == ContextPage::PasswordManager {
self.password_mgr.clear();
}
self.update_focus()
}
}
@ -2147,6 +2171,23 @@ impl Application for App {
self.pane_model.panes.drop(pane, target);
}
Message::PaneDragged(_) => {}
#[cfg(feature = "password_manager")]
Message::PasswordManager(msg) => {
return self.password_mgr.update(msg);
}
#[cfg(feature = "password_manager")]
Message::PasswordPaste(password, pane) => {
if let Some(tab_model) = self.pane_model.panes.get(pane) {
let entity = tab_model.active();
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
terminal.paste(password.into_unsecure());
terminal.input_scroll(b"\n".as_slice());
self.core.window.show_context = false;
self.password_mgr.clear();
}
}
}
Message::Paste(entity_opt) => {
return clipboard::read().map(move |value_opt| match value_opt {
Some(value) => action::app(Message::PasteValue(entity_opt, value)),
@ -2612,6 +2653,16 @@ impl Application for App {
ColorSchemeKind::Light => light_entity,
});
}
#[cfg(feature = "password_manager")]
if ContextPage::PasswordManager == context_page {
if self.core.window.show_context {
self.password_mgr.pane = Some(self.pane_model.focused());
return self.password_mgr.refresh_password_list();
} else {
self.password_mgr.clear();
}
}
}
Message::UpdateDefaultProfile((default, profile_id)) => {
config_set!(default_profile, default.then_some(profile_id));
@ -2685,6 +2736,12 @@ impl Application for App {
Message::ToggleContextPage(ContextPage::Settings),
)
.title(fl!("settings")),
#[cfg(feature = "password_manager")]
ContextPage::PasswordManager => context_drawer::context_drawer(
self.password_mgr.context_page(self.core.system_theme()),
Message::ToggleContextPage(ContextPage::PasswordManager),
)
.title(fl!("passwords-title")),
})
}

View file

@ -70,7 +70,7 @@ pub fn context_menu<'a>(
.on_press(Message::TabContextAction(entity, action))
};
widget::container(column!(
let mut content = column!(
menu_item(fl!("copy"), Action::Copy),
menu_item(fl!("paste"), Action::Paste),
menu_item(fl!("select-all"), Action::SelectAll),
@ -83,12 +83,20 @@ pub fn context_menu<'a>(
divider::horizontal::light(),
menu_item(fl!("new-tab"), Action::TabNew),
menu_item(fl!("menu-settings"), Action::Settings),
menu_checkbox(
);
#[cfg(feature = "password_manager")]
{
content = content.push(menu_item(
fl!("menu-password-manager"),
Action::PasswordManager,
));
}
content = content.push(menu_checkbox(
fl!("show-headerbar"),
config.show_headerbar,
Action::ShowHeaderBar(!config.show_headerbar)
),
))
Action::ShowHeaderBar(!config.show_headerbar),
));
widget::container(content)
.padding(1)
//TODO: move style to libcosmic
.style(|theme| {
@ -234,6 +242,12 @@ pub fn menu_bar<'a>(
Action::ColorSchemes(config.color_scheme_kind()),
),
MenuItem::Button(fl!("menu-settings"), None, Action::Settings),
#[cfg(feature = "password_manager")]
MenuItem::Button(
fl!("menu-password-manager"),
None,
Action::PasswordManager,
),
MenuItem::Divider,
MenuItem::Button(fl!("menu-about"), None, Action::About),
],

509
src/password_manager.rs Normal file
View file

@ -0,0 +1,509 @@
use cosmic::{
Element, Task, Theme, cosmic_theme,
iced::{Alignment, Length, Padding},
style,
widget::{self, settings::Section},
};
use crate::{Message, fl, icon_cache_get};
#[derive(Clone, Debug)]
pub enum PasswordManagerMessage {
Error(String),
FetchAndPastePassword(String),
FetchAndExpand(String),
Collapse,
Delete(String),
Expand(String, secstr::SecUtf8),
New,
RefreshList,
ToggleShowPassword,
ListRefreshed(Vec<String>),
DescriptionInput(String),
DescriptionInputAndUpdate(String),
PasswordInput(String),
PasswordInputAndUpdate(String),
Update,
None,
}
struct PasswordInputState {
pub original: Option<InputState>,
pub input: InputState,
pub show_password: bool,
}
#[derive(Clone, PartialEq, Eq)]
struct InputState {
pub identifier: String,
pub password: String,
}
pub struct PasswordManager {
input_state: Option<PasswordInputState>,
pub password_list: Vec<String>,
//Which pane we should paste to, ie. which pane had focus when the
//password manager was opened. Just to be sure it doesn't change under
//our feet.
pub pane: Option<widget::pane_grid::Pane>,
pub expanded_entry: Option<String>,
}
impl PasswordManager {
pub fn new() -> Self {
Self {
input_state: None,
password_list: Default::default(),
pane: None,
expanded_entry: None,
}
}
pub fn update(&mut self, msg: PasswordManagerMessage) -> Task<cosmic::Action<Message>> {
match msg {
PasswordManagerMessage::Error(err) => {
log::error!("{err}");
}
PasswordManagerMessage::FetchAndPastePassword(identifier) => {
return self.fetch_and_paste(identifier);
}
PasswordManagerMessage::FetchAndExpand(identifier) => {
return self.fetch_and_expand(identifier);
}
PasswordManagerMessage::Delete(identifier) => {
return self.delete_password(identifier);
}
PasswordManagerMessage::RefreshList => {
return self.refresh_password_list();
}
PasswordManagerMessage::ListRefreshed(list) => {
self.password_list = list;
}
PasswordManagerMessage::Collapse => {
self.expanded_entry = None;
self.input_state = None;
self.expanded_entry = None;
}
PasswordManagerMessage::Expand(identifier, password) => {
self.input_state = Some(PasswordInputState {
original: Some(InputState {
identifier: identifier.clone(),
password: password.clone().into_unsecure(),
}),
input: InputState {
identifier: identifier.clone(),
password: password.into_unsecure(),
},
show_password: false,
});
self.expanded_entry = Some(identifier);
}
PasswordManagerMessage::ToggleShowPassword => {
if let Some(input_state) = self.input_state.as_mut() {
input_state.show_password = !input_state.show_password;
}
}
PasswordManagerMessage::DescriptionInput(description) => {
if let Some(input_state) = self.input_state.as_mut() {
input_state.input.identifier = description;
}
}
PasswordManagerMessage::DescriptionInputAndUpdate(description) => {
if let Some(input_state) = self.input_state.as_mut() {
input_state.input.identifier = description.clone();
return self.add_or_update_password_entry();
}
}
PasswordManagerMessage::PasswordInput(password) => {
if let Some(input_state) = self.input_state.as_mut() {
input_state.input.password = password;
}
}
PasswordManagerMessage::PasswordInputAndUpdate(password) => {
if let Some(input_state) = self.input_state.as_mut() {
input_state.input.password = password;
return self.add_or_update_password_entry();
}
}
PasswordManagerMessage::Update => {
return self.add_or_update_password_entry();
}
PasswordManagerMessage::New => {
self.new_password();
}
PasswordManagerMessage::None => {}
}
Task::none()
}
pub fn clear(&mut self) {
self.input_state = None;
self.password_list.clear();
self.pane = None;
self.expanded_entry = None;
}
pub fn fetch_and_paste(&self, identifier: String) -> Task<cosmic::Action<Message>> {
if let Some(pane) = self.pane {
cosmic::task::future(async move {
match store::get_password(identifier.clone()).await {
Ok(password) => Message::PasswordPaste(password, pane),
Err(err) => Message::PasswordManager(PasswordManagerMessage::Error(format!(
"Failed to fetch password {identifier}: {err}"
))),
}
})
} else {
log::error!("No active pane set for password manager to use");
Task::none()
}
}
pub fn fetch_and_expand(&mut self, identifier: String) -> Task<cosmic::Action<Message>> {
cosmic::task::future(async move {
match store::get_password(identifier.clone()).await {
Ok(password) => {
Message::PasswordManager(PasswordManagerMessage::Expand(identifier, password))
}
Err(err) => Message::PasswordManager(PasswordManagerMessage::Error(format!(
"Failed to fetch password {identifier}: {err}"
))),
}
})
}
pub fn refresh_password_list(&self) -> Task<cosmic::Action<Message>> {
cosmic::task::future(async {
match store::fetch_password_list().await {
Ok(list) => Message::PasswordManager(PasswordManagerMessage::ListRefreshed(list)),
Err(err) => Message::PasswordManager(PasswordManagerMessage::Error(format!(
"Failed to fetch password list: {err}"
))),
}
})
}
pub fn delete_password(&mut self, identifier: String) -> Task<cosmic::Action<Message>> {
if self.expanded_entry.as_ref() == Some(&identifier) {
self.expanded_entry = None;
}
cosmic::task::future(async move {
if let Err(err) = store::delete_password(identifier.clone()).await {
return Message::PasswordManager(PasswordManagerMessage::Error(format!(
"Failed to delete password {identifier}: {err}"
)));
}
match store::fetch_password_list().await {
Ok(list) => Message::PasswordManager(PasswordManagerMessage::ListRefreshed(list)),
Err(err) => Message::PasswordManager(PasswordManagerMessage::Error(format!(
"Failed to fetch password list: {err}"
))),
}
})
}
pub fn add_or_update_password_entry(&mut self) -> Task<cosmic::Action<Message>> {
if let Some(input_state) = &self.input_state
&& !input_state.input.identifier.is_empty()
{
let original = input_state.original.clone();
let identifier = input_state.input.identifier.clone();
let password = input_state.input.password.clone();
let expanded_identifier = input_state
.original
.as_ref()
.map(|i| i.identifier.clone())
.unwrap_or(String::new());
// Ensure we have a non-empty identifier
if identifier.is_empty() {
return Task::none();
}
// If the identifier have changed, we need to update
// the password list and expand the new id
if expanded_identifier != identifier {
self.expanded_entry = Some(identifier.clone());
if let Some(i) = self
.password_list
.iter()
.position(|s| s == &expanded_identifier)
{
self.password_list[i] = identifier.clone();
}
}
// Don't do anything if nothing have changed
if let Some(original) = &original {
if original == &input_state.input {
return Task::none();
}
}
cosmic::task::future(async move {
if let Err(err) = store::add_password(identifier.clone(), password.clone()).await {
Message::PasswordManager(PasswordManagerMessage::Error(format!(
"Failed to add password {identifier}: {err}"
)))
} else {
if let Some(original) = original {
if original.identifier != identifier {
if let Err(err) =
store::delete_password(original.identifier.clone()).await
{
return Message::PasswordManager(PasswordManagerMessage::Error(
format!(
"Failed to delete password {}: {err}",
original.identifier
),
));
}
}
}
Message::PasswordManager(PasswordManagerMessage::None)
}
})
} else {
Task::none()
}
}
pub fn context_page(&self, theme: &Theme) -> Element<'_, Message> {
let cosmic_theme::Spacing {
space_s,
space_xs,
space_xxs,
space_xxxs,
..
} = theme.cosmic().spacing;
let mut sections = Vec::with_capacity(2);
let mut passwords_section = widget::settings::section();
for password_id in &self.password_list {
let expanded = self.expanded_entry.as_ref() == Some(password_id);
passwords_section = passwords_section.add(
widget::settings::item::item_row(vec![
widget::button::text(password_id.clone())
.width(Length::Fixed(290.0))
.on_press(Message::PasswordManager(
PasswordManagerMessage::FetchAndPastePassword(password_id.clone()),
))
.into(),
widget::button::custom(icon_cache_get("edit-delete-symbolic", 16))
.on_press(Message::PasswordManager(PasswordManagerMessage::Delete(
password_id.clone(),
)))
.class(style::Button::Icon)
.into(),
if expanded {
widget::button::custom(icon_cache_get("go-up-symbolic", 16))
.on_press(Message::PasswordManager(PasswordManagerMessage::Collapse))
} else {
widget::button::custom(icon_cache_get("go-down-symbolic", 16)).on_press(
Message::PasswordManager(PasswordManagerMessage::FetchAndExpand(
password_id.clone(),
)),
)
}
.class(style::Button::Icon)
.into(),
])
.align_y(Alignment::Center)
.spacing(space_xxs),
);
if expanded {
if let Some(input_state) = &self.input_state {
let expanded_section: Section<'_, Message> = widget::settings::section().add(
widget::column::with_children(vec![
widget::column::with_children(vec![
widget::text(fl!("password-input-description")).into(),
widget::text_input("", input_state.input.identifier.clone())
.on_input(move |text| {
Message::PasswordManager(
PasswordManagerMessage::DescriptionInput(text),
)
})
.on_submit(move |text| {
Message::PasswordManager(
PasswordManagerMessage::DescriptionInputAndUpdate(text),
)
})
.on_unfocus(Message::PasswordManager(
PasswordManagerMessage::Update,
))
.into(),
])
.spacing(space_xxxs)
.into(),
widget::column::with_children(vec![
widget::text(fl!("password-input")).into(),
widget::secure_input(
"",
input_state.input.password.clone(),
Some(Message::PasswordManager(
PasswordManagerMessage::ToggleShowPassword,
)),
!input_state.show_password,
)
.on_input(move |text| {
Message::PasswordManager(PasswordManagerMessage::PasswordInput(
text,
))
})
.on_submit(move |text| {
Message::PasswordManager(
PasswordManagerMessage::PasswordInputAndUpdate(text),
)
})
.on_unfocus(Message::PasswordManager(
PasswordManagerMessage::Update,
))
.into(),
])
.spacing(space_xxxs)
.into(),
])
.padding([0, space_s])
.spacing(space_xs),
);
let padding = Padding {
top: 0.0,
bottom: 0.0,
left: space_s.into(),
right: space_s.into(),
};
passwords_section =
passwords_section.add(widget::container(expanded_section).padding(padding))
}
}
}
sections.push(passwords_section.into());
let add_password = widget::row::with_children(vec![
widget::horizontal_space().into(),
widget::button::standard(fl!("add-password"))
.on_press(Message::PasswordManager(PasswordManagerMessage::New))
.into(),
]);
sections.push(add_password.into());
widget::settings::view_column(sections).into()
}
pub fn new_password(&mut self) {
if !self.password_list.contains(&"".to_string()) {
self.password_list.push("".to_string());
}
self.input_state = Some(PasswordInputState {
original: None,
input: InputState {
identifier: Default::default(),
password: Default::default(),
},
show_password: true,
});
self.expanded_entry = Some("".to_string());
}
}
impl Default for PasswordManager {
fn default() -> Self {
Self::new()
}
}
mod store {
use std::string::FromUtf8Error;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
SecretService(#[from] secret_service::Error),
#[error(transparent)]
FromUtf8(#[from] FromUtf8Error),
#[error("No password found for identifier `{0}`")]
NoPasswordForIdentifier(String),
}
pub async fn fetch_password_list() -> Result<Vec<String>, Error> {
let mut list = Vec::new();
let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh).await?;
let collection = ss.get_default_collection().await?;
let mut attributes = std::collections::HashMap::new();
attributes.insert("application", "com.system76.CosmicTerm");
let search_items = collection.search_items(attributes).await?;
for item in search_items {
if let Some(identity) = item
.get_attributes()
.await
.ok()
.and_then(|attribs| attribs.get("identifier").cloned())
{
list.push(identity);
}
}
list.sort();
Ok(list)
}
pub async fn add_password(identifier: String, password: String) -> Result<(), Error> {
let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh).await?;
let collection = ss.get_default_collection().await?;
let mut attributes = std::collections::HashMap::new();
attributes.insert("application", "com.system76.CosmicTerm");
attributes.insert("identifier", &identifier);
let label = format!("CosmicTerm - {}", identifier);
collection
.create_item(&label, attributes, password.as_bytes(), true, "text/plain")
.await?;
Ok(())
}
pub async fn get_password(identifier: String) -> Result<secstr::SecUtf8, Error> {
let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh).await?;
let collection = ss.get_default_collection().await?;
let mut attributes = std::collections::HashMap::new();
attributes.insert("application", "com.system76.CosmicTerm");
attributes.insert("identifier", &identifier);
let search_items = collection.search_items(attributes).await?;
if let Some(item) = search_items.first() {
let secret = item.get_secret().await?;
Ok(String::from_utf8(secret)?.into())
} else {
Err(Error::NoPasswordForIdentifier(identifier))
}
}
pub async fn delete_password(identifier: String) -> Result<(), Error> {
let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh).await?;
let collection = ss.get_default_collection().await?;
let mut attributes = std::collections::HashMap::new();
attributes.insert("application", "com.system76.CosmicTerm");
attributes.insert("identifier", &identifier);
let search_items = collection.search_items(attributes).await?;
if let Some(item) = search_items.first() {
Ok(item.delete().await?)
} else {
Err(Error::NoPasswordForIdentifier(identifier))
}
}
}