1 Commits

Author SHA1 Message Date
663bbfa2f3 start switching crypto implementations 2023-05-07 21:14:07 -07:00
14 changed files with 87 additions and 351 deletions

View File

@ -1,6 +1,6 @@
{
"name": "creddy",
"version": "0.2.2",
"version": "0.2.0",
"scripts": {
"dev": "vite",
"build": "vite build",

95
src-tauri/Cargo.lock generated
View File

@ -68,6 +68,36 @@ version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "app"
version = "0.2.0"
dependencies = [
"argon2",
"auto-launch",
"aws-config",
"aws-sdk-sts",
"aws-smithy-types",
"aws-types",
"chacha20poly1305",
"clap",
"dirs 5.0.1",
"is-terminal",
"netstat2",
"once_cell",
"serde",
"serde_json",
"sodiumoxide",
"sqlx",
"strum",
"strum_macros",
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-single-instance",
"thiserror",
"tokio",
]
[[package]]
name = "argon2"
version = "0.5.0"
@ -945,37 +975,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "creddy"
version = "0.2.2"
dependencies = [
"argon2",
"auto-launch",
"aws-config",
"aws-sdk-sts",
"aws-smithy-types",
"aws-types",
"chacha20poly1305",
"clap",
"dirs 5.0.1",
"gag",
"netstat2",
"once_cell",
"serde",
"serde_json",
"sodiumoxide",
"sqlx",
"strum",
"strum_macros",
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-single-instance",
"thiserror",
"tokio",
"windows 0.48.0",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.8"
@ -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

@ -1,11 +1,11 @@
[package]
name = "creddy"
version = "0.2.2"
description = "A friendly AWS credentials manager"
authors = ["Joseph Montanaro"]
name = "app"
version = "0.2.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "creddy"
default-run = "app"
edition = "2021"
rust-version = "1.57"
@ -17,7 +17,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 +35,10 @@ strum_macros = "0.24"
auto-launch = "0.4.0"
dirs = "5.0"
clap = { version = "3.2.23", features = ["derive"] }
# 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"
is-terminal = "0.4.7"
argon2 = "0.5.0"
chacha20poly1305 = "0.10.1"
generic_array = "0.14.6"
[features]
# by default Tauri runs in production mode

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")
@ -98,11 +89,7 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
}
#[cfg(unix)]
{
let e = cmd.exec(); // never returns if successful
Err(ExecError::ExecutionFailed(e))?;
Ok(())
}
cmd.exec().map_err(|e| ExecError::ExecutionFailed(e))?;
#[cfg(windows)]
{

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

@ -1,7 +1,7 @@
use std::fmt::{self, Formatter};
use std::time::{SystemTime, UNIX_EPOCH};
use aws_smithy_types::date_time::{DateTime, Format};
use aws_smithy_types::date_time::{DateTime, Format};
use argon2::{
Argon2,
Algorithm,
@ -15,6 +15,7 @@ use chacha20poly1305::{
aead::{
Aead,
AeadCore,
Key,
KeyInit,
Error as AeadError,
generic_array::GenericArray,
@ -53,17 +54,18 @@ impl Session {
None => {return Ok(Session::Empty);}
};
let salt: [u8; 32] = row.salt
let salt_buf: [u8; 32] = row.salt
.try_into()
.map_err(|_e| SetupError::InvalidRecord)?;
let nonce_buf: [u8; 24] = row.nonce
.try_into()
.map_err(|_e| SetupError::InvalidRecord)?;
let nonce = XNonce::from_exact_iter(row.nonce.into_iter())
.ok_or(SetupError::InvalidRecord)?;
let creds = LockedCredentials {
access_key_id: row.access_key_id,
secret_key_enc: row.secret_key_enc,
salt,
nonce,
salt: Salt(salt_buf),
nonce: Nonce(nonce_buf),
};
Ok(Session::Locked(creds))
}
@ -100,8 +102,8 @@ impl LockedCredentials {
)
.bind(&self.access_key_id)
.bind(&self.secret_key_enc)
.bind(&self.salt[..])
.bind(&self.nonce[..])
.bind(&self.salt.0[0..])
.bind(&self.nonce.0[0..])
.execute(pool)
.await?;
@ -109,10 +111,9 @@ impl LockedCredentials {
}
pub fn decrypt(&self, passphrase: &str) -> Result<BaseCredentials, UnlockError> {
let crypto = Crypto::new(passphrase, &self.salt)
.map_err(|e| CryptoError::Argon2(e))?;
let crypto = Crypto::new(passphrase, &self.salt);
let decrypted = crypto.decrypt(&self.nonce, &self.secret_key_enc)
.map_err(|e| CryptoError::Aead(e))?;
.map_err(|_| UnlockError::BadPassphrase)?;
let secret_access_key = String::from_utf8(decrypted)
.map_err(|_| UnlockError::InvalidUtf8)?;
@ -133,10 +134,10 @@ pub struct BaseCredentials {
}
impl BaseCredentials {
pub fn encrypt(&self, passphrase: &str) -> Result<LockedCredentials, CryptoError> {
pub fn encrypt(&self, passphrase: &str) -> Result<CryptoError, LockedCredentials> {
let salt = Crypto::salt();
let crypto = Crypto::new(passphrase, &salt)?;
let (nonce, secret_key_enc) = crypto.encrypt(self.secret_access_key.as_bytes())?;
let (nonce, secret_key_enc) = crypto.encrypt(self.secret_access_key.as_bytes());
let locked = LockedCredentials {
access_key_id: self.access_key_id.clone(),
@ -270,24 +271,11 @@ impl Crypto {
/// a key on my (somewhat older) CPU. This is probably overkill, but
/// given that it should only have to happen ~once a day for most
/// usage, it should be acceptable.
#[cfg(not(debug_assertions))]
const MEM_COST: u32 = 128 * 1024;
#[cfg(not(debug_assertions))]
const TIME_COST: u32 = 8;
/// But since this takes a million years without optimizations,
/// we turn it way down in debug builds.
#[cfg(debug_assertions)]
const MEM_COST: u32 = 48 * 1024;
#[cfg(debug_assertions)]
const TIME_COST: u32 = 1;
fn new(passphrase: &str, salt: &[u8]) -> argon2::Result<Crypto> {
let params = ParamsBuilder::new()
.m_cost(Self::MEM_COST)
.m_cost(128 * 1024)
.p_cost(1)
.t_cost(Self::TIME_COST)
.t_cost(8)
.build()
.unwrap(); // only errors if the given params are invalid

View File

@ -164,8 +164,8 @@ pub enum UnlockError {
NotLocked,
#[error("No saved credentials were found")]
NoCredentials,
#[error(transparent)]
Crypto(#[from] CryptoError),
#[error("Invalid passphrase")]
BadPassphrase,
#[error("Data was found to be corrupt after decryption")]
InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
#[error("Database error: {0}")]
@ -175,15 +175,6 @@ pub enum UnlockError {
}
#[derive(Debug, ThisError, AsRefStr)]
pub enum CryptoError {
#[error(transparent)]
Argon2(#[from] argon2::Error),
#[error("Invalid passphrase")] // I think this is the only way decryption fails
Aead(#[from] chacha20poly1305::aead::Error),
}
// Errors encountered while trying to figure out who's on the other end of a request
#[derive(Debug, ThisError, AsRefStr)]
pub enum ClientInfoError {

View File

@ -3,20 +3,6 @@
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,
}
};
mod app;
mod cli;
@ -29,40 +15,18 @@ 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 +34,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

@ -48,7 +48,7 @@ impl AppState {
}
pub async fn new_creds(&self, base_creds: BaseCredentials, passphrase: &str) -> Result<(), UnlockError> {
let locked = base_creds.encrypt(passphrase)?;
let locked = base_creds.encrypt(passphrase);
// do this first so that if it fails we don't save bad credentials
self.new_session(base_creds).await?;
locked.save(&self.pool).await?;

View File

@ -7,8 +7,8 @@
"distDir": "../dist"
},
"package": {
"productName": "Creddy",
"version": "0.2.2"
"productName": "creddy",
"version": "0.2.0"
},
"tauri": {
"allowlist": {
@ -47,14 +47,6 @@
"timestampUrl": ""
}
},
"cli": {
"description": "A friendly AWS credentials manager",
"subcommands": {
"run": {
"description": "Launch Creddy"
}
}
},
"security": {
"csp": {
"default-src": ["'self'"],

View File

@ -15,6 +15,7 @@ invoke('get_config').then(config => $appState.config = config);
listen('credentials-request', (tauriEvent) => {
$appState.pendingRequests.put(tauriEvent.payload);
});
window.state = $appState;
acceptRequest();
</script>

View File

@ -1,113 +0,0 @@
<script>
export let color = 'base-content';
export let thickness = '2px';
let classes = '';
export { classes as class };
const colorVars = {
'primary': 'p',
'primary-focus': 'pf',
'primary-content': 'pc',
'secondary': 's',
'secondary-focus': 'sf',
'secondary-content': 'sc',
'accent': 'a',
'accent-focus': 'af',
'accent-content': 'ac',
'neutral': 'n',
'neutral-focus': 'nf',
'neutral-content': 'nc',
'base-100': 'b1',
'base-200': 'b2',
'base-300': 'b3',
'base-content': 'bc',
'info': 'in',
'info-content': 'inc',
'success': 'su',
'success-content': 'suc',
'warning': 'wa',
'warning-content': 'wac',
'error': 'er',
'error-content': 'erc',
}
let arcStyle = `border-width: ${thickness};`;
arcStyle += `border-color: hsl(var(--${colorVars[color]})) transparent transparent transparent;`;
</script>
<style>
#spinner {
position: relative;
animation: spin;
animation-duration: 1.5s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes spin {
50% { transform: rotate(225deg); }
100% { transform: rotate(360deg); }
}
.arc {
position: absolute;
top: 0;
left: 0;
border-radius: 9999px;
}
.arc-top {
transform: rotate(-45deg);
}
.arc-right {
animation: spin-right;
animation-duration: 3s;
animation-iteration-count: infinite;
}
.arc-bottom {
animation: spin-bottom;
animation-duration: 3s;
animation-iteration-count: infinite;
}
.arc-left {
animation: spin-left;
animation-duration: 3s;
animation-iteration-count: infinite;
}
@keyframes spin-top {
0% { transform: rotate(-45deg); }
50% { transform: rotate(315deg); }
100% { transform: rotate(-45deg); }
}
@keyframes spin-right {
0% { transform: rotate(45deg); }
50% { transform: rotate(315deg); }
100% { transform: rotate(405deg); }
}
@keyframes spin-bottom {
0% { transform: rotate(135deg); }
50% { transform: rotate(315deg); }
100% { transform: rotate(495deg); }
}
@keyframes spin-left {
0% { transform: rotate(225deg); }
50% { transform: rotate(315deg); }
100% { transform: rotate(585deg); }
}
</style>
<div id="spinner" class="w-6 h-6 {classes}">
<div class="arc arc-top w-full h-full" style={arcStyle}></div>
<div class="arc arc-right w-full h-full" style={arcStyle}></div>
<div class="arc arc-bottom w-full h-full" style={arcStyle}></div>
<div class="arc arc-left w-full h-full" style={arcStyle}></div>
</div>

View File

@ -7,7 +7,6 @@
import { navigate } from '../lib/routing.js';
import Link from '../ui/Link.svelte';
import ErrorAlert from '../ui/ErrorAlert.svelte';
import Spinner from '../ui/Spinner.svelte';
let errorMsg = null;
@ -20,7 +19,6 @@
}
}
let saving = false;
async function save() {
if (passphrase !== confirmPassphrase) {
alert.shake();
@ -29,7 +27,6 @@
let credentials = {AccessKeyId, SecretAccessKey};
try {
saving = true;
await invoke('save_credentials', {credentials, passphrase});
if ($appState.currentRequest) {
navigate('Approve');
@ -50,8 +47,6 @@
if (alert) {
alert.shake();
}
saving = false;
}
}
</script>
@ -70,13 +65,7 @@
<input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
<button type="submit" class="btn btn-primary">
{#if saving}
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
{:else}
Submit
{/if}
</button>
<input type="submit" class="btn btn-primary" />
<Link target="Home" hotkey="Escape">
<button class="btn btn-sm btn-outline w-full">Cancel</button>
</Link>

View File

@ -7,14 +7,12 @@
import { getRootCause } from '../lib/errors.js';
import ErrorAlert from '../ui/ErrorAlert.svelte';
import Link from '../ui/Link.svelte';
import Spinner from '../ui/Spinner.svelte';
let errorMsg = null;
let alert;
let passphrase = '';
let loadTime = 0;
let saving = false;
async function unlock() {
// The hotkey for navigating here from homepage is Enter, which also
// happens to trigger the form submit event
@ -23,7 +21,6 @@
}
try {
saving = true;
let r = await invoke('unlock', {passphrase});
$appState.credentialStatus = 'unlocked';
if ($appState.currentRequest) {
@ -46,8 +43,6 @@
if (alert) {
alert.shake();
}
saving = true;
}
}
@ -67,14 +62,7 @@
<!-- svelte-ignore a11y-autofocus -->
<input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
<button type="submit" class="btn btn-primary">
{#if saving}
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
{:else}
Submit
{/if}
</button>
<input type="submit" class="btn btn-primary" />
<Link target="Home" hotkey="Escape">
<button class="btn btn-outline btn-sm w-full">Cancel</button>
</Link>