Implement a simple password manager
This commit is contained in:
parent
10fd396ea3
commit
71b9fb5226
8 changed files with 720 additions and 27 deletions
93
Cargo.lock
generated
93
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
57
src/main.rs
57
src/main.rs
|
|
@ -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")),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
24
src/menu.rs
24
src/menu.rs
|
|
@ -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
509
src/password_manager.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue