encrypt/decrypt and db interaction
This commit is contained in:
46
src-tauri/src/clientinfo.rs
Normal file
46
src-tauri/src/clientinfo.rs
Normal file
@ -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<Vec<u32>, 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(())
|
||||
}
|
@ -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<SqlxError> for SetupError {
|
||||
fn from(e: SqlxError) -> SetupError {
|
||||
SetupError::DbError(e)
|
||||
}
|
||||
}
|
||||
impl From<MigrateError> 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<tokio::io::Error> for RequestError {
|
||||
fn from(e: std::io::Error) -> RequestError {
|
||||
RequestError::StreamIOError(e)
|
||||
@ -38,6 +68,11 @@ impl From<Utf8Error> for RequestError {
|
||||
RequestError::InvalidUtf8
|
||||
}
|
||||
}
|
||||
impl From<GetCredentialsError> 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<SqlxError> 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<netstat2::error::Error> for ClientInfoError {
|
||||
fn from(e: netstat2::error::Error) -> ClientInfoError {
|
||||
ClientInfoError::NetstatError(e)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;}
|
||||
};
|
||||
|
@ -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::<crate::state::AppState>();
|
||||
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?;
|
||||
|
@ -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<u8>,
|
||||
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<SessionStatus>,
|
||||
credentials: RwLock<Option<Credentials>>,
|
||||
request_count: RwLock<u64>,
|
||||
open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
||||
pub session: RwLock<Session>,
|
||||
pub request_count: RwLock<u64>,
|
||||
pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(status: SessionStatus, creds: Option<Credentials>) -> Result<Self, sqlx::error::Error> {
|
||||
pub fn new() -> Result<Self, SetupError> {
|
||||
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<Session, SetupError> {
|
||||
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<String, GetCredentialsError> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user