1 Commits

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

View File

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

2
src-tauri/Cargo.lock generated
View File

@ -70,7 +70,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]] [[package]]
name = "app" name = "app"
version = "0.2.1" version = "0.2.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"auto-launch", "auto-launch",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "app" name = "app"
version = "0.2.1" version = "0.2.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
license = "" license = ""
@ -36,8 +36,9 @@ auto-launch = "0.4.0"
dirs = "5.0" dirs = "5.0"
clap = { version = "3.2.23", features = ["derive"] } clap = { version = "3.2.23", features = ["derive"] }
is-terminal = "0.4.7" is-terminal = "0.4.7"
argon2 = { version = "0.5.0", features = ["std"] } argon2 = "0.5.0"
chacha20poly1305 = { version = "0.10.1", features = ["std"] } chacha20poly1305 = "0.10.1"
generic_array = "0.14.6"
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode

View File

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

View File

@ -15,6 +15,7 @@ use chacha20poly1305::{
aead::{ aead::{
Aead, Aead,
AeadCore, AeadCore,
Key,
KeyInit, KeyInit,
Error as AeadError, Error as AeadError,
generic_array::GenericArray, generic_array::GenericArray,
@ -53,17 +54,18 @@ impl Session {
None => {return Ok(Session::Empty);} 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() .try_into()
.map_err(|_e| SetupError::InvalidRecord)?; .map_err(|_e| SetupError::InvalidRecord)?;
let nonce = XNonce::from_exact_iter(row.nonce.into_iter())
.ok_or(SetupError::InvalidRecord)?;
let creds = LockedCredentials { let creds = LockedCredentials {
access_key_id: row.access_key_id, access_key_id: row.access_key_id,
secret_key_enc: row.secret_key_enc, secret_key_enc: row.secret_key_enc,
salt, salt: Salt(salt_buf),
nonce, nonce: Nonce(nonce_buf),
}; };
Ok(Session::Locked(creds)) Ok(Session::Locked(creds))
} }
@ -100,8 +102,8 @@ impl LockedCredentials {
) )
.bind(&self.access_key_id) .bind(&self.access_key_id)
.bind(&self.secret_key_enc) .bind(&self.secret_key_enc)
.bind(&self.salt[..]) .bind(&self.salt.0[0..])
.bind(&self.nonce[..]) .bind(&self.nonce.0[0..])
.execute(pool) .execute(pool)
.await?; .await?;
@ -109,10 +111,9 @@ impl LockedCredentials {
} }
pub fn decrypt(&self, passphrase: &str) -> Result<BaseCredentials, UnlockError> { pub fn decrypt(&self, passphrase: &str) -> Result<BaseCredentials, UnlockError> {
let crypto = Crypto::new(passphrase, &self.salt) let crypto = Crypto::new(passphrase, &self.salt);
.map_err(|e| CryptoError::Argon2(e))?;
let decrypted = crypto.decrypt(&self.nonce, &self.secret_key_enc) 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) let secret_access_key = String::from_utf8(decrypted)
.map_err(|_| UnlockError::InvalidUtf8)?; .map_err(|_| UnlockError::InvalidUtf8)?;
@ -133,10 +134,10 @@ pub struct BaseCredentials {
} }
impl 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 salt = Crypto::salt();
let crypto = Crypto::new(passphrase, &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 { let locked = LockedCredentials {
access_key_id: self.access_key_id.clone(), 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 /// 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 /// given that it should only have to happen ~once a day for most
/// usage, it should be acceptable. /// 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> { fn new(passphrase: &str, salt: &[u8]) -> argon2::Result<Crypto> {
let params = ParamsBuilder::new() let params = ParamsBuilder::new()
.m_cost(Self::MEM_COST) .m_cost(128 * 1024)
.p_cost(1) .p_cost(1)
.t_cost(Self::TIME_COST) .t_cost(8)
.build() .build()
.unwrap(); // only errors if the given params are invalid .unwrap(); // only errors if the given params are invalid

View File

@ -164,8 +164,8 @@ pub enum UnlockError {
NotLocked, NotLocked,
#[error("No saved credentials were found")] #[error("No saved credentials were found")]
NoCredentials, NoCredentials,
#[error(transparent)] #[error("Invalid passphrase")]
Crypto(#[from] CryptoError), BadPassphrase,
#[error("Data was found to be corrupt after decryption")] #[error("Data was found to be corrupt after decryption")]
InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
#[error("Database error: {0}")] #[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 // Errors encountered while trying to figure out who's on the other end of a request
#[derive(Debug, ThisError, AsRefStr)] #[derive(Debug, ThisError, AsRefStr)]
pub enum ClientInfoError { pub enum ClientInfoError {

View File

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

View File

@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "creddy", "productName": "creddy",
"version": "0.2.1" "version": "0.2.0"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

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

View File

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