2 Commits

Author SHA1 Message Date
c260e37e78 cryptography notes 2023-05-19 10:04:48 -07:00
7501253970 add separate binary for Windows CLI 2023-05-15 13:09:26 -07:00
11 changed files with 127 additions and 141 deletions

9
doc/cryptography.md Normal file
View File

@ -0,0 +1,9 @@
My original plan was to use [libsodium](https://doc.libsodium.org/) to handle encryption. However, the Rust bindings for libsodium are no longer actively maintained, which left me uncomfortable with using it. Instead, I switched to the [RustCrypto](https://github.com/RustCrypto) implementations of the same (or nearly the same) cryptographic primitives provided by libsodium.
Creddy makes use of two cryptographic primitives: A key-derivation function, which is currently `argon2id`, and a symmetric encryption algorithm, currently `XChaCha20Poly1305`.
* I chose `argon2id` because it's what libsodium uses, and because its difficulty parameters admit of very granular tuning.
* I chose `XChaCha20Poly1305` because it's _almost_ what libsodium uses - libsodium uses `XSalsa20Poly1305`, and it's my undersatnding that `XChaCha20Poly1305` is an evolution of the former. In both cases I use the eXtended variants, which make use of longer (24-byte) nonces than the non-X variants. This appealed to me because I wanted to be able to randomly generate a nonce every time I needed one, and I have seen [recommendations](https://latacora.micro.blog/2018/04/03/cryptographic-right-answers.html) that the 12-byte nonces used by the non-X variants are _juuust_ a touch small for that to be truly worry-free. The RustCrypto implementation of `XChaCha20Poly1305` has also been subject to a security audit, which is nice.
I tuned the `argon2id` parameters so that key-derivation would take ~800ms on my Ryzen 1600X. This is probably overkill, but I don't intend for key-derivation to be a frequent occurrence - no more than once a day, under normal circumstances. Taking in the neighborhood of 1 second seemed about the longest I could reasonably go.
**DISCLAIMER**: I am not a professional cryptographer, merely an interested amateur. While I've tried to be as careful as possible with selecting and making use of the cryptographic building blocks I've chosen here, there is always the possibility that I've screwed something up. If anyone would like to sponsor an _actual_ security review of Creddy by people who _actually_ know what they're doing instead of just what they've read on the internet, please let me know.

37
src-tauri/Cargo.lock generated
View File

@ -958,7 +958,7 @@ dependencies = [
"chacha20poly1305",
"clap",
"dirs 5.0.1",
"gag",
"is-terminal",
"netstat2",
"once_cell",
"serde",
@ -973,7 +973,6 @@ dependencies = [
"tauri-plugin-single-instance",
"thiserror",
"tokio",
"windows 0.48.0",
]
[[package]]
@ -1348,17 +1347,6 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "filedescriptor"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
dependencies = [
"libc",
"thiserror",
"winapi",
]
[[package]]
name = "filetime"
version = "0.2.21"
@ -1541,16 +1529,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "gag"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a713bee13966e9fbffdf7193af71d54a6b35a0bb34997cd6c9519ebeb5005972"
dependencies = [
"filedescriptor",
"tempfile",
]
[[package]]
name = "gdk"
version = "0.15.4"
@ -2121,6 +2099,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "is-terminal"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi 0.3.1",
"io-lifetimes",
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -4019,7 +4009,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7e0f1d535e7cbbbab43c82be4fc992b84f9156c16c160955617e0260ebc449"
dependencies = [
"anyhow",
"clap",
"cocoa",
"dirs-next",
"embed_plist",

View File

@ -9,6 +9,14 @@ default-run = "creddy"
edition = "2021"
rust-version = "1.57"
[[bin]]
name = "creddy_cli"
path = "src/bin/creddy_cli.rs"
[[bin]]
name = "creddy"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
@ -17,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["cli", "dialog", "os-all", "system-tray"] }
tauri = { version = "1.2", features = ["dialog", "os-all", "system-tray"] }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
sodiumoxide = "0.2.7"
tokio = { version = ">=1.19", features = ["full"] }
@ -35,11 +43,9 @@ strum_macros = "0.24"
auto-launch = "0.4.0"
dirs = "5.0"
clap = { version = "3.2.23", features = ["derive"] }
# is-terminal = "0.4.7"
is-terminal = "0.4.7"
argon2 = { version = "0.5.0", features = ["std"] }
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Console"] }
gag = "1.0"
[features]
# by default Tauri runs in production mode

22
src-tauri/conf/cli.wxs Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<DirectoryRef Id="INSTALLDIR">
<!-- Create a subdirectory for the console binary so that we can add it to PATH -->
<Directory Id="BinDir" Name="bin">
<Component Id="CliBinary" Guid="b6358c8e-504f-41fd-b14b-38af821dcd04">
<!-- Same name as the main executable, so that it can be invoked as just "creddy" -->
<File Id="Bin_Cli" Source="..\..\creddy_cli.exe" Name="creddy.exe" KeyPath="yes"/>
</Component>
</Directory>
</DirectoryRef>
<DirectoryRef Id="TARGETDIR">
<Component Id="AddToPath" Guid="b5fdaf7e-94f2-4aad-9144-aa3a8edfa675">
<Environment Id="CreddyInstallDir" Action="set" Name="PATH" Part="last" Permanent="no" Value="[BinDir]" />
</Component>
</DirectoryRef>
</Fragment>
</Wix>

View File

@ -0,0 +1,45 @@
// Windows isn't really amenable to having a single executable work as both a CLI and GUI app,
// so we just have a second binary for CLI usage
use creddy::{
cli,
errors::CliError,
};
use std::{
env,
process::{self, Command},
};
fn main() {
let args = cli::parser().get_matches();
if let Some(true) = args.get_one::<bool>("help") {
cli::parser().print_help().unwrap(); // if we can't print help we can't print an error
process::exit(0);
}
let res = match args.subcommand() {
None | Some(("run", _)) => launch_gui(),
Some(("show", m)) => cli::show(m),
Some(("exec", m)) => cli::exec(m),
_ => unreachable!(),
};
if let Err(e) = res {
eprintln!("Error: {e}");
}
}
fn launch_gui() -> Result<(), CliError> {
let mut path = env::current_exe()?;
path.pop(); // bin dir
// binaries are colocated in dev, but not in production
#[cfg(not(debug_assertions))]
path.pop(); // install dir
path.push("creddy.exe"); // exe in main install dir (aka gui exe)
Command::new(path).spawn()?;
Ok(())
}

View File

@ -13,6 +13,7 @@ use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
};
use crate::app;
use crate::config::AppConfig;
use crate::credentials::{BaseCredentials, SessionCredentials};
@ -22,16 +23,6 @@ use crate::errors::*;
pub fn parser() -> Command<'static> {
Command::new("creddy")
.about("A friendly AWS credentials manager")
// we don't want the default help handling because it early-exits,
// and on Windows we need to setup the console before we can output
.disable_help_flag(true)
.arg(
Arg::new("help")
.short('h')
.long("help")
.action(ArgAction::SetTrue)
.help("Print this message or the help of the given subcommand(s)")
)
.subcommand(
Command::new("run")
.about("Launch Creddy")

View File

@ -2,6 +2,7 @@ use std::net::Ipv4Addr;
use std::path::PathBuf;
use auto_launch::AutoLaunchBuilder;
use is_terminal::IsTerminal;
use serde::{Serialize, Deserialize};
use sqlx::SqlitePool;
@ -95,7 +96,7 @@ pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
path.push("Creddy");
std::fs::create_dir_all(&path)?;
if cfg!(debug_assertions) {
if cfg!(debug_assertions) && std::io::stdout().is_terminal() {
path.push("creddy.dev.db");
}
else {

View File

@ -228,6 +228,8 @@ pub enum CliError {
Request(#[from] RequestError),
#[error(transparent)]
Exec(#[from] ExecError),
#[error(transparent)]
Io(#[from] std::io::Error),
}

10
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,10 @@
pub mod app;
pub mod cli;
mod config;
mod credentials;
pub mod errors;
mod clientinfo;
mod ipc;
mod state;
mod server;
mod tray;

View File

@ -3,66 +3,21 @@
windows_subsystem = "windows"
)]
#[cfg(windows)]
use {
std::fs::File,
std::os::windows::io::FromRawHandle,
std::os::raw::c_void,
gag::Redirect,
windows::Win32::System::Console::{
AllocConsole,
AttachConsole,
GetStdHandle,
STD_OUTPUT_HANDLE,
STD_ERROR_HANDLE,
}
use creddy::{
app,
cli,
errors::ErrorPopup,
};
mod app;
mod cli;
mod config;
mod credentials;
mod errors;
mod clientinfo;
mod ipc;
mod state;
mod server;
mod tray;
use crate::errors::ErrorPopup;
use std::io::Write;
fn main() {
let args = cli::parser().get_matches();
let help = matches!(args.get_one::<bool>("help"), Some(true));
// This is the only case that doesn't need a console
if let None | Some(("run", _)) = args.subcommand() {
if !help {
let res = match cli::parser().get_matches().subcommand() {
None | Some(("run", _)) => {
app::run().error_popup("Creddy failed to start");
return;
}
}
// on Windows, need to do the whole allocate-a-console thing
#[cfg(windows)]
attach_console();
// let (out, err) = setup_console();
println!("Testing stdout");
// writeln!(&mut out, "Testing allocated file");
if help {
// if we can't print help, we can't print an error, so just panic
dbg!(args.get_one::<bool>("help"));
cli::parser().print_help().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3));
return;
}
let res = match args.subcommand() {
Ok(())
},
Some(("show", m)) => cli::show(m),
Some(("exec", m)) => cli::exec(m),
// clap ensures that subcommand is either None or run/show/exec
_ => unreachable!(),
};
@ -70,43 +25,3 @@ fn main() {
eprintln!("Error: {e}");
}
}
fn attach_console() {
unsafe { AttachConsole(u32::MAX); }
}
fn setup_console() -> (Redirect<File>, Redirect<File>) {
let (mut stdout, stderr) = unsafe {
AllocConsole();
// if we can't get handles to stdout/err, we can't display these errors,
// so there's no point in doing anything other than panicking
let stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE).unwrap();
let stderr_handle = GetStdHandle(STD_ERROR_HANDLE).unwrap();
let mut f = File::create("C:\\Users\\Joe\\Downloads\\debug.txt").unwrap();
writeln!(&mut f, "{stdout_handle:?}\n{:?}", stdout_handle.0 as *mut c_void).unwrap();
(
File::from_raw_handle(stdout_handle.0 as *mut c_void),
File::from_raw_handle(stderr_handle.0 as *mut c_void),
)
};
writeln!(&mut stdout, "Testing stdout before redirect")
.map_err(|e| log_err(e))
.unwrap();
(
Redirect::stdout(stdout).unwrap(),
Redirect::stderr(stderr).unwrap()
)
}
fn log_err(e: impl std::error::Error) {
let mut f = File::create("C:\\Users\\Joe\\Downloads\\log.txt").unwrap();
writeln!(&mut f, "{e}").unwrap();
}

View File

@ -7,7 +7,7 @@
"distDir": "../dist"
},
"package": {
"productName": "Creddy",
"productName": "creddy",
"version": "0.2.2"
},
"tauri": {
@ -44,14 +44,10 @@
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"cli": {
"description": "A friendly AWS credentials manager",
"subcommands": {
"run": {
"description": "Launch Creddy"
"timestampUrl": "",
"wix": {
"fragmentPaths": ["conf/cli.wxs"],
"componentRefs": ["CliBinary", "AddToPath"]
}
}
},