get backend running
This commit is contained in:
		
							
								
								
									
										52
									
								
								src-tauri/migrations/20240617142724_credential_split.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src-tauri/migrations/20240617142724_credential_split.sql
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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))?;
 | 
					                kv::save(pool, "salt", salt).await?;
 | 
				
			||||||
                (salt, nonce, blob)
 | 
					                kv::save(pool, "verify_nonce", &nonce.as_slice()).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?;
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob),
 | 
					 | 
				
			||||||
            // "saving" an empty session just means doing nothing
 | 
					            // "saving" an empty session just means doing nothing
 | 
				
			||||||
            Self::Empty => return Ok(()),
 | 
					            Self::Empty => (),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        kv::save(pool, "salt", salt).await?;
 | 
					 | 
				
			||||||
        kv::save(pool, "verify_nonce", nonce).await?;
 | 
					 | 
				
			||||||
        kv::save(pool, "verify_blob", blob).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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 {{ [...] }}")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>(
 | 
				
			||||||
                // repeat this match block for every name
 | 
					                //containing an Option...
 | 
				
			||||||
                $(
 | 
					                Some(
 | 
				
			||||||
                    // load_bytes returns Result<Option<_>>, the Result is handled by 
 | 
					                    // containing a tuple...
 | 
				
			||||||
                    // the ? and we match on the Option
 | 
					                    (
 | 
				
			||||||
                    match load_bytes(pool, $name)? {
 | 
					                        // ...with one item for each repetition of $name
 | 
				
			||||||
                        Some(v) => v,
 | 
					                        $(
 | 
				
			||||||
                        None => return Ok(None)
 | 
					                            // load_bytes returns Result<Option<_>>, the Result is handled by 
 | 
				
			||||||
                    },
 | 
					                            // the ? and we match on the Option
 | 
				
			||||||
                )*
 | 
					                            match crate::kv::load_bytes($pool, $name).await? {
 | 
				
			||||||
 | 
					                                Some(v) => v,
 | 
				
			||||||
 | 
					                                None => return Ok(None)
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )*
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        })()
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) use load_bytes_multi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// macro_rules! load_multi {
 | 
					// macro_rules! load_multi {
 | 
				
			||||||
//     (
 | 
					//     (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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", ¬ification)?;
 | 
					        app_handle.emit("credentials-request", ¬ification)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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<(), ()> {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,22 +45,19 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
 | 
				
			|||||||
        lease.release();
 | 
					        lease.release();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // more lock-management
 | 
					    // 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
 | 
				
			||||||
        let app_session = state.session.read().await;
 | 
					    // this will result in an error popup to the user (see main hotkey handler)
 | 
				
			||||||
        // session should really be unlocked at this point, but if the frontend misbehaves
 | 
					    if use_base {
 | 
				
			||||||
        // (i.e. lies about unlocking) we could end up here with a locked session
 | 
					        let base_creds = state.get_aws_base().await?;
 | 
				
			||||||
        // this will result in an error popup to the user (see main hotkey handler)
 | 
					        cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
 | 
				
			||||||
        let (base_creds, session_creds) = app_session.try_get()?;
 | 
					        cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
 | 
				
			||||||
        if use_base {
 | 
					    }
 | 
				
			||||||
            cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
 | 
					    else {
 | 
				
			||||||
            cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
 | 
					        let session_creds = state.get_aws_session().await?;
 | 
				
			||||||
        }
 | 
					        cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
 | 
				
			||||||
        else {
 | 
					        cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
 | 
				
			||||||
            cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
 | 
					        cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
 | 
				
			||||||
            cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
 | 
					 | 
				
			||||||
            cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let res = match cmd.spawn() {
 | 
					    let res = match cmd.spawn() {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user