get backend running

This commit is contained in:
Joseph Montanaro 2024-06-19 05:10:55 -04:00
parent d0a2532c27
commit 9928996fab
11 changed files with 310 additions and 207 deletions

View File

@ -0,0 +1,52 @@
-- app structure is changing - instead of passphrase/salt being per credential,
-- we now have a single app-wide key, which is generated by hashing the passphrase
-- with the known salt. To verify the key thus produced, we store a value previously
-- encrypted with that key, and attempt decryption once the key has been re-generated.
-- For migration purposes, we want convert the passphrase for the most recent set of
-- AWS credentials and turn it into the app-wide passphrase. The only value that we
-- have which is encrypted with that passphrase is the secret key for those credentials,
-- so we will just use that as the `verify_blob`. Feels a little weird, but oh well.
WITH latest_creds AS (
SELECT *
FROM credentials
ORDER BY created_at DESC
LIMIT 1
)
INSERT INTO kv (name, value)
SELECT 'salt', salt FROM latest_creds
UNION ALL
SELECT 'verify_nonce', nonce FROM latest_creds
UNION ALL
SELECT 'verify_blob', secret_key_enc FROM latest_creds;
-- Credentials are now going to be stored in a separate table per type of credential
CREATE TABLE aws_credentials (
name TEXT UNIQUE NOT NULL,
access_key_id TEXT NOT NULL,
secret_key_enc BLOB NOT NULL,
nonce BLOB NOT NULL,
-- at some point we may want to offer to auto-rotate AWS keys,
-- so let's make sure to keep track of when they were created
created_at INTEGER NOT NULL
);
INSERT INTO aws_credentials (name, access_key_id, secret_key_enc, nonce, created_at)
SELECT 'default', access_key_id, secret_key_enc, nonce, created_at
FROM credentials
ORDER BY created_at DESC
LIMIT 1;
DROP TABLE credentials;
-- SSH keys are the new hotness
CREATE TABLE ssh_keys (
name TEXT UNIQUE NOT NULL,
public_key BLOB NOT NULL,
private_key_enc BLOB NOT NULL,
nonce BLOB NOT NULL,
created_at INTEGER NOT NULL
);

View File

@ -23,7 +23,7 @@ use tauri::menu::MenuItem;
use crate::{ use crate::{
config::{self, AppConfig}, config::{self, AppConfig},
credentials::Session, credentials::AppSession,
ipc, ipc,
server::Server, server::Server,
errors::*, errors::*,
@ -48,7 +48,7 @@ pub fn run() -> tauri::Result<()> {
ipc::respond, ipc::respond,
ipc::get_session_status, ipc::get_session_status,
ipc::signal_activity, ipc::signal_activity,
ipc::save_credentials, ipc::save_aws_credential,
ipc::get_config, ipc::get_config,
ipc::save_config, ipc::save_config,
ipc::launch_terminal, ipc::launch_terminal,
@ -109,7 +109,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
err => err?, err => err?,
}; };
let session = Session::load(&pool).await?; let app_session = AppSession::load(&pool).await?;
Server::start(app.handle().clone())?; Server::start(app.handle().clone())?;
config::set_auto_launch(conf.start_on_login)?; config::set_auto_launch(conf.start_on_login)?;
@ -128,12 +128,11 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
.map(|names| names.split(':').any(|n| n == "GNOME")) .map(|names| names.split(':').any(|n| n == "GNOME"))
.unwrap_or(false); .unwrap_or(false);
// if session is empty, this is probably the first launch, so don't autohide
if !conf.start_minimized || is_first_launch { if !conf.start_minimized || is_first_launch {
show_main_window(&app.handle())?; show_main_window(&app.handle())?;
} }
let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome); let state = AppState::new(conf, app_session, pool, setup_errors, desktop_is_gnome);
app.manage(state); app.manage(state);
// make sure we do this after managing app state, so that it doesn't panic // make sure we do this after managing app state, so that it doesn't panic

View File

@ -12,7 +12,6 @@ use clap::{
}; };
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::credentials::Credentials;
use crate::errors::*; use crate::errors::*;
use crate::server::{Request, Response}; use crate::server::{Request, Response};
use crate::shortcuts::ShortcutAction; use crate::shortcuts::ShortcutAction;
@ -80,9 +79,10 @@ pub fn parser() -> Command<'static> {
pub fn get(args: &ArgMatches) -> Result<(), CliError> { pub fn get(args: &ArgMatches) -> Result<(), CliError> {
let base = args.get_one("base").unwrap_or(&false); let base = args.get_one("base").unwrap_or(&false);
let output = match get_credentials(*base)? { let output = match make_request(&Request::GetAwsCredentials { base: *base })? {
Credentials::Base(creds) => serde_json::to_string(&creds).unwrap(), Response::AwsBase(creds) => serde_json::to_string(&creds).unwrap(),
Credentials::Session(creds) => serde_json::to_string(&creds).unwrap(), Response::AwsSession(creds) => serde_json::to_string(&creds).unwrap(),
r => return Err(RequestError::Unexpected(r).into()),
}; };
println!("{output}"); println!("{output}");
Ok(()) Ok(())
@ -98,16 +98,17 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
let mut cmd = ChildCommand::new(cmd_name); let mut cmd = ChildCommand::new(cmd_name);
cmd.args(cmd_line); cmd.args(cmd_line);
match get_credentials(base)? { match make_request(&Request::GetAwsCredentials { base })? {
Credentials::Base(creds) => { Response::AwsBase(creds) => {
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id); cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key); cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
}, },
Credentials::Session(creds) => { Response::AwsSession(creds) => {
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id); cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key); cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
cmd.env("AWS_SESSION_TOKEN", creds.session_token); cmd.env("AWS_SESSION_TOKEN", creds.session_token);
} },
r => return Err(RequestError::Unexpected(r).into()),
} }
#[cfg(unix)] #[cfg(unix)]
@ -157,16 +158,6 @@ pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
} }
fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
let req = Request::GetAwsCredentials { base };
match make_request(&req) {
Ok(Response::Aws(creds)) => Ok(creds),
Ok(r) => Err(RequestError::Unexpected(r)),
Err(e) => Err(e),
}
}
#[tokio::main] #[tokio::main]
async fn make_request(req: &Request) -> Result<Response, RequestError> { async fn make_request(req: &Request) -> Result<Response, RequestError> {
let mut data = serde_json::to_string(req).unwrap(); let mut data = serde_json::to_string(req).unwrap();

View File

@ -1,9 +1,15 @@
use std::fmt::{self, Formatter};
use std::time::{SystemTime, UNIX_EPOCH};
use aws_smithy_types::date_time::{DateTime, Format};
use chacha20poly1305::XNonce;
use serde::{ use serde::{
Serialize, Serialize,
Deserialize, Deserialize,
Serializer, Serializer,
Deserializer, Deserializer,
}; };
use serde::de::{self, Visitor};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use super::{Crypto, PersistentCredential}; use super::{Crypto, PersistentCredential};
@ -27,40 +33,46 @@ impl AwsBaseCredential {
} }
impl PersistentCredential for AwsBaseCredential { impl PersistentCredential for AwsBaseCredential {
pub async fn save(&self, crypt: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> { async fn save(&self, crypto: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
let (nonce, ciphertext) = crypto.encrypt(self.secret_access_key.as_bytes())?; let (nonce, ciphertext) = crypto.encrypt(self.secret_access_key.as_bytes())?;
let nonce_bytes = &nonce.as_slice();
sqlx::query!( sqlx::query!(
"INSERT INTO aws_credentials ( "INSERT INTO aws_credentials (
name, name,
key_id, access_key_id,
secret_key_enc, secret_key_enc,
nonce, nonce,
updated_at created_at
) )
VALUES ('main', ?, ?, ? strftime('%s')) VALUES ('default', ?, ?, ?, strftime('%s'))
ON CONFLICT DO UPDATE SET ON CONFLICT DO UPDATE SET
key_id = excluded.key_id, access_key_id = excluded.access_key_id,
secret_key_enc = excluded.secret_key_enc, secret_key_enc = excluded.secret_key_enc,
nonce = excluded.nonce nonce = excluded.nonce,
updated_at = excluded.updated_at", created_at = excluded.created_at",
self.access_key_id, self.access_key_id,
ciphertext, ciphertext,
nonce, nonce_bytes,
).execute(pool).await?; ).execute(pool).await?;
Ok(()) Ok(())
} }
pub async fn load(crypt: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { async fn load(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
let row = sqlx::query!("SELECT * FROM aws_credentials WHERE name = 'main'") let row = sqlx::query!("SELECT * FROM aws_credentials WHERE name = 'default'")
.fetch_optional(pool) .fetch_optional(pool)
.await? .await?
.ok_or(LoadCredentialsError::NoCredentials); .ok_or(LoadCredentialsError::NoCredentials)?;
// note: switch to try_from eventually
let nonce = XNonce::clone_from_slice(&row.nonce);
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
let secret_key = String::from_utf8(secret_key_bytes)
.map_err(|_| LoadCredentialsError::InvalidData)?;
let secret_key = crypto.decrypt(&row.nonce, &row.secret_key_enc)?;
let creds = Self { let creds = Self {
version: 1, version: 1,
access_key_id: row.key_id, access_key_id: row.access_key_id,
secret_access_key: secret_key, secret_access_key: secret_key,
}; };
Ok(creds) Ok(creds)
@ -82,7 +94,7 @@ pub struct AwsSessionCredential {
} }
impl AwsSessionCredential { impl AwsSessionCredential {
pub async fn from_base(base: &BaseCredentials) -> Result<Self, GetSessionError> { pub async fn from_base(base: &AwsBaseCredential) -> Result<Self, GetSessionError> {
let req_creds = aws_sdk_sts::Credentials::new( let req_creds = aws_sdk_sts::Credentials::new(
&base.access_key_id, &base.access_key_id,
&base.secret_access_key, &base.secret_access_key,
@ -116,7 +128,7 @@ impl AwsSessionCredential {
.ok_or(GetSessionError::EmptyResponse)? .ok_or(GetSessionError::EmptyResponse)?
.clone(); .clone();
let session_creds = SessionCredentials { let session_creds = AwsSessionCredential {
version: 1, version: 1,
access_key_id, access_key_id,
secret_access_key, secret_access_key,
@ -143,6 +155,9 @@ impl AwsSessionCredential {
} }
fn default_credentials_version() -> usize { 1 }
struct DateTimeVisitor; struct DateTimeVisitor;
impl<'de> Visitor<'de> for DateTimeVisitor { impl<'de> Visitor<'de> for DateTimeVisitor {

View File

@ -1,3 +1,5 @@
use std::fmt::{Debug, Formatter};
use argon2::{ use argon2::{
Argon2, Argon2,
Algorithm, Algorithm,
@ -12,32 +14,25 @@ use chacha20poly1305::{
Aead, Aead,
AeadCore, AeadCore,
KeyInit, KeyInit,
Error as AeadError,
generic_array::GenericArray, generic_array::GenericArray,
}, },
}; };
use serde::{Serialize, Deserialize}; use serde::Deserialize;
use sqlx::{FromRow, SqlitePool}; use sqlx::SqlitePool;
use crate::errors::*;
use crate::kv; use crate::kv;
mod aws; mod aws;
pub use aws::{AwsBaseCredential, AwsSessionCredential}; pub use aws::{AwsBaseCredential, AwsSessionCredential};
pub enum CredentialKind { pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
AwsBase,
AwsSession,
}
pub trait PersistentCredential {
async fn load(crypt: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError>; async fn load(crypt: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError>;
async fn save(&self, crypt: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError>; async fn save(&self, crypt: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError>;
} }
#[derive(Clone, Debug)]
#[derive(Debug, Clone)]
pub enum AppSession { pub enum AppSession {
Unlocked { Unlocked {
salt: [u8; 32], salt: [u8; 32],
@ -54,14 +49,14 @@ pub enum AppSession {
impl AppSession { impl AppSession {
pub fn new(passphrase: &str) -> Result<Self, CryptoError> { pub fn new(passphrase: &str) -> Result<Self, CryptoError> {
let salt = Crypto::salt(); let salt = Crypto::salt();
let crypto = Crypto::new(passphrase, &salt); let crypto = Crypto::new(passphrase, &salt)?;
Ok(Self::Unlocked {salt, crypto}) Ok(Self::Unlocked {salt, crypto})
} }
pub fn unlock(self, passphrase: &str) -> Result<Self, UnlockError> { pub fn unlock(&mut self, passphrase: &str) -> Result<(), UnlockError> {
let (salt, nonce, blob) = match self { let (salt, nonce, blob) = match self {
Self::Empty => return Err(UnlockError::NoCredentials), Self::Empty => return Err(UnlockError::NoCredentials),
Self::Unlocked => return Err(UnlockError::NotLocked), Self::Unlocked {..} => return Err(UnlockError::NotLocked),
Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob), Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob),
}; };
@ -69,61 +64,78 @@ impl AppSession {
.map_err(|e| CryptoError::Argon2(e))?; .map_err(|e| CryptoError::Argon2(e))?;
// if passphrase is incorrect, this will fail // if passphrase is incorrect, this will fail
let verify = crypto.decrypt(&nonce, &blob)?; let _verify = crypto.decrypt(&nonce, &blob)?;
Ok(Self::Unlocked{crypto, salt}) *self = Self::Unlocked {crypto, salt: *salt};
Ok(())
} }
pub async fn load(pool: &SqlitePool) -> Result<Self, LoadKvError> { pub async fn load(pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
match kv::load_bytes_multi!(pool, "salt", "verify_nonce", "verify_blob").await? { match kv::load_bytes_multi!(pool, "salt", "verify_nonce", "verify_blob").await? {
Some((salt, verify_nonce, verify_blob)) => { Some((salt, nonce, blob)) => {
Ok(Self::Locked {salt, verify_nonce, verify_blob}),
Ok(Self::Locked {
salt: salt.try_into().map_err(|_| LoadCredentialsError::InvalidData)?,
// note: replace this with try_from at some point
verify_nonce: XNonce::clone_from_slice(&nonce),
verify_blob: blob,
})
}, },
None => Ok(Self::Empty), None => Ok(Self::Empty),
} }
} }
pub async fn save(&self, pool: &SqlitePool) -> Result<(), LockError> { pub async fn save(&self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
let (salt, nonce, blob) = match self { match self {
Self::Unlocked {salt, crypto} => { Self::Unlocked {salt, crypto} => {
let (nonce, blob) = crypto.encrypt(b"correct horse battery staple") let (nonce, blob) = crypto.encrypt(b"correct horse battery staple")?;
.map_err(|e| CryptoError::Aead(e))?;
(salt, nonce, blob)
},
Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob),
// "saving" an empty session just means doing nothing
Self::Empty => return Ok(()),
};
kv::save(pool, "salt", salt).await?; kv::save(pool, "salt", salt).await?;
kv::save(pool, "verify_nonce", nonce).await?; kv::save(pool, "verify_nonce", &nonce.as_slice()).await?;
kv::save(pool, "verify_blob", blob).await?; kv::save(pool, "verify_blob", &blob).await?;
},
Self::Locked {salt, verify_nonce, verify_blob} => {
kv::save(pool, "salt", salt).await?;
kv::save(pool, "verify_nonce", &verify_nonce.as_slice()).await?;
kv::save(pool, "verify_blob", verify_blob).await?;
},
// "saving" an empty session just means doing nothing
Self::Empty => (),
};
Ok(()) Ok(())
} }
pub fn try_encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), CryptoError> { pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> {
let crypto = match self { match self {
Self::Empty => Err(GetCredentialsError::Empty), Self::Empty => Err(GetCredentialsError::Empty),
Self::Locked => Err(GetCredentialsError::Locked), Self::Locked {..} => Err(GetCredentialsError::Locked),
Self::Unlocked {crypto, ..} => Ok(crypto),
}
}
pub fn try_encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), GetCredentialsError> {
let crypto = match self {
Self::Empty => return Err(GetCredentialsError::Empty),
Self::Locked {..} => return Err(GetCredentialsError::Locked),
Self::Unlocked {crypto, ..} => crypto, Self::Unlocked {crypto, ..} => crypto,
}?; };
let res = crypto.encrypt(data)?; let res = crypto.encrypt(data)?;
Ok(res) Ok(res)
} }
pub fn try_decrypt(&self, nonce: XNonce, data: &[u8]) -> Result<Vec<u8>, CryptoError> { pub fn try_decrypt(&self, nonce: XNonce, data: &[u8]) -> Result<Vec<u8>, GetCredentialsError> {
let crypto = match self { let crypto = match self {
Self::Empty => Err(GetCredentialsError::Empty), Self::Empty => return Err(GetCredentialsError::Empty),
Self::Locked => Err(GetCredentialsError::Locked), Self::Locked {..} => return Err(GetCredentialsError::Locked),
Self::Unlocked {crypto, ..} => crypto, Self::Unlocked {crypto, ..} => crypto,
}?; };
let res = crypto.decrypt(nonce, data)?; let res = crypto.decrypt(&nonce, data)?;
Ok(res) Ok(res)
} }
} }
#[derive(Clone)]
pub struct Crypto { pub struct Crypto {
cipher: XChaCha20Poly1305, cipher: XChaCha20Poly1305,
} }
@ -181,13 +193,20 @@ impl Crypto {
salt salt
} }
fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), AeadError> { fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), CryptoError> {
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = self.cipher.encrypt(&nonce, data)?; let ciphertext = self.cipher.encrypt(&nonce, data)?;
Ok((nonce, ciphertext)) Ok((nonce, ciphertext))
} }
fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result<Vec<u8>, AeadError> { fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
self.cipher.decrypt(nonce, data) let plaintext = self.cipher.decrypt(nonce, data)?;
Ok(plaintext)
}
}
impl Debug for Crypto {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "Crypto {{ [...] }}")
} }
} }

View File

@ -208,6 +208,12 @@ pub enum GetCredentialsError {
Locked, Locked,
#[error("No credentials are known")] #[error("No credentials are known")]
Empty, Empty,
#[error(transparent)]
Crypto(#[from] CryptoError),
#[error(transparent)]
Load(#[from] LoadCredentialsError),
#[error(transparent)]
GetSession(#[from] GetSessionError),
} }
@ -245,8 +251,8 @@ pub enum UnlockError {
pub enum LockError { pub enum LockError {
#[error("App is not unlocked")] #[error("App is not unlocked")]
NotUnlocked, NotUnlocked,
#[error("Database error: {0}")] #[error(transparent)]
DbError(#[from] SqlxError), LoadCredentials(#[from] LoadCredentialsError),
#[error(transparent)] #[error(transparent)]
Setup(#[from] SetupError), Setup(#[from] SetupError),
#[error(transparent)] #[error(transparent)]
@ -261,19 +267,23 @@ pub enum SaveCredentialsError {
#[error("Database error: {0}")] #[error("Database error: {0}")]
DbError(#[from] SqlxError), DbError(#[from] SqlxError),
#[error("Encryption error: {0}")] #[error("Encryption error: {0}")]
Encryption(#[from] chacha20poly1305::Error), Crypto(#[from] CryptoError),
#[error(transparent)]
Session(#[from] GetCredentialsError),
} }
#[derive(Debug, ThisError, AsRefStr)] #[derive(Debug, ThisError, AsRefStr)]
pub enum LoadCredentialsError { pub enum LoadCredentialsError {
#[error("Database error: {0}")] #[error("Database error: {0}")]
DbError(#[from] SqlxError), DbError(#[from] SqlxError),
#[error("Encryption error: {0}")] #[error("Invalid passphrase")] // pretty sure this is the only way decryption fails
Encryption(#[from] chacha20poly1305::Error), Encryption(#[from] CryptoError),
#[error("Credentials not found")] #[error("Credentials not found")]
NoCredentials, NoCredentials,
#[error("Could not decode credentials: {0}")] #[error("Could not decode credential data")]
Invalid(#[from] serde_json::Error), InvalidData,
#[error(transparent)]
LoadKv(#[from] LoadKvError),
} }
@ -292,6 +302,10 @@ pub enum CryptoError {
Argon2(#[from] argon2::Error), Argon2(#[from] argon2::Error),
#[error("Invalid passphrase")] // I think this is the only way decryption fails #[error("Invalid passphrase")] // I think this is the only way decryption fails
Aead(#[from] chacha20poly1305::aead::Error), Aead(#[from] chacha20poly1305::aead::Error),
#[error("App is currently locked")]
Locked,
#[error("No passphrase has been specified")]
Empty,
} }
@ -409,6 +423,8 @@ impl_serialize_basic!(GetCredentialsError);
impl_serialize_basic!(ClientInfoError); impl_serialize_basic!(ClientInfoError);
impl_serialize_basic!(WindowError); impl_serialize_basic!(WindowError);
impl_serialize_basic!(LockError); impl_serialize_basic!(LockError);
impl_serialize_basic!(SaveCredentialsError);
impl_serialize_basic!(LoadCredentialsError);
impl Serialize for HandlerError { impl Serialize for HandlerError {

View File

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use tauri::State; use tauri::State;
use crate::config::AppConfig; use crate::config::AppConfig;
use crate::credentials::{Session,BaseCredentials}; use crate::credentials::{AppSession, AwsBaseCredential};
use crate::errors::*; use crate::errors::*;
use crate::clientinfo::Client; use crate::clientinfo::Client;
use crate::state::AppState; use crate::state::AppState;
@ -17,6 +17,31 @@ pub struct AwsRequestNotification {
} }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SshRequestNotification {
pub id: u64,
pub client: Client,
pub key_name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum RequestNotification {
Aws(AwsRequestNotification),
Ssh(SshRequestNotification),
}
impl RequestNotification {
pub fn new_aws(id: u64, client: Client, base: bool) -> Self {
Self::Aws(AwsRequestNotification {id, client, base})
}
pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self {
Self::Ssh(SshRequestNotification {id, client, key_name})
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RequestResponse { pub struct RequestResponse {
pub id: u64, pub id: u64,
@ -46,11 +71,11 @@ pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Resul
#[tauri::command] #[tauri::command]
pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String, ()> { pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String, ()> {
let session = app_state.session.read().await; let session = app_state.app_session.read().await;
let status = match *session { let status = match *session {
Session::Locked(_) => "locked".into(), AppSession::Locked{..} => "locked".into(),
Session::Unlocked{..} => "unlocked".into(), AppSession::Unlocked{..} => "unlocked".into(),
Session::Empty => "empty".into() AppSession::Empty => "empty".into(),
}; };
Ok(status) Ok(status)
} }
@ -64,12 +89,11 @@ pub async fn signal_activity(app_state: State<'_, AppState>) -> Result<(), ()> {
#[tauri::command] #[tauri::command]
pub async fn save_credentials( pub async fn save_aws_credential(
credentials: BaseCredentials, credential: AwsBaseCredential,
passphrase: String,
app_state: State<'_, AppState> app_state: State<'_, AppState>
) -> Result<(), UnlockError> { ) -> Result<(), SaveCredentialsError> {
app_state.new_creds(credentials, &passphrase).await app_state.save_creds(credential).await
} }

View File

@ -44,45 +44,37 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>
} }
// pub async fn load_bytes_multi<const N: usize>(
// pool: &SqlitePool,
// names: [&str; N],
// ) -> Result<Option<[Vec<u8>; N]>, sqlx::Error> {
// // just use multiple queries, who cares
// let res: [Vec<u8>; N] = Default::default();
// for (i, name) in names.as_slice().iter().enumerate() {
// match load_bytes(pool, name).await? {
// Some(bytes) => res[i] = bytes,
// None => return Ok(None),
// }
// }
// Ok(res);
// }
macro_rules! load_bytes_multi { macro_rules! load_bytes_multi {
( (
$pool:ident, $pool:ident,
$($name:literal),* $($name:literal),*
) => { ) => {
// wrap everything up in an immediately-invoked closure for easy short-circuiting // wrap everything up in an async block for easy short-circuiting...
(|| { async {
// a tuple, with one item for each repetition of $name // ...returning a Result...
Ok::<_, sqlx::Error>(
//containing an Option...
Some(
// containing a tuple...
( (
// repeat this match block for every name // ...with one item for each repetition of $name
$( $(
// load_bytes returns Result<Option<_>>, the Result is handled by // load_bytes returns Result<Option<_>>, the Result is handled by
// the ? and we match on the Option // the ? and we match on the Option
match load_bytes(pool, $name)? { match crate::kv::load_bytes($pool, $name).await? {
Some(v) => v, Some(v) => v,
None => return Ok(None) None => return Ok(None)
}, },
)* )*
) )
})() )
)
}
} }
} }
pub(crate) use load_bytes_multi;
// macro_rules! load_multi { // macro_rules! load_multi {
// ( // (

View File

@ -7,8 +7,11 @@ use tauri::{AppHandle, Manager};
use crate::errors::*; use crate::errors::*;
use crate::clientinfo::{self, Client}; use crate::clientinfo::{self, Client};
use crate::credentials::Credentials; use crate::credentials::{
use crate::ipc::{Approval, AwsRequestNotification}; AwsBaseCredential,
AwsSessionCredential,
};
use crate::ipc::{Approval, RequestNotification};
use crate::state::AppState; use crate::state::AppState;
use crate::shortcuts::{self, ShortcutAction}; use crate::shortcuts::{self, ShortcutAction};
@ -40,7 +43,8 @@ pub enum Request {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum Response { pub enum Response {
Aws(Credentials), AwsBase(AwsBaseCredential),
AwsSession(AwsSessionCredential),
Empty, Empty,
} }
@ -127,7 +131,7 @@ async fn get_aws_credentials(
// but ? returns immediately, and we want to unregister the request before returning // but ? returns immediately, and we want to unregister the request before returning
// so we bundle it all up in an async block and return a Result so we can handle errors // so we bundle it all up in an async block and return a Result so we can handle errors
let proceed = async { let proceed = async {
let notification = AwsRequestNotification {id: request_id, client, base}; let notification = RequestNotification::new_aws(request_id, client, base);
app_handle.emit("credentials-request", &notification)?; app_handle.emit("credentials-request", &notification)?;
let response = tokio::select! { let response = tokio::select! {
@ -141,12 +145,12 @@ async fn get_aws_credentials(
match response.approval { match response.approval {
Approval::Approved => { Approval::Approved => {
if response.base { if response.base {
let creds = state.base_creds_cloned().await?; let creds = state.get_aws_base().await?;
Ok(Response::Aws(Credentials::Base(creds))) Ok(Response::AwsBase(creds))
} }
else { else {
let creds = state.session_creds_cloned().await?; let creds = state.get_aws_session().await?;
Ok(Response::Aws(Credentials::Session(creds))) Ok(Response::AwsSession(creds.clone()))
} }
}, },
Approval::Denied => Err(HandlerError::Denied), Approval::Denied => Err(HandlerError::Denied),

View File

@ -3,7 +3,7 @@ use std::time::Duration;
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::{ use tokio::{
sync::RwLock, sync::{RwLock, RwLockReadGuard},
sync::oneshot::{self, Sender}, sync::oneshot::{self, Sender},
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
@ -14,12 +14,12 @@ use tauri::{
use crate::app; use crate::app;
use crate::credentials::{ use crate::credentials::{
Session, AppSession,
BaseCredentials, AwsSessionCredential,
SessionCredentials,
}; };
use crate::{config, config::AppConfig}; use crate::{config, config::AppConfig};
use crate::ipc::{self, Approval, RequestResponse}; use crate::credentials::{AwsBaseCredential, PersistentCredential};
use crate::ipc::{self, RequestResponse};
use crate::errors::*; use crate::errors::*;
use crate::shortcuts; use crate::shortcuts;
@ -101,7 +101,8 @@ impl VisibilityLease {
#[derive(Debug)] #[derive(Debug)]
pub struct AppState { pub struct AppState {
pub config: RwLock<AppConfig>, pub config: RwLock<AppConfig>,
pub session: RwLock<Session>, pub app_session: RwLock<AppSession>,
pub aws_session: RwLock<Option<AwsSessionCredential>>,
pub last_activity: RwLock<OffsetDateTime>, pub last_activity: RwLock<OffsetDateTime>,
pub request_count: RwLock<u64>, pub request_count: RwLock<u64>,
pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>, pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>,
@ -116,14 +117,15 @@ pub struct AppState {
impl AppState { impl AppState {
pub fn new( pub fn new(
config: AppConfig, config: AppConfig,
session: Session, app_session: AppSession,
pool: SqlitePool, pool: SqlitePool,
setup_errors: Vec<String>, setup_errors: Vec<String>,
desktop_is_gnome: bool, desktop_is_gnome: bool,
) -> AppState { ) -> AppState {
AppState { AppState {
config: RwLock::new(config), config: RwLock::new(config),
session: RwLock::new(session), app_session: RwLock::new(app_session),
aws_session: RwLock::new(None),
last_activity: RwLock::new(OffsetDateTime::now_utc()), last_activity: RwLock::new(OffsetDateTime::now_utc()),
request_count: RwLock::new(0), request_count: RwLock::new(0),
waiting_requests: RwLock::new(HashMap::new()), waiting_requests: RwLock::new(HashMap::new()),
@ -135,13 +137,12 @@ impl AppState {
} }
} }
pub async fn new_creds(&self, base_creds: BaseCredentials, passphrase: &str) -> Result<(), UnlockError> { pub async fn save_creds<C>(&self, credential: C) -> Result<(), SaveCredentialsError>
let locked = base_creds.encrypt(passphrase)?; where C: PersistentCredential
// do this first so that if it fails we don't save bad credentials {
self.new_session(base_creds).await?; let session = self.app_session.read().await;
locked.save(&self.pool).await?; let crypto = session.try_get_crypto()?;
credential.save(crypto, &self.pool).await
Ok(())
} }
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> { pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
@ -171,6 +172,7 @@ impl AppState {
c c
}; };
let mut waiting_requests = self.waiting_requests.write().await; let mut waiting_requests = self.waiting_requests.write().await;
waiting_requests.insert(*count, sender); // `count` is the request id waiting_requests.insert(*count, sender); // `count` is the request id
*count *count
@ -187,11 +189,6 @@ impl AppState {
} }
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> { pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
if let Approval::Approved = response.approval {
let mut session = self.session.write().await;
session.renew_if_expired().await?;
}
let mut waiting_requests = self.waiting_requests.write().await; let mut waiting_requests = self.waiting_requests.write().await;
waiting_requests waiting_requests
.remove(&response.id) .remove(&response.id)
@ -201,24 +198,17 @@ impl AppState {
} }
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> { pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
let base_creds = match *self.session.read().await { let mut session = self.app_session.write().await;
Session::Empty => {return Err(UnlockError::NoCredentials);}, session.unlock(passphrase)
Session::Unlocked{..} => {return Err(UnlockError::NotLocked);},
Session::Locked(ref locked) => locked.decrypt(passphrase)?,
};
// Read lock is dropped here, so this doesn't deadlock
self.new_session(base_creds).await?;
Ok(())
} }
pub async fn lock(&self) -> Result<(), LockError> { pub async fn lock(&self) -> Result<(), LockError> {
let mut session = self.session.write().await; let mut session = self.app_session.write().await;
match *session { match *session {
Session::Empty => Err(LockError::NotUnlocked), AppSession::Empty => Err(LockError::NotUnlocked),
Session::Locked(_) => Err(LockError::NotUnlocked), AppSession::Locked{..} => Err(LockError::NotUnlocked),
Session::Unlocked{..} => { AppSession::Unlocked{..} => {
*session = Session::load(&self.pool).await?; *session = AppSession::load(&self.pool).await?;
let app_handle = app::APP.get().unwrap(); let app_handle = app::APP.get().unwrap();
app_handle.emit("locked", None::<usize>)?; app_handle.emit("locked", None::<usize>)?;
@ -228,6 +218,29 @@ impl AppState {
} }
} }
pub async fn get_aws_base(&self) -> Result<AwsBaseCredential, GetCredentialsError> {
let app_session = self.app_session.read().await;
let crypto = app_session.try_get_crypto()?;
let creds = AwsBaseCredential::load(crypto, &self.pool).await?;
Ok(creds)
}
pub async fn get_aws_session(&self) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> {
// yes, this sometimes results in double-fetching base credentials from disk
// I'm done trying to be optimal
{
let mut aws_session = self.aws_session.write().await;
if aws_session.is_none() || aws_session.as_ref().unwrap().is_expired() {
let base_creds = self.get_aws_base().await?;
*aws_session = Some(AwsSessionCredential::from_base(&base_creds).await?);
}
}
// we know this is safe, because we juse made sure of it
let s = RwLockReadGuard::map(self.aws_session.read().await, |opt| opt.as_ref().unwrap());
Ok(s)
}
pub async fn signal_activity(&self) { pub async fn signal_activity(&self) {
let mut last_activity = self.last_activity.write().await; let mut last_activity = self.last_activity.write().await;
*last_activity = OffsetDateTime::now_utc(); *last_activity = OffsetDateTime::now_utc();
@ -245,27 +258,8 @@ impl AppState {
} }
pub async fn is_unlocked(&self) -> bool { pub async fn is_unlocked(&self) -> bool {
let session = self.session.read().await; let session = self.app_session.read().await;
matches!(*session, Session::Unlocked{..}) matches!(*session, AppSession::Unlocked{..})
}
pub async fn base_creds_cloned(&self) -> Result<BaseCredentials, GetCredentialsError> {
let app_session = self.session.read().await;
let (base, _session) = app_session.try_get()?;
Ok(base.clone())
}
pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> {
let app_session = self.session.read().await;
let (_base, session) = app_session.try_get()?;
Ok(session.clone())
}
async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
let session = SessionCredentials::from_base(&base).await?;
let mut app_session = self.session.write().await;
*app_session = Session::Unlocked {base, session};
Ok(())
} }
pub async fn register_terminal_request(&self) -> Result<(), ()> { pub async fn register_terminal_request(&self) -> Result<(), ()> {

View File

@ -45,23 +45,20 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
lease.release(); lease.release();
} }
// more lock-management
{
let app_session = state.session.read().await;
// session should really be unlocked at this point, but if the frontend misbehaves // session should really be unlocked at this point, but if the frontend misbehaves
// (i.e. lies about unlocking) we could end up here with a locked session // (i.e. lies about unlocking) we could end up here with a locked session
// this will result in an error popup to the user (see main hotkey handler) // this will result in an error popup to the user (see main hotkey handler)
let (base_creds, session_creds) = app_session.try_get()?;
if use_base { if use_base {
let base_creds = state.get_aws_base().await?;
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id); cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key); cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
} }
else { else {
let session_creds = state.get_aws_session().await?;
cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id); cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key); cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token); cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
} }
}
let res = match cmd.spawn() { let res = match cmd.spawn() {
Ok(_) => Ok(()), Ok(_) => Ok(()),