From ddd1005067c35fe98307ff78bb8868f153298c3c Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 8 May 2023 22:13:08 -0700 Subject: [PATCH] switch crypto implementation and add spinner --- package.json | 2 +- src-tauri/Cargo.lock | 123 ++++++++++++++++++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/src/credentials.rs | 143 +++++++++++++++++++++++------- src-tauri/src/errors.rs | 13 ++- src-tauri/src/state.rs | 2 +- src-tauri/tauri.conf.json | 2 +- src/App.svelte | 1 - src/ui/Spinner.svelte | 113 +++++++++++++++++++++++ src/views/EnterCredentials.svelte | 13 ++- src/views/Unlock.svelte | 14 ++- 11 files changed, 387 insertions(+), 43 deletions(-) create mode 100644 src/ui/Spinner.svelte diff --git a/package.json b/package.json index 9328533..7b9b6f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "creddy", - "version": "0.2.0", + "version": "0.2.1", "scripts": { "dev": "vite", "build": "vite build", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5eb8555..809d492 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "ahash" version = "0.7.6" @@ -60,13 +70,15 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "app" -version = "0.2.0" +version = "0.2.1" dependencies = [ + "argon2", "auto-launch", "aws-config", "aws-sdk-sts", "aws-smithy-types", "aws-types", + "chacha20poly1305", "clap", "dirs 5.0.1", "is-terminal", @@ -86,6 +98,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "argon2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "async-broadcast" version = "0.5.1" @@ -545,12 +568,27 @@ dependencies = [ "simd-abstraction", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block" version = "0.1.6" @@ -727,6 +765,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "3.2.25" @@ -962,6 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1996,6 +2070,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -2551,6 +2634,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl-probe" version = "0.1.5" @@ -2681,6 +2770,17 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.12" @@ -2872,6 +2972,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4456,6 +4567,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e953216..267e53b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.2.0" +version = "0.2.1" description = "A Tauri App" authors = ["you"] license = "" @@ -36,6 +36,8 @@ 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"] } [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/credentials.rs b/src-tauri/src/credentials.rs index 491fd0d..5a18aed 100644 --- a/src-tauri/src/credentials.rs +++ b/src-tauri/src/credentials.rs @@ -1,7 +1,25 @@ 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, + Version, + ParamsBuilder, + password_hash::rand_core::{RngCore, OsRng}, +}; +use chacha20poly1305::{ + XChaCha20Poly1305, + XNonce, + aead::{ + Aead, + AeadCore, + KeyInit, + Error as AeadError, + generic_array::GenericArray, + }, +}; use serde::{ Serialize, Deserialize, @@ -10,12 +28,7 @@ use serde::{ }; use serde::de::{self, Visitor}; use sqlx::SqlitePool; -use sodiumoxide::crypto::{ - pwhash, - pwhash::Salt, - secretbox, - secretbox::{Nonce, Key} -}; + use crate::errors::*; @@ -40,18 +53,17 @@ impl Session { None => {return Ok(Session::Empty);} }; - let salt_buf: [u8; 32] = row.salt - .try_into() - .map_err(|_e| SetupError::InvalidRecord)?; - let nonce_buf: [u8; 24] = row.nonce + let salt: [u8; 32] = row.salt .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: Salt(salt_buf), - nonce: Nonce(nonce_buf), + salt, + nonce, }; Ok(Session::Locked(creds)) } @@ -76,8 +88,8 @@ impl Session { pub struct LockedCredentials { pub access_key_id: String, pub secret_key_enc: Vec, - pub salt: Salt, - pub nonce: Nonce, + pub salt: [u8; 32], + pub nonce: XNonce, } impl LockedCredentials { @@ -88,8 +100,8 @@ impl LockedCredentials { ) .bind(&self.access_key_id) .bind(&self.secret_key_enc) - .bind(&self.salt.0[0..]) - .bind(&self.nonce.0[0..]) + .bind(&self.salt[..]) + .bind(&self.nonce[..]) .execute(pool) .await?; @@ -97,11 +109,10 @@ impl LockedCredentials { } pub fn decrypt(&self, passphrase: &str) -> Result { - let mut key_buf = [0; secretbox::KEYBYTES]; - // pretty sure this only fails if we're out of memory - pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &self.salt).unwrap(); - let decrypted = secretbox::open(&self.secret_key_enc, &self.nonce, &Key(key_buf)) - .map_err(|_| UnlockError::BadPassphrase)?; + let crypto = Crypto::new(passphrase, &self.salt) + .map_err(|e| CryptoError::Argon2(e))?; + let decrypted = crypto.decrypt(&self.nonce, &self.secret_key_enc) + .map_err(|e| CryptoError::Aead(e))?; let secret_access_key = String::from_utf8(decrypted) .map_err(|_| UnlockError::InvalidUtf8)?; @@ -122,21 +133,18 @@ pub struct BaseCredentials { } impl BaseCredentials { - pub fn encrypt(&self, passphrase: &str) -> LockedCredentials { - let salt = pwhash::gen_salt(); - let mut key_buf = [0; secretbox::KEYBYTES]; - pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap(); - let key = Key(key_buf); - let nonce = secretbox::gen_nonce(); + pub fn encrypt(&self, passphrase: &str) -> Result { + let salt = Crypto::salt(); + let crypto = Crypto::new(passphrase, &salt)?; + let (nonce, secret_key_enc) = crypto.encrypt(self.secret_access_key.as_bytes())?; - let secret_key_enc = secretbox::seal(self.secret_access_key.as_bytes(), &nonce, &key); - - LockedCredentials { + let locked = LockedCredentials { access_key_id: self.access_key_id.clone(), secret_key_enc, salt, nonce, - } + }; + Ok(locked) } } @@ -241,4 +249,73 @@ fn deserialize_expiration<'de, D>(deserializer: D) -> Result where D: Deserializer<'de> { deserializer.deserialize_str(DateTimeVisitor) -} \ No newline at end of file +} + + +struct Crypto { + cipher: XChaCha20Poly1305, +} + +impl Crypto { + /// Argon2 params rationale: + /// + /// m_cost is measured in KiB, so 128 * 1024 gives us 128MiB. + /// This should roughly double the memory usage of the application + /// while deriving the key. + /// + /// p_cost is irrelevant since (at present) there isn't any parallelism + /// implemented, so we leave it at 1. + /// + /// With the above m_cost, t_cost = 8 results in about 800ms to derive + /// 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 in an unoptimized build, + /// 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 { + let params = ParamsBuilder::new() + .m_cost(Self::MEM_COST) + .p_cost(1) + .t_cost(Self::TIME_COST) + .build() + .unwrap(); // only errors if the given params are invalid + + let hasher = Argon2::new( + Algorithm::Argon2id, + Version::V0x13, + params, + ); + + let mut key = [0; 32]; + hasher.hash_password_into(passphrase.as_bytes(), &salt, &mut key)?; + let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key)); + Ok(Crypto { cipher }) + } + + fn salt() -> [u8; 32] { + let mut salt = [0; 32]; + OsRng.fill_bytes(&mut salt); + salt + } + + fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec), AeadError> { + let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); + let ciphertext = self.cipher.encrypt(&nonce, data)?; + Ok((nonce, ciphertext)) + } + + fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result, AeadError> { + self.cipher.decrypt(nonce, data) + } +} diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index 7d014f0..ae91ac5 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -164,8 +164,8 @@ pub enum UnlockError { NotLocked, #[error("No saved credentials were found")] NoCredentials, - #[error("Invalid passphrase")] - BadPassphrase, + #[error(transparent)] + Crypto(#[from] CryptoError), #[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,6 +175,15 @@ 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 { diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 6ceae38..eaaaed2 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -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?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bb39c79..42c28d6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "creddy", - "version": "0.2.0" + "version": "0.2.1" }, "tauri": { "allowlist": { diff --git a/src/App.svelte b/src/App.svelte index 1a96d9f..4f0d979 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -15,7 +15,6 @@ invoke('get_config').then(config => $appState.config = config); listen('credentials-request', (tauriEvent) => { $appState.pendingRequests.put(tauriEvent.payload); }); -window.state = $appState; acceptRequest(); diff --git a/src/ui/Spinner.svelte b/src/ui/Spinner.svelte new file mode 100644 index 0000000..6931b3f --- /dev/null +++ b/src/ui/Spinner.svelte @@ -0,0 +1,113 @@ + + + + + +
+
+
+
+
+
diff --git a/src/views/EnterCredentials.svelte b/src/views/EnterCredentials.svelte index 9f8cf50..cfecaa8 100644 --- a/src/views/EnterCredentials.svelte +++ b/src/views/EnterCredentials.svelte @@ -7,6 +7,7 @@ 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; @@ -19,6 +20,7 @@ } } + let saving = false; async function save() { if (passphrase !== confirmPassphrase) { alert.shake(); @@ -27,6 +29,7 @@ let credentials = {AccessKeyId, SecretAccessKey}; try { + saving = true; await invoke('save_credentials', {credentials, passphrase}); if ($appState.currentRequest) { navigate('Approve'); @@ -47,6 +50,8 @@ if (alert) { alert.shake(); } + + saving = false; } } @@ -65,7 +70,13 @@ - + diff --git a/src/views/Unlock.svelte b/src/views/Unlock.svelte index 3609e4d..637da20 100644 --- a/src/views/Unlock.svelte +++ b/src/views/Unlock.svelte @@ -7,12 +7,14 @@ 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 @@ -21,6 +23,7 @@ } try { + saving = true; let r = await invoke('unlock', {passphrase}); $appState.credentialStatus = 'unlocked'; if ($appState.currentRequest) { @@ -43,6 +46,8 @@ if (alert) { alert.shake(); } + + saving = true; } } @@ -62,7 +67,14 @@ - + +