diff --git a/.gitignore b/.gitignore index 39218fd..ab19753 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ *.rpm *.deb dist/pkgs/arch/*.tar.gz + +# Macos +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e53daf..612117d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) + - [0.3.1](#031) - [0.3.0](#030) - [0.2.0](#020) - [0.1.3](#013) @@ -10,6 +11,19 @@ --- +## 0.3.1 + +FIXME: Released on ??? + +- **Keyring to store secrets** + - On both MacOS and Windows, the secret used to encrypt passwords in bookmarks it is now store in the OS secret vault. This provides much more security to store the password +- Enhancements: + - Added connection timeout to 30 seconds to SFTP/SCP clients and improved name lookup system. +- Bugfix: + - Solved index in explorer files list which was no more kept after 0.3.0 + - SCP file transfer: fixed possible wrong file size when sending file, due to a possible incoherent size between the file explorer and the actual file size. +- Breaking changes: on **MacOS / Windows systems only**, the password you saved for bookmarks won't be working anymore if you have support for the keyring crate. Because of the migration to keyring, the previously used secret hasn't been migrated to the storage, instead a new secret will be used. To solve this, just save the bookmark again with the password. + ## 0.3.0 Released on 10/01/2021 diff --git a/Cargo.lock b/Cargo.lock index 3e30123..e3b522f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + [[package]] name = "aes-soft" version = "0.6.4" @@ -10,6 +21,16 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -205,16 +226,32 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.2", "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-foundation-sys" version = "0.8.2" @@ -272,6 +309,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-mac" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "dbus" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a0c10ea61042b7555729ab0608727bbbb06ce709c11e6047cfa4e10f6d052d" +dependencies = [ + "libc", +] + [[package]] name = "debug-helper" version = "0.3.10" @@ -423,6 +479,26 @@ dependencies = [ "wasi 0.10.0+wasi-snapshot-preview1", ] +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest 0.9.0", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -452,6 +528,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyring" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bcd64f48199f69993c705fd2f76882e53969db93bc6345021bc8bb6462a9ffa" +dependencies = [ + "byteorder", + "secret-service", + "security-framework 0.4.4", + "winapi", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -594,8 +682,8 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", - "security-framework-sys", + "security-framework 2.0.0", + "security-framework-sys 2.0.0", "tempfile", ] @@ -608,6 +696,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -618,6 +740,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -908,6 +1053,35 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "secret-service" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d752040301c251d653aa740dec847e95767ce312cfc469bee85eb13cbf81d8a" +dependencies = [ + "aes", + "block-modes", + "dbus", + "hkdf", + "lazy_static", + "num", + "rand 0.7.3", + "sha2", +] + +[[package]] +name = "security-framework" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +dependencies = [ + "bitflags", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "libc", + "security-framework-sys 0.4.3", +] + [[package]] name = "security-framework" version = "2.0.0" @@ -915,10 +1089,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" dependencies = [ "bitflags", - "core-foundation", - "core-foundation-sys", + "core-foundation 0.9.1", + "core-foundation-sys 0.8.2", + "libc", + "security-framework-sys 2.0.0", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +dependencies = [ + "core-foundation-sys 0.7.0", "libc", - "security-framework-sys", ] [[package]] @@ -927,7 +1111,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.2", "libc", ] @@ -1019,6 +1203,12 @@ dependencies = [ "parking_lot 0.10.2", ] +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + [[package]] name = "syn" version = "1.0.58" @@ -1046,7 +1236,7 @@ dependencies = [ [[package]] name = "termscp" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bitflags", "bytesize", @@ -1058,6 +1248,7 @@ dependencies = [ "ftp4", "getopts", "hostname", + "keyring", "lazy_static", "magic-crypt", "rand 0.8.1", diff --git a/Cargo.toml b/Cargo.toml index 37cb4ec..3182414 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "termscp" -version = "0.3.0" +version = "0.3.1" authors = ["Christian Visintin"] edition = "2018" license = "GPL-3.0" @@ -43,6 +43,9 @@ whoami = "1.0.1" [target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies] users = "0.11.0" +[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] +keyring = "0.10.1" + [[bin]] name = "termscp" path = "src/main.rs" diff --git a/README.md b/README.md index c41286c..1bf9150 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # TermSCP -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.1-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp) [![Build](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![codecov](https://codecov.io/gh/veeso/termscp/branch/main/graph/badge.svg?token=au67l7nQah)](https://codecov.io/gh/veeso/termscp) ~ Basically, WinSCP on a terminal ~ Developed by Christian Visintin -Current version: 0.3.0 (10/01/2021) +FIXME: Current version: 0.3.1 (17/01/2021) --- @@ -93,8 +93,8 @@ Requirements: ### Deb package 📦 -Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.0_amd64.deb) -or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.0_amd64.deb` +Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb) +or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb` then install through dpkg: @@ -106,8 +106,8 @@ gdebi termscp_*.deb ### RPM package 📦 -Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.0-1.x86_64.rpm) -or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.0-1.x86_64.rpm` +Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm) +or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm` then install through rpm: @@ -133,7 +133,7 @@ Start PowerShell as administrator and run choco install termscp ``` -Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.0.nupkg) +Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.1.nupkg) and then with PowerShell started with administrator previleges, run: @@ -233,7 +233,12 @@ If you go to [gallery](#gallery-), there is a GIF showing how bookmarks work ### Are my passwords Safe 😈 Well, kinda. -As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Well, no, the key used to encrypt your passwords is generated at the first launch of termscp and stored on your drive. So it's still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉. +As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Well, depends on your operating system: + +On Windows and MacOS the passwords are (if possible, but should be) in respectively the Windows Vault and the Keychain. This is actually super-safe and is directly managed by your operating system. + +On Linux and BSD, on the other hand the key used to encrypt your passwords is stored on your drive. So it's still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉. +Actually [keyring-rs](https://github.com/hwchen/keyring-rs), supports Linux, but for different reasons I preferred not to make it available for this configuration. If you want to read more about my decision read [this issue](https://github.com/veeso/termscp/issues/2), while if you think this might have been implemented differently feel free to open an issue with your proposal. --- @@ -357,6 +362,7 @@ TermSCP is powered by these aweseome projects: - [bytesize](https://github.com/hyunsik/bytesize) - [crossterm](https://github.com/crossterm-rs/crossterm) - [edit](https://github.com/milkey-mouse/edit) +- [keyring-rs](https://github.com/hwchen/keyring-rs) - [rpassword](https://github.com/conradkleinespel/rpassword) - [rust-ftp](https://github.com/mattnenterprise/rust-ftp) - [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) diff --git a/src/activity_manager.rs b/src/activity_manager.rs index b8964a0..130381e 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -27,7 +27,7 @@ use std::path::PathBuf; // Deps use crate::filetransfer::FileTransferProtocol; -use crate::host::Localhost; +use crate::host::{HostError, Localhost}; use crate::ui::activities::{ auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity, filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity, @@ -60,11 +60,11 @@ impl ActivityManager { /// ### new /// /// Initializes a new Activity Manager - pub fn new(local_dir: &PathBuf, interval: Duration) -> Result { + pub fn new(local_dir: &PathBuf, interval: Duration) -> Result { // Prepare Context let host: Localhost = match Localhost::new(local_dir.clone()) { Ok(h) => h, - Err(_) => return Err(()), + Err(e) => return Err(e), }; let ctx: Context = Context::new(host); Ok(ActivityManager { diff --git a/src/filetransfer/ftp_transfer.rs b/src/filetransfer/ftp_transfer.rs index ad01f24..c3c4964 100644 --- a/src/filetransfer/ftp_transfer.rs +++ b/src/filetransfer/ftp_transfer.rs @@ -150,10 +150,12 @@ impl FtpFileTransfer { Err(_) => None, }; // Get filesize - let filesize: usize = match metadata.get(6).unwrap().as_str().parse::() { - Ok(sz) => sz, - Err(_) => 0, - }; + let filesize: usize = metadata + .get(6) + .unwrap() + .as_str() + .parse::() + .unwrap_or(0); let file_name: String = String::from(metadata.get(8).unwrap().as_str()); // Check if file_name is '.' or '..' if file_name.as_str() == "." || file_name.as_str() == ".." { @@ -240,10 +242,7 @@ impl FtpFileTransfer { true => 0, // If is directory, filesize is 0 false => match metadata.get(3) { // If is file, parse arg 3 - Some(val) => match val.as_str().parse::() { - Ok(sz) => sz, - Err(_) => 0, - }, + Some(val) => val.as_str().parse::().unwrap_or(0), None => 0, // Should not happen }, }; diff --git a/src/filetransfer/scp_transfer.rs b/src/filetransfer/scp_transfer.rs index 1ada5bf..b4d6066 100644 --- a/src/filetransfer/scp_transfer.rs +++ b/src/filetransfer/scp_transfer.rs @@ -36,13 +36,11 @@ use crate::utils::parser::parse_lstime; // Includes use regex::Regex; use ssh2::{Channel, Session}; -use std::net::TcpStream; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; +use std::ops::Range; use std::path::{Path, PathBuf}; -use std::time::SystemTime; -use std::{ - io::{BufReader, BufWriter, Read, Write}, - ops::Range, -}; +use std::time::{Duration, SystemTime}; /// ## ScpFileTransfer /// @@ -137,10 +135,7 @@ impl ScpFileTransfer { Err(_) => None, }; // Get filesize - let filesize: usize = match metadata.get(6).unwrap().as_str().parse::() { - Ok(sz) => sz, - Err(_) => 0, - }; + let filesize: usize = metadata.get(6).unwrap().as_str().parse::().unwrap_or(0); // Get link and name let (file_name, symlink_path): (String, Option) = match is_symlink { true => self.get_name_and_link(metadata.get(8).unwrap().as_str()), @@ -281,12 +276,34 @@ impl FileTransfer for ScpFileTransfer { password: Option, ) -> Result, FileTransferError> { // Setup tcp stream - let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) { - Ok(stream) => stream, - Err(err) => { + let socket_addresses: Vec = + match format!("{}:{}", address, port).to_socket_addrs() { + Ok(s) => s.collect(), + Err(err) => { + return Err(FileTransferError::new_ex( + FileTransferErrorType::BadAddress, + format!("{}", err), + )) + } + }; + let mut tcp: Option = None; + // Try addresses + for socket_addr in socket_addresses.iter() { + match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) { + Ok(stream) => { + tcp = Some(stream); + break; + } + Err(_) => continue, + } + } + // If stream is None, return connection timeout + let tcp: TcpStream = match tcp { + Some(t) => t, + None => { return Err(FileTransferError::new_ex( - FileTransferErrorType::BadAddress, - format!("{}", err), + FileTransferErrorType::ConnectionError, + String::from("Connection timeout"), )) } }; @@ -741,7 +758,13 @@ impl FileTransfer for ScpFileTransfer { }; (mtime, atime) }; - match session.scp_send(file_name, mode, local.size as u64, Some(times)) { + // We need to get the size of local; NOTE: don't use the `size` attribute, since might be out of sync + let file_size: u64 = match std::fs::metadata(local.abs_path.as_path()) { + Ok(metadata) => metadata.len(), + Err(_) => local.size as u64, // NOTE: fallback to fsentry size + }; + // Send file + match session.scp_send(file_name, mode, file_size, Some(times)) { Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ProtocolError, diff --git a/src/filetransfer/sftp_transfer.rs b/src/filetransfer/sftp_transfer.rs index 9ef2d8c..c7ce56d 100644 --- a/src/filetransfer/sftp_transfer.rs +++ b/src/filetransfer/sftp_transfer.rs @@ -34,7 +34,7 @@ use crate::system::sshkey_storage::SshKeyStorage; // Includes use ssh2::{FileStat, OpenFlags, OpenType, Session, Sftp}; use std::io::{BufReader, BufWriter, Read, Write}; -use std::net::TcpStream; +use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; @@ -203,12 +203,34 @@ impl FileTransfer for SftpFileTransfer { password: Option, ) -> Result, FileTransferError> { // Setup tcp stream - let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) { - Ok(stream) => stream, - Err(err) => { + let socket_addresses: Vec = + match format!("{}:{}", address, port).to_socket_addrs() { + Ok(s) => s.collect(), + Err(err) => { + return Err(FileTransferError::new_ex( + FileTransferErrorType::BadAddress, + format!("{}", err), + )) + } + }; + let mut tcp: Option = None; + // Try addresses + for socket_addr in socket_addresses.iter() { + match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) { + Ok(stream) => { + tcp = Some(stream); + break; + } + Err(_) => continue, + } + } + // If stream is None, return connection timeout + let tcp: TcpStream = match tcp { + Some(t) => t, + None => { return Err(FileTransferError::new_ex( - FileTransferErrorType::BadAddress, - format!("{}", err), + FileTransferErrorType::ConnectionError, + String::from("Connection timeout"), )) } }; diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs index 4a0a4fc..ffc8ea3 100644 --- a/src/fs/explorer/mod.rs +++ b/src/fs/explorer/mod.rs @@ -30,6 +30,7 @@ extern crate bitflags; // Locals use super::FsEntry; // Ext +use std::cmp::Reverse; use std::collections::VecDeque; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -137,7 +138,7 @@ impl FileExplorer { /// /// Iterate over files /// Filters are applied based on current options (e.g. hidden files not returned) - pub fn iter_files(&self) -> Box + '_> { + pub fn iter_files(&self) -> impl Iterator + '_ { // Filter let opts: ExplorerOpts = self.opts; Box::new(self.files.iter().filter(move |x| { @@ -154,7 +155,7 @@ impl FileExplorer { /// ### iter_files_all /// /// Iterate all files; doesn't care about options - pub fn iter_files_all(&self) -> Box + '_> { + pub fn iter_files_all(&self) -> impl Iterator + '_ { Box::new(self.files.iter()) } @@ -238,16 +239,14 @@ impl FileExplorer { /// /// Sort files by creation time; the newest comes first fn sort_files_by_creation_time(&mut self) { - self.files - .sort_by(|a: &FsEntry, b: &FsEntry| b.get_creation_time().cmp(&a.get_creation_time())); + self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time())); } /// ### sort_files_by_size /// /// Sort files by size fn sort_files_by_size(&mut self) { - self.files - .sort_by(|a: &FsEntry, b: &FsEntry| b.get_size().cmp(&a.get_size())); + self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_size())); } /// ### sort_files_directories_first @@ -432,6 +431,19 @@ impl FileExplorer { } } + /// ### set_abs_index + /// + /// Set absolute index + pub fn set_abs_index(&mut self, idx: usize) { + self.index = match idx >= self.files.len() { + true => match self.files.len() { + 0 => 0, + _ => self.files.len() - 1, + }, + false => idx, + }; + } + /// ### toggle_hidden_files /// /// Enable/disable hidden files @@ -726,6 +738,33 @@ mod tests { assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); } + #[test] + fn test_fs_explorer_set_abs_index() { + let mut explorer: FileExplorer = FileExplorer::default(); + explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES); + // Create files (files are then sorted by name DEFAULT) + explorer.set_files(vec![ + make_fs_entry("README.md", false), + make_fs_entry("src/", true), + make_fs_entry(".git/", true), + make_fs_entry("CONTRIBUTING.md", false), + make_fs_entry("CODE_OF_CONDUCT.md", false), + make_fs_entry("CHANGELOG.md", false), + make_fs_entry("LICENSE", false), + make_fs_entry("Cargo.toml", false), + make_fs_entry("Cargo.lock", false), + make_fs_entry("codecov.yml", false), + make_fs_entry(".gitignore", false), + ]); + explorer.set_abs_index(3); + assert_eq!(explorer.get_index(), 3); + explorer.set_abs_index(12); + assert_eq!(explorer.get_index(), 10); + explorer.set_files(vec![]); + explorer.set_abs_index(12); + assert_eq!(explorer.get_index(), 0); + } + #[test] fn test_fs_explorer_sort_by_creation_time() { let mut explorer: FileExplorer = FileExplorer::default(); diff --git a/src/main.rs b/src/main.rs index 52de229..76d4580 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,8 +167,8 @@ fn main() { // Create activity manager (and context too) let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) { Ok(m) => m, - Err(_) => { - eprintln!("Invalid directory '{}'", wrkdir.display()); + Err(err) => { + eprintln!("Could not start activity manager: {}", err); std::process::exit(255); } }; diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index c65fbea..3b7aac9 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -23,6 +23,12 @@ * */ +// Deps +extern crate whoami; +// Crate +#[cfg(any(target_os = "windows", target_os = "macos"))] +use super::keys::keyringstorage::KeyringStorage; +use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError}; // Local use crate::bookmarks::serializer::BookmarkSerializer; use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts}; @@ -31,8 +37,7 @@ use crate::utils::crypto; use crate::utils::fmt::fmt_time; use crate::utils::random::random_alphanumeric_with_len; // Ext -use std::fs::{OpenOptions, Permissions}; -use std::io::{Read, Write}; +use std::fs::OpenOptions; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::string::ToString; @@ -53,23 +58,60 @@ impl BookmarksClient { /// /// Instantiates a new BookmarksClient /// Bookmarks file path must be provided - /// Key file must be provided + /// Storage path for file provider must be provided pub fn new( bookmarks_file: &Path, - key_file: &Path, + storage_path: &Path, recents_size: usize, ) -> Result { // Create default hosts let default_hosts: UserHosts = Default::default(); - // If key file doesn't exist, create key, otherwise read it - let key: String = match key_file.exists() { - true => match BookmarksClient::load_key(key_file) { - Ok(key) => key, - Err(err) => return Err(err), - }, - false => match BookmarksClient::generate_key(key_file) { - Ok(key) => key, - Err(err) => return Err(err), + // Make a key storage (windows / macos) + #[cfg(any(target_os = "windows", target_os = "macos"))] + let (key_storage, service_id): (Box, &str) = { + let username: String = whoami::username(); + let storage: KeyringStorage = KeyringStorage::new(username.as_str()); + // Check if keyring storage is supported + #[cfg(not(test))] + let app_name: &str = "termscp"; + #[cfg(test)] // NOTE: when running test, add -test + let app_name: &str = "termscp-test"; + match storage.is_supported() { + true => (Box::new(storage), app_name), + false => (Box::new(FileStorage::new(storage_path)), "bookmarks"), + } + }; + // Make a key storage (linux / unix) + #[cfg(any(target_os = "linux", target_os = "unix"))] + let (key_storage, service_id): (Box, &str) = { + #[cfg(not(test))] + let app_name: &str = "bookmarks"; + #[cfg(test)] // NOTE: when running test, add -test + let app_name: &str = "bookmarks-test"; + (Box::new(FileStorage::new(storage_path)), app_name) + }; + // Load key + let key: String = match key_storage.get_key(service_id) { + Ok(k) => k, + Err(e) => match e { + KeyStorageError::NoSuchKey => { + // If no such key, generate key and set it into the storage + let key: String = Self::generate_key(); + if let Err(e) = key_storage.set_key(service_id, key.as_str()) { + return Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + format!("Could not write key to storage: {}", e), + )); + } + // Return key + key + } + _ => { + return Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + format!("Could not get key from storage: {}", e), + )) + } }, }; let mut client: BookmarksClient = BookmarksClient { @@ -96,7 +138,7 @@ impl BookmarksClient { /// ### iter_bookmarks /// /// Iterate over bookmarks keys - pub fn iter_bookmarks(&self) -> Box + '_> { + pub fn iter_bookmarks(&self) -> impl Iterator + '_ { Box::new(self.hosts.bookmarks.keys()) } @@ -156,7 +198,7 @@ impl BookmarksClient { /// ### iter_recents /// /// Iterate over recents keys - pub fn iter_recents(&self) -> Box + '_> { + pub fn iter_recents(&self) -> impl Iterator + '_ { Box::new(self.hosts.recents.keys()) } @@ -276,36 +318,10 @@ impl BookmarksClient { /// ### generate_key /// - /// Generate a new AES key and write it to key file - fn generate_key(key_file: &Path) -> Result { + /// Generate a new AES key + fn generate_key() -> String { // Generate 256 bytes (2048 bits) key - let key: String = random_alphanumeric_with_len(256); - // Write file - match OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(key_file) - { - Ok(mut file) => { - // Write key to file - if let Err(err) = file.write_all(key.as_bytes()) { - return Err(SerializerError::new_ex( - SerializerErrorKind::IoError, - err.to_string(), - )); - } - // Set file to readonly - let mut permissions: Permissions = file.metadata().unwrap().permissions(); - permissions.set_readonly(true); - let _ = file.set_permissions(permissions); - Ok(key) - } - Err(err) => Err(SerializerError::new_ex( - SerializerErrorKind::IoError, - err.to_string(), - )), - } + random_alphanumeric_with_len(256) } /// ### make_bookmark @@ -331,28 +347,6 @@ impl BookmarksClient { } } - /// ### load_key - /// - /// Load key from key_file - fn load_key(key_file: &Path) -> Result { - match OpenOptions::new().read(true).open(key_file) { - Ok(mut file) => { - let mut key: String = String::with_capacity(256); - match file.read_to_string(&mut key) { - Ok(_) => Ok(key), - Err(err) => Err(SerializerError::new_ex( - SerializerErrorKind::IoError, - err.to_string(), - )), - } - } - Err(err) => Err(SerializerError::new_ex( - SerializerErrorKind::IoError, - err.to_string(), - )), - } - } - /// ### encrypt_str /// /// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64 @@ -375,6 +369,7 @@ impl BookmarksClient { } #[cfg(test)] +#[cfg(not(target_os = "macos"))] // CI/CD blocks mod tests { use super::*; @@ -382,6 +377,7 @@ mod tests { use std::time::Duration; #[test] + fn test_system_bookmarks_new() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -397,6 +393,7 @@ mod tests { } #[test] + #[cfg(any(target_os = "unix", target_os = "linux"))] fn test_system_bookmarks_new_err() { assert!(BookmarksClient::new( Path::new("/tmp/oifoif/omar"), @@ -413,6 +410,7 @@ mod tests { } #[test] + fn test_system_bookmarks_new_from_existing() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -458,6 +456,7 @@ mod tests { } #[test] + fn test_system_bookmarks_manipulate_bookmarks() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -503,6 +502,7 @@ mod tests { #[test] #[should_panic] + fn test_system_bookmarks_bad_bookmark_name() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -521,6 +521,7 @@ mod tests { } #[test] + fn test_system_bookmarks_manipulate_recents() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -555,6 +556,7 @@ mod tests { } #[test] + fn test_system_bookmarks_dup_recent() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -579,6 +581,7 @@ mod tests { } #[test] + fn test_system_bookmarks_recents_more_than_limit() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -626,6 +629,7 @@ mod tests { #[test] #[should_panic] + fn test_system_bookmarks_add_bookmark_empty() { let tmp_dir: tempfile::TempDir = create_tmp_dir(); let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); @@ -646,10 +650,10 @@ mod tests { /// ### get_paths /// /// Get paths for configuration and key for bookmarks + fn get_paths(dir: &Path) -> (PathBuf, PathBuf) { - let mut k: PathBuf = PathBuf::from(dir); + let k: PathBuf = PathBuf::from(dir); let mut c: PathBuf = k.clone(); - k.push("bookmarks.key"); c.push("bookmarks.toml"); (c, k) } @@ -657,6 +661,7 @@ mod tests { /// ### create_tmp_dir /// /// Create temporary directory + fn create_tmp_dir() -> tempfile::TempDir { tempfile::TempDir::new().ok().unwrap() } diff --git a/src/system/config_client.rs b/src/system/config_client.rs index 3bb7174..50a1061 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -242,7 +242,7 @@ impl ConfigClient { /// ### iter_ssh_keys /// /// Get an iterator through hosts in the ssh key storage - pub fn iter_ssh_keys(&self) -> Box + '_> { + pub fn iter_ssh_keys(&self) -> impl Iterator + '_ { Box::new(self.config.remote.ssh_keys.keys()) } diff --git a/src/system/environment.rs b/src/system/environment.rs index 0b994ae..b9c7973 100644 --- a/src/system/environment.rs +++ b/src/system/environment.rs @@ -59,14 +59,12 @@ pub fn init_config_dir() -> Result, String> { /// ### get_bookmarks_paths /// /// Get paths for bookmarks client -/// Returns: path of bookmarks.toml and path of key -pub fn get_bookmarks_paths(config_dir: &Path) -> (PathBuf, PathBuf) { +/// Returns: path of bookmarks.toml +pub fn get_bookmarks_paths(config_dir: &Path) -> PathBuf { // Prepare paths let mut bookmarks_file: PathBuf = PathBuf::from(config_dir); bookmarks_file.push("bookmarks.toml"); - let mut key_file: PathBuf = PathBuf::from(config_dir); - key_file.push(".bookmarks.key"); // key file is hidden - (bookmarks_file, key_file) + bookmarks_file } /// ### get_config_paths @@ -123,10 +121,7 @@ mod tests { fn test_system_environment_get_bookmarks_paths() { assert_eq!( get_bookmarks_paths(&Path::new("/home/omar/.config/termscp/")), - ( - PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"), - PathBuf::from("/home/omar/.config/termscp/.bookmarks.key") - ) + PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"), ); } diff --git a/src/system/keys/filestorage.rs b/src/system/keys/filestorage.rs new file mode 100644 index 0000000..fe9e921 --- /dev/null +++ b/src/system/keys/filestorage.rs @@ -0,0 +1,163 @@ +//! ## FileStorage +//! +//! `filestorage` provides an implementation of the `KeyStorage` trait using a file + +/* +* +* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Local +use super::{KeyStorage, KeyStorageError}; +// Ext +use std::fs::{OpenOptions, Permissions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +/// ## FileStorage +/// +/// File storage is an implementation o the `KeyStorage` which uses a file to store the key +pub struct FileStorage { + dir_path: PathBuf, +} + +impl FileStorage { + /// ### new + /// + /// Instantiates a new `FileStorage` + pub fn new(dir_path: &Path) -> Self { + FileStorage { + dir_path: PathBuf::from(dir_path), + } + } + + /// ### make_file_path + /// + /// Make file path for key file from `dir_path` and the application id + fn make_file_path(&self, storage_id: &str) -> PathBuf { + let mut p: PathBuf = self.dir_path.clone(); + let file_name = format!(".{}.key", storage_id); + p.push(file_name); + p + } +} + +impl KeyStorage for FileStorage { + /// ### get_key + /// + /// Retrieve key from the key storage. + /// The key might be acccess through an identifier, which identifies + /// the key in the storage + fn get_key(&self, storage_id: &str) -> Result { + let key_file: PathBuf = self.make_file_path(storage_id); + // Check if file exists + if !key_file.exists() { + return Err(KeyStorageError::NoSuchKey); + } + // Read key from file + match OpenOptions::new().read(true).open(key_file.as_path()) { + Ok(mut file) => { + let mut key: String = String::new(); + match file.read_to_string(&mut key) { + Ok(_) => Ok(key), + Err(_) => Err(KeyStorageError::ProviderError), + } + } + Err(_) => Err(KeyStorageError::ProviderError), + } + } + + /// ### set_key + /// + /// Set the key into the key storage + fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> { + let key_file: PathBuf = self.make_file_path(storage_id); + // Write key + match OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(key_file.as_path()) + { + Ok(mut file) => { + // Write key to file + if file.write_all(key.as_bytes()).is_err() { + return Err(KeyStorageError::ProviderError); + } + // Set file to readonly + let mut permissions: Permissions = file.metadata().unwrap().permissions(); + permissions.set_readonly(true); + let _ = file.set_permissions(permissions); + Ok(()) + } + Err(_) => Err(KeyStorageError::ProviderError), + } + } + + /// is_supported + /// + /// Returns whether the key storage is supported on the host system + fn is_supported(&self) -> bool { + true + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_system_keys_filestorage_make_dir() { + let storage: FileStorage = FileStorage::new(&Path::new("/tmp/")); + assert_eq!( + storage.make_file_path("bookmarks").as_path(), + Path::new("/tmp/.bookmarks.key") + ); + } + + #[test] + fn test_system_keys_filestorage_ok() { + let key_dir: tempfile::TempDir = + tempfile::TempDir::new().expect("Could not create tempdir"); + let storage: FileStorage = FileStorage::new(key_dir.path()); + // Supported + assert!(storage.is_supported()); + let app_name: &str = "termscp"; + let secret: &str = "Th15-15/My-Супер-Секрет"; + // Secret should not exist + assert_eq!( + storage.get_key(app_name).err().unwrap(), + KeyStorageError::NoSuchKey + ); + // Write secret + assert!(storage.set_key(app_name, secret).is_ok()); + // Get secret + assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret); + } + + #[test] + fn test_system_keys_filestorage_err() { + let bad_dir: &Path = Path::new("/piro/poro/pero/"); + let storage: FileStorage = FileStorage::new(bad_dir); + let app_name: &str = "termscp"; + let secret: &str = "Th15-15/My-Супер-Секрет"; + assert!(storage.set_key(app_name, secret).is_err()); + } +} diff --git a/src/system/keys/keyringstorage.rs b/src/system/keys/keyringstorage.rs new file mode 100644 index 0000000..0e3662d --- /dev/null +++ b/src/system/keys/keyringstorage.rs @@ -0,0 +1,129 @@ +//! ## KeyringStorage +//! +//! `keyringstorage` provides an implementation of the `KeyStorage` trait using the OS keyring + +/* +* +* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Deps +extern crate keyring; +// Local +use super::{KeyStorage, KeyStorageError}; +// Ext +use keyring::{Keyring, KeyringError}; + +/// ## KeyringStorage +/// +/// provides a `KeyStorage` implementation using the keyring crate +pub struct KeyringStorage { + username: String, +} + +impl KeyringStorage { + /// ### new + /// + /// Instantiates a new KeyringStorage + pub fn new(username: &str) -> Self { + KeyringStorage { + username: username.to_string(), + } + } +} + +impl KeyStorage for KeyringStorage { + /// ### get_key + /// + /// Retrieve key from the key storage. + /// The key might be acccess through an identifier, which identifies + /// the key in the storage + fn get_key(&self, storage_id: &str) -> Result { + let storage: Keyring = Keyring::new(storage_id, self.username.as_str()); + match storage.get_password() { + Ok(s) => Ok(s), + Err(e) => match e { + KeyringError::NoPasswordFound => Err(KeyStorageError::NoSuchKey), + #[cfg(target_os = "windows")] + KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey), + #[cfg(target_os = "macos")] + KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey), + _ => panic!("{}", e), + }, + } + } + + /// ### set_key + /// + /// Set the key into the key storage + fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> { + let storage: Keyring = Keyring::new(storage_id, self.username.as_str()); + match storage.set_password(key) { + Ok(_) => Ok(()), + Err(_) => Err(KeyStorageError::ProviderError), + } + } + + /// is_supported + /// + /// Returns whether the key storage is supported on the host system + fn is_supported(&self) -> bool { + let dummy: String = String::from("dummy-service"); + let storage: Keyring = Keyring::new(dummy.as_str(), self.username.as_str()); + // Check what kind of error is returned + match storage.get_password() { + Ok(_) => true, + Err(err) => !matches!(err, KeyringError::NoBackendFound), + } + } +} + +#[cfg(test)] +mod tests { + + extern crate whoami; + use super::*; + + use whoami::username; + + #[test] + fn test_system_keys_keyringstorage() { + let username: String = username(); + let storage: KeyringStorage = KeyringStorage::new(username.as_str()); + assert!(storage.is_supported()); + let app_name: &str = "termscp-test2"; + let secret: &str = "Th15-15/My-Супер-Секрет"; + let kring: Keyring = Keyring::new(app_name, username.as_str()); + let _ = kring.delete_password(); + drop(kring); + // Secret should not exist + assert_eq!( + storage.get_key(app_name).err().unwrap(), + KeyStorageError::NoSuchKey + ); + // Write secret + assert!(storage.set_key(app_name, secret).is_ok()); + // Get secret + assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret); + + // Delete the key manually... + let kring: Keyring = Keyring::new(app_name, username.as_str()); + assert!(kring.delete_password().is_ok()); + } +} diff --git a/src/system/keys/mod.rs b/src/system/keys/mod.rs new file mode 100644 index 0000000..02c66b7 --- /dev/null +++ b/src/system/keys/mod.rs @@ -0,0 +1,90 @@ +//! ## KeyStorage +//! +//! `keystorage` provides the trait to manipulate to a KeyStorage + +/* +* +* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Storages +pub mod filestorage; +#[cfg(any(target_os = "windows", target_os = "macos"))] +pub mod keyringstorage; + +/// ## KeyStorageError +/// +/// defines the error type for the `KeyStorage` +#[derive(PartialEq, std::fmt::Debug)] +pub enum KeyStorageError { + //BadKey, + ProviderError, + NoSuchKey, +} + +impl std::fmt::Display for KeyStorageError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let err: String = String::from(match &self { + //KeyStorageError::BadKey => "Bad key syntax", + KeyStorageError::ProviderError => "Provider service error", + KeyStorageError::NoSuchKey => "No such key", + }); + write!(f, "{}", err) + } +} + +/// ## KeyStorage +/// +/// this traits provides the methods to communicate and interact with the key storage. +pub trait KeyStorage { + /// ### get_key + /// + /// Retrieve key from the key storage. + /// The key might be acccess through an identifier, which identifies + /// the key in the storage + fn get_key(&self, storage_id: &str) -> Result; + + /// ### set_key + /// + /// Set the key into the key storage + fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError>; + + /// is_supported + /// + /// Returns whether the key storage is supported on the host system + fn is_supported(&self) -> bool; +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_system_keys_mod_errors() { + assert_eq!( + format!("{}", KeyStorageError::ProviderError), + String::from("Provider service error") + ); + assert_eq!( + format!("{}", KeyStorageError::NoSuchKey), + String::from("No such key") + ); + } +} diff --git a/src/system/mod.rs b/src/system/mod.rs index 2e2e624..371d408 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -27,4 +27,5 @@ pub mod bookmarks_client; pub mod config_client; pub mod environment; +pub(crate) mod keys; pub mod sshkey_storage; diff --git a/src/ui/activities/auth_activity/bookmarks.rs b/src/ui/activities/auth_activity/bookmarks.rs index f9f553d..0e2f99c 100644 --- a/src/ui/activities/auth_activity/bookmarks.rs +++ b/src/ui/activities/auth_activity/bookmarks.rs @@ -224,11 +224,15 @@ impl AuthActivity { match environment::init_config_dir() { Ok(path) => { // If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system. - if let Some(path) = path { - let (bookmarks_file, key_file): (PathBuf, PathBuf) = - environment::get_bookmarks_paths(path.as_path()); + if let Some(config_dir_path) = path { + let bookmarks_file: PathBuf = + environment::get_bookmarks_paths(config_dir_path.as_path()); // Initialize client - match BookmarksClient::new(bookmarks_file.as_path(), key_file.as_path(), 16) { + match BookmarksClient::new( + bookmarks_file.as_path(), + config_dir_path.as_path(), + 16, + ) { Ok(cli) => self.bookmarks_client = Some(cli), Err(err) => { self.popup = Some(Popup::Alert( @@ -236,7 +240,7 @@ impl AuthActivity { format!( "Could not initialize bookmarks (at \"{}\", \"{}\"): {}", bookmarks_file.display(), - key_file.display(), + config_dir_path.display(), err ), )) diff --git a/src/ui/activities/filetransfer_activity/session.rs b/src/ui/activities/filetransfer_activity/session.rs index 9d53ac1..ad702a1 100644 --- a/src/ui/activities/filetransfer_activity/session.rs +++ b/src/ui/activities/filetransfer_activity/session.rs @@ -600,9 +600,12 @@ impl FileTransferActivity { match self.context.as_ref().unwrap().local.scan_dir(path) { Ok(files) => { // Set files and sort (sorting is implicit) + let prev_index: usize = self.local.get_index(); self.local.set_files(files); + // Restore index + self.local.set_abs_index(prev_index); // Set index; keep if possible, otherwise set to last item - self.local.set_index(match self.local.get_current_file() { + self.local.set_abs_index(match self.local.get_current_file() { Some(_) => self.local.get_index(), None => match self.local.count() { 0 => 0, @@ -626,9 +629,12 @@ impl FileTransferActivity { match self.client.list_dir(path) { Ok(files) => { // Set files and sort (sorting is implicit) + let prev_index: usize = self.remote.get_index(); self.remote.set_files(files); + // Restore index + self.remote.set_abs_index(prev_index); // Set index; keep if possible, otherwise set to last item - self.remote.set_index(match self.remote.get_current_file() { + self.remote.set_abs_index(match self.remote.get_current_file() { Some(_) => self.remote.get_index(), None => match self.remote.count() { 0 => 0,