diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5eb8555..a84289d 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" @@ -62,11 +72,13 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" 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", @@ -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..7d2924d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,6 +36,9 @@ auto-launch = "0.4.0" dirs = "5.0" clap = { version = "3.2.23", features = ["derive"] } 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 diff --git a/src-tauri/src/credentials.rs b/src-tauri/src/credentials.rs index 491fd0d..f15e417 100644 --- a/src-tauri/src/credentials.rs +++ b/src-tauri/src/credentials.rs @@ -2,6 +2,25 @@ use std::fmt::{self, Formatter}; use std::time::{SystemTime, UNIX_EPOCH}; 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, + Key, + KeyInit, + Error as AeadError, + generic_array::GenericArray, + }, +}; use serde::{ Serialize, Deserialize, @@ -10,12 +29,7 @@ use serde::{ }; use serde::de::{self, Visitor}; use sqlx::SqlitePool; -use sodiumoxide::crypto::{ - pwhash, - pwhash::Salt, - secretbox, - secretbox::{Nonce, Key} -}; + use crate::errors::*; @@ -76,8 +90,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 { @@ -97,10 +111,8 @@ 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)) + let crypto = Crypto::new(passphrase, &self.salt); + let decrypted = crypto.decrypt(&self.nonce, &self.secret_key_enc) .map_err(|_| UnlockError::BadPassphrase)?; let secret_access_key = String::from_utf8(decrypted) .map_err(|_| UnlockError::InvalidUtf8)?; @@ -122,21 +134,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 +250,60 @@ 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. + fn new(passphrase: &str, salt: &[u8]) -> argon2::Result { + let params = ParamsBuilder::new() + .m_cost(128 * 1024) + .p_cost(1) + .t_cost(8) + .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) + } +}