encrypt/decrypt and db interaction

This commit is contained in:
Joseph Montanaro 2022-12-03 21:47:09 -08:00
parent 196510e9a2
commit 1e4e1c9a5f
9 changed files with 375 additions and 49 deletions

109
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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 (

View 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(())
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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;}
};

View File

@ -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?;

View File

@ -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),
}
}
}