diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 02c53aa..d3e534f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -68,10 +68,12 @@ checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" name = "app" version = "0.1.0" dependencies = [ + "netstat2", "serde", "serde_json", "sodiumoxide", "sqlx", + "sysinfo", "tauri", "tauri-build", "tokio", @@ -433,6 +435,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.7.1", + "scopeguard", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -697,7 +723,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ - "memoffset", + "memoffset 0.6.5", "rustc_version 0.3.3", ] @@ -1623,6 +1649,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1696,6 +1731,20 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "netstat2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0faa3f4ad230fd2bf2a5dad71476ecbaeaed904b3c7e7e5b1f266c415c03761f" +dependencies = [ + "bitflags", + "byteorder", + "libc", + "num-derive", + "num-traits", + "thiserror", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -1729,6 +1778,26 @@ dependencies = [ "winrt-notification", ] +[[package]] +name = "ntapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2372,6 +2441,29 @@ dependencies = [ "cty", ] +[[package]] +name = "rayon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +dependencies = [ + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3060,6 +3152,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ddf41e393a9133c81d5f0974195366bd57082deac6e0eb02ed39b8341c2bb6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + [[package]] name = "system-deps" version = "5.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6c55425..c792a56 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,8 @@ tauri = { version = "1.0.5", features = ["api-all"] } sodiumoxide = "0.2.7" tokio = { version = ">=1.19", features = ["full"] } sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] } +netstat2 = "0.9.1" +sysinfo = "0.26.8" [features] # by default Tauri runs in production mode diff --git a/src-tauri/migrations/20221201002355_initial.sql b/src-tauri/migrations/20221201002355_initial.sql index b4e225d..c0a870f 100644 --- a/src-tauri/migrations/20221201002355_initial.sql +++ b/src-tauri/migrations/20221201002355_initial.sql @@ -1,9 +1,9 @@ -- Add migration script here CREATE TABLE credentials ( - access_key_id TEXT, - secret_key BLOB, -- encrypted - nonce BLOB, - expires INTEGER + access_key_id TEXT NOT NULL, + secret_key_enc BLOB NOT NULL, + salt BLOB NOT NULL, + nonce BLOB NOT NULL ); CREATE TABLE config ( diff --git a/src-tauri/src/clientinfo.rs b/src-tauri/src/clientinfo.rs new file mode 100644 index 0000000..f3ecc06 --- /dev/null +++ b/src-tauri/src/clientinfo.rs @@ -0,0 +1,46 @@ +use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo}; +use sysinfo::{System, SystemExt, Pid, ProcessExt}; + +use crate::errors::*; + + +fn get_associated_pids(local_port: u16) -> Result, netstat2::error::Error> { + let mut it = netstat2::iterate_sockets_info( + AddressFamilyFlags::IPV4, + ProtocolFlags::TCP + )?; + + for (i, item) in it.enumerate() { + let sock_info = item?; + let proto_info = match sock_info.protocol_socket_info { + ProtocolSocketInfo::Tcp(tcp_info) => tcp_info, + ProtocolSocketInfo::Udp(_) => {continue;} + }; + + if proto_info.local_port == local_port + && proto_info.remote_port == 12345 + && proto_info.local_addr == std::net::Ipv4Addr::LOCALHOST + && proto_info.remote_addr == std::net::Ipv4Addr::LOCALHOST + { + println!("PIDs associated with socket: {:?}", &sock_info.associated_pids); + println!("Scanned {i} sockets"); + return Ok(sock_info.associated_pids) + } + } + Ok(vec![]) +} + + +pub fn get_client_info(local_port: u16) -> Result<(), ClientInfoError> { + let mut sys = System::new(); + for p in get_associated_pids(local_port)? { + let pid = Pid::from(p as usize); + sys.refresh_process(pid); + let proc = sys.process(pid) + .ok_or(ClientInfoError::PidNotFound)?; + let path = proc.exe().to_string_lossy(); + println!("exe for requesting process: {path}"); + } + + Ok(()) +} diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index c43a206..e3a1678 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -2,6 +2,36 @@ use std::fmt::{Display, Formatter}; use std::convert::From; use std::str::Utf8Error; +use sqlx::{ + error::Error as SqlxError, + migrate::MigrateError, +}; + + +// error during initial setup (primarily loading state from db) +pub enum SetupError { + InvalidRecord, // e.g. wrong size blob for nonce or salt + DbError(SqlxError), +} +impl From for SetupError { + fn from(e: SqlxError) -> SetupError { + SetupError::DbError(e) + } +} +impl From for SetupError { + fn from (e: MigrateError) -> SetupError { + SetupError::DbError(SqlxError::from(e)) + } +} +impl Display for SetupError { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + match self { + SetupError::InvalidRecord => write!(f, "Malformed database record"), + SetupError::DbError(e) => write!(f, "Error from database: {e}"), + } + } +} + // error when attempting to tell a request handler whether to release or deny crednetials pub enum SendResponseError { @@ -26,8 +56,8 @@ pub enum RequestError { InvalidUtf8, MalformedHttpRequest, RequestTooLarge, + NoCredentials(GetCredentialsError), } - impl From for RequestError { fn from(e: std::io::Error) -> RequestError { RequestError::StreamIOError(e) @@ -38,6 +68,11 @@ impl From for RequestError { RequestError::InvalidUtf8 } } +impl From for RequestError { + fn from (e: GetCredentialsError) -> RequestError { + RequestError::NoCredentials(e) + } +} impl Display for RequestError { fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { @@ -47,6 +82,55 @@ impl Display for RequestError { InvalidUtf8 => write!(f, "Could not decode UTF-8 from bytestream"), MalformedHttpRequest => write!(f, "Maformed HTTP request"), RequestTooLarge => write!(f, "HTTP request too large"), + NoCredentials(GetCredentialsError::Locked) => write!(f, "Recieved go-ahead but app is locked"), + NoCredentials(GetCredentialsError::Empty) => write!(f, "Received go-ahead but no credentials are known"), } } } + + +pub enum GetCredentialsError { + Locked, + Empty, +} + + +pub enum UnlockError { + NotLocked, + NoCredentials, + BadPassphrase, + InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded + DbError(SqlxError), +} +impl From for UnlockError { + fn from (e: SqlxError) -> UnlockError { + match e { + SqlxError::RowNotFound => UnlockError::NoCredentials, + _ => UnlockError::DbError(e), + } + } +} +impl Display for UnlockError { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + use UnlockError::*; + match self { + NotLocked => write!(f, "App is not locked"), + NoCredentials => write!(f, "No saved credentials were found"), + BadPassphrase => write!(f, "Invalid passphrase"), + InvalidUtf8 => write!(f, "Decrypted data was corrupted"), + DbError(e) => write!(f, "Database error: {e}"), + } + } +} + + +// Errors encountered while trying to figure out who's on the other end of a request +pub enum ClientInfoError { + PidNotFound, + NetstatError(netstat2::error::Error), +} +impl From for ClientInfoError { + fn from(e: netstat2::error::Error) -> ClientInfoError { + ClientInfoError::NetstatError(e) + } +} diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index cb14af9..ca7d0cc 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -1,8 +1,7 @@ use serde::{Serialize, Deserialize}; use tauri::State; -use crate::state::AppState; -use crate::storage; +use crate::state::{AppState, Session}; #[derive(Copy, Clone, Serialize, Deserialize)] @@ -33,9 +32,21 @@ pub fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Res #[tauri::command] -pub fn unlock(passphrase: String, app_state: State<'_, AppState>) -> bool { - let root_credentials = storage::load(&passphrase); - app_state.set_creds(root_credentials); // for now - true +pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), String> { + app_state.decrypt(&passphrase) + .await + .map_err(|e| e.to_string())?; + Ok(()) +} + + +#[tauri::command] +pub fn session_status(app_state: State<'_, AppState>) -> String { + let session = app_state.session.read().unwrap(); + match *session { + Session::Locked(_) => "locked".into(), + Session::Unlocked(_) => "unlocked".into(), + Session::Empty => "empty".into() + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2d92300..238a5a8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,6 +6,7 @@ use std::str::FromStr; mod errors; +mod clientinfo; mod ipc; mod state; mod server; @@ -13,7 +14,7 @@ mod storage; fn main() { - let initial_state = match state::AppState::new(state::SessionStatus::Locked, None) { + let initial_state = match state::AppState::new() { Ok(state) => state, Err(e) => {eprintln!("{}", e); return;} }; diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index 8c44dba..03e96e9 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -45,6 +45,11 @@ async fn handle(mut stream: TcpStream, app_handle: AppHandle) -> Result<(), Requ let (chan_send, chan_recv) = oneshot::channel(); let app_state = app_handle.state::(); let request_id = app_state.register_request(chan_send); + + if let std::net::SocketAddr::V4(addr) = stream.peer_addr()? { + crate::clientinfo::get_client_info(addr.port()); + } + // Do we want to panic if this fails? Does that mean the frontend is dead? app_handle.emit_all("credentials-request", Request {id: request_id}).unwrap(); @@ -77,7 +82,7 @@ async fn handle(mut stream: TcpStream, app_handle: AppHandle) -> Result<(), Requ return Ok(()); } - let creds = app_state.get_creds_serialized(); + let creds = app_state.get_creds_serialized()?; stream.write(b"\r\nContent-Length: ").await?; stream.write(creds.as_bytes().len().to_string().as_bytes()).await?; diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index f6bfeaa..800ca00 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -4,6 +4,13 @@ use std::sync::RwLock; use serde::{Serialize, Deserialize}; use tokio::sync::oneshot::Sender; use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, sqlite::SqliteConnectOptions}; +use sodiumoxide::crypto::{ + pwhash, + pwhash::Salt, + secretbox, + secretbox::{Nonce, Key} +}; +use tauri::async_runtime as runtime; use crate::ipc; use crate::errors::*; @@ -26,54 +33,98 @@ pub enum Credentials { } -#[derive(Serialize, Deserialize)] -pub enum SessionStatus { - Unlocked, - Locked, - Empty, +pub struct LockedCredentials { + access_key_id: String, + secret_key_enc: Vec, + salt: Salt, + nonce: Nonce, } +pub enum Session { + Unlocked(Credentials), + Locked(LockedCredentials), + Empty, +} + + +// #[derive(Serialize, Deserialize)] +// pub enum SessionStatus { +// Unlocked, +// Locked, +// Empty, +// } + + pub struct AppState { - status: RwLock, - credentials: RwLock>, - request_count: RwLock, - open_requests: RwLock>>, + pub session: RwLock, + pub request_count: RwLock, + pub open_requests: RwLock>>, pool: SqlitePool, } impl AppState { - pub fn new(status: SessionStatus, creds: Option) -> Result { + pub fn new() -> Result { let conn_opts = SqliteConnectOptions::new() .filename("creddy.db") .create_if_missing(true); let pool_opts = SqlitePoolOptions::new(); - let pool: SqlitePool = tauri::async_runtime::block_on(pool_opts.connect_with(conn_opts))?; - tauri::async_runtime::block_on(sqlx::migrate!().run(&pool))?; + let pool: SqlitePool = runtime::block_on(pool_opts.connect_with(conn_opts))?; + runtime::block_on(sqlx::migrate!().run(&pool))?; + let creds = runtime::block_on(Self::load_creds(&pool))?; let state = AppState { - status: RwLock::new(status), - credentials: RwLock::new(creds), + session: RwLock::new(creds), request_count: RwLock::new(0), open_requests: RwLock::new(HashMap::new()), pool, }; + Ok(state) } - async fn _load_from_db(&self) -> Result<(), sqlx::error::Error> { - let row: (i32,) = sqlx::query_as("SELECT COUNT(*) FROM credentials") - .fetch_one(&self.pool) + async fn load_creds(pool: &SqlitePool) -> Result { + let res = sqlx::query!("SELECT * FROM credentials") + .fetch_optional(pool) .await?; + let row = match res { + Some(r) => r, + None => {return Ok(Session::Empty);} + }; - let mut status = self.status.write().unwrap(); - if row.0 > 0 { - *status = SessionStatus::Locked; - } - else { - *status = SessionStatus::Empty; - } + let salt_buf: [u8; 32] = row.salt + .try_into() + .map_err(|_e| SetupError::InvalidRecord)?; + let nonce_buf: [u8; 24] = row.nonce + .try_into() + .map_err(|_e| SetupError::InvalidRecord)?; + + let creds = LockedCredentials { + access_key_id: row.access_key_id, + secret_key_enc: row.secret_key_enc, + salt: Salt(salt_buf), + nonce: Nonce(nonce_buf), + }; + Ok(Session::Locked(creds)) + } + + pub async fn save_creds(&self, creds: Credentials, passphrase: &str) -> Result<(), sqlx::error::Error> { + let (key_id, secret_key) = match creds { + Credentials::LongLived {access_key_id, secret_access_key} => { + (access_key_id, secret_access_key) + }, + _ => unreachable!(), + }; + let salt = pwhash::gen_salt(); + let mut key_buf = [0; secretbox::KEYBYTES]; + pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap(); + let key = Key(key_buf); + // not sure we need both salt AND nonce given that we generate a + // fresh salt every time we encrypt, but better safe than sorry + let nonce = secretbox::gen_nonce(); + let key_enc = secretbox::seal(secret_key.as_bytes(), &nonce, &key); + // insert into database Ok(()) } @@ -85,7 +136,7 @@ impl AppState { }; let mut open_requests = self.open_requests.write().unwrap(); - open_requests.insert(*count, chan); + open_requests.insert(*count, chan); // `count` is the request id *count } @@ -100,17 +151,36 @@ impl AppState { .map_err(|_e| SendResponseError::Abandoned) } - pub fn set_creds(&self, new_creds: Credentials) { - let mut current_creds = self.credentials.write().unwrap(); - *current_creds = Some(new_creds); - let mut status = self.status.write().unwrap(); - *status = SessionStatus::Unlocked; + pub async fn decrypt(&self, passphrase: &str) -> Result<(), UnlockError> { + let session = self.session.read().unwrap(); + let locked = match *session { + Session::Empty => {return Err(UnlockError::NoCredentials);}, + Session::Unlocked(_) => {return Err(UnlockError::NotLocked);}, + Session::Locked(ref c) => c, + }; + + let mut key_buf = [0; secretbox::KEYBYTES]; + // pretty sure this only fails if we're out of memory + pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &locked.salt).unwrap(); + let decrypted = secretbox::open(&locked.secret_key_enc, &locked.nonce, &Key(key_buf)) + .map_err(|_e| UnlockError::BadPassphrase)?; + + let secret_str = String::from_utf8(decrypted).map_err(|_e| UnlockError::InvalidUtf8)?; + let mut session = self.session.write().unwrap(); + let creds = Credentials::LongLived { + access_key_id: locked.access_key_id.clone(), + secret_access_key: secret_str, + }; + *session = Session::Unlocked(creds); + Ok(()) } - pub fn get_creds_serialized(&self) -> String { - let creds_option = self.credentials.read().unwrap(); - // fix this at some point - let creds = creds_option.as_ref().unwrap(); - serde_json::to_string(creds).unwrap() + pub fn get_creds_serialized(&self) -> Result { + let session = self.session.read().unwrap(); + match *session { + Session::Unlocked(ref creds) => Ok(serde_json::to_string(creds).unwrap()), + Session::Locked(_) => Err(GetCredentialsError::Locked), + Session::Empty => Err(GetCredentialsError::Empty), + } } }