start working on generalizing credential logic
This commit is contained in:
193
src-tauri/src/credentials/mod.rs
Normal file
193
src-tauri/src/credentials/mod.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
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};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use crate::kv;
|
||||
|
||||
mod aws;
|
||||
pub use aws::{AwsBaseCredential, AwsSessionCredential};
|
||||
|
||||
|
||||
pub enum CredentialKind {
|
||||
AwsBase,
|
||||
AwsSession,
|
||||
}
|
||||
|
||||
|
||||
pub trait PersistentCredential {
|
||||
async fn load(crypt: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError>;
|
||||
async fn save(&self, crypt: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AppSession {
|
||||
Unlocked {
|
||||
salt: [u8; 32],
|
||||
crypto: Crypto,
|
||||
},
|
||||
Locked {
|
||||
salt: [u8; 32],
|
||||
verify_nonce: XNonce,
|
||||
verify_blob: Vec<u8>
|
||||
},
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl AppSession {
|
||||
pub fn new(passphrase: &str) -> Result<Self, CryptoError> {
|
||||
let salt = Crypto::salt();
|
||||
let crypto = Crypto::new(passphrase, &salt);
|
||||
Ok(Self::Unlocked {salt, crypto})
|
||||
}
|
||||
|
||||
pub fn unlock(self, passphrase: &str) -> Result<Self, UnlockError> {
|
||||
let (salt, nonce, blob) = match self {
|
||||
Self::Empty => return Err(UnlockError::NoCredentials),
|
||||
Self::Unlocked => return Err(UnlockError::NotLocked),
|
||||
Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob),
|
||||
};
|
||||
|
||||
let crypto = Crypto::new(passphrase, salt)
|
||||
.map_err(|e| CryptoError::Argon2(e))?;
|
||||
|
||||
// if passphrase is incorrect, this will fail
|
||||
let verify = crypto.decrypt(&nonce, &blob)?;
|
||||
|
||||
Ok(Self::Unlocked{crypto, salt})
|
||||
}
|
||||
|
||||
pub async fn load(pool: &SqlitePool) -> Result<Self, LoadKvError> {
|
||||
match kv::load_bytes_multi!(pool, "salt", "verify_nonce", "verify_blob").await? {
|
||||
Some((salt, verify_nonce, verify_blob)) => {
|
||||
Ok(Self::Locked {salt, verify_nonce, verify_blob}),
|
||||
},
|
||||
None => Ok(Self::Empty),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self, pool: &SqlitePool) -> Result<(), LockError> {
|
||||
let (salt, nonce, blob) = match self {
|
||||
Self::Unlocked {salt, crypto} => {
|
||||
let (nonce, blob) = crypto.encrypt(b"correct horse battery staple")
|
||||
.map_err(|e| CryptoError::Aead(e))?;
|
||||
(salt, nonce, blob)
|
||||
},
|
||||
Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob),
|
||||
// "saving" an empty session just means doing nothing
|
||||
Self::Empty => return Ok(()),
|
||||
};
|
||||
|
||||
kv::save(pool, "salt", salt).await?;
|
||||
kv::save(pool, "verify_nonce", nonce).await?;
|
||||
kv::save(pool, "verify_blob", blob).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), CryptoError> {
|
||||
let crypto = match self {
|
||||
Self::Empty => Err(GetCredentialsError::Empty),
|
||||
Self::Locked => Err(GetCredentialsError::Locked),
|
||||
Self::Unlocked {crypto, ..} => crypto,
|
||||
}?;
|
||||
let res = crypto.encrypt(data)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn try_decrypt(&self, nonce: XNonce, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let crypto = match self {
|
||||
Self::Empty => Err(GetCredentialsError::Empty),
|
||||
Self::Locked => Err(GetCredentialsError::Locked),
|
||||
Self::Unlocked {crypto, ..} => crypto,
|
||||
}?;
|
||||
let res = crypto.decrypt(nonce, data)?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub 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 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)
|
||||
.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<u8>), 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<Vec<u8>, AeadError> {
|
||||
self.cipher.decrypt(nonce, data)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user