diff --git a/src/api.rs b/src/api.rs index 92fa526..6c6590e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc}; use uuid::Uuid; use reqwest::header::{self, HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize}; +use serde::de::DeserializeOwned; use crate::cipher::{CipherSuite, CipherString}; @@ -42,11 +43,16 @@ pub enum ApiError { }, } +#[derive(Debug, Deserialize, Serialize)] pub struct AuthData { access_token: String, expires_in: usize, token_type: String, + kdf: usize, + pub kdf_iterations: usize, + + #[serde(skip)] pub cipher: CipherSuite, } @@ -225,6 +231,13 @@ pub struct VaultData { domains: Option, } +#[derive(Debug)] +pub struct AppData { + pub auth: AuthData, + pub vault: VaultData, +} + + fn perform_prelogin(client: &reqwest::Client, email: &str) -> Result { let url = format!("{}/accounts/prelogin", BASE_URL); @@ -286,7 +299,7 @@ pub fn authenticate(email: &str, password: &str) -> Result { let PreloginResponseData { kdf, kdf_iterations } = perform_prelogin(&client, email)?; - let cipher = CipherSuite::from(email, password, kdf, kdf_iterations); + let cipher = CipherSuite::from(email, password, kdf_iterations); let LoginResponseData { access_token, expires_in, token_type } = perform_token_auth(&client, email, &cipher)?; @@ -295,6 +308,8 @@ pub fn authenticate(email: &str, password: &str) -> Result { access_token, expires_in, token_type, + kdf, + kdf_iterations, cipher, }) } @@ -336,7 +351,7 @@ pub fn sync(auth_data: &AuthData) -> Result { } -fn get_vault_data_path() -> Result { +fn get_app_data_path() -> Result { let project_dirs = directories::ProjectDirs::from("", "", "bwtui") .ok_or("could not retrieve data directory path")?; @@ -347,28 +362,33 @@ fn get_vault_data_path() -> Result { let mut path = PathBuf::new(); path.push(target_dir); - path.push("data.json"); Ok(path) } -pub fn save_vault_data(vault_data: &VaultData) -> Result<(), ApiError> { - let path = get_vault_data_path() +fn save_data_to(filename: &str, data: &T) -> Result<(), ApiError> + where T: Serialize +{ + let mut path = get_app_data_path() .map_err(|error| ApiError::VaultDataWriteFailed { error })?; + path.push(filename); let file = File::create(path) .map_err(|e| ApiError::VaultDataWriteFailed { error: e.to_string() })?; let writer = BufWriter::new(file); - serde_json::to_writer(writer, vault_data) + serde_json::to_writer(writer, data) .map_err(|e| ApiError::VaultDataWriteFailed { error: e.to_string() }) } -pub fn read_local_vault_data() -> Result { - let path = get_vault_data_path() +fn read_data_from(filename: &str) -> Result + where T: DeserializeOwned +{ + let mut path = get_app_data_path() .map_err(|error| ApiError::VaultDataReadFailed { error })?; + path.push(filename); let file = File::open(path) .map_err(|e| ApiError::VaultDataReadFailed { error: e.to_string() })?; @@ -378,3 +398,21 @@ pub fn read_local_vault_data() -> Result { .map_err(|e| ApiError::VaultDataReadFailed { error: e.to_string() }) } + +pub fn read_app_data() -> Result { + let auth = read_data_from("auth.json")?; + let vault = read_data_from("vault.json")?; + + Ok(AppData { + auth, + vault, + }) +} + + +pub fn save_app_data(auth: &AuthData, vault: &VaultData) -> Result<(), ApiError> { + save_data_to("auth.json", auth)?; + save_data_to("vault.json", vault)?; + + Ok(()) +} diff --git a/src/cipher.rs b/src/cipher.rs index 123185c..666ea7d 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -12,10 +12,8 @@ use serde::de::Visitor; use sha2::Sha256; +#[derive(Debug, Default)] pub struct CipherSuite { - kdf: usize, - kdf_iterations: usize, - master_key: Vec, pub master_key_hash: String, mac_key: Vec, @@ -23,14 +21,30 @@ pub struct CipherSuite { decrypt_key: Option>, } +#[derive(Debug, failure::Fail)] +pub enum CipherError { + #[fail(display = "failed to verify key")] + InvalidMac, + + #[fail(display = "only type 2 ciphers are supported")] + InvalidKeyType, + + #[fail(display = "key length must be exactly 32 bytes")] + InvalidKeyLength, + + #[fail(display = "block mode error")] + BlockModeError, + + #[fail(display = "failed to set decrypt key: {:?}", 0)] + DecryptionKeyError(String), +} + impl CipherSuite { - pub fn from(email: &str, password: &str, kdf: usize, kdf_iterations: usize) -> Self { + pub fn from(email: &str, password: &str, kdf_iterations: usize) -> Self { let (master_key, master_key_hash, mac_key) = derive_master_key(email, password, kdf_iterations); Self { - kdf, - kdf_iterations, master_key, master_key_hash, mac_key, @@ -38,11 +52,14 @@ impl CipherSuite { } } - pub fn set_decrypt_key(&mut self, key: &CipherString) { - let key = key.decrypt_raw(&self.master_key, &self.mac_key).unwrap(); + pub fn set_decrypt_key(&mut self, key: &CipherString) -> Result<(), CipherError> { + let key = key.decrypt_raw(&self.master_key, &self.mac_key) + .map_err(|e| CipherError::DecryptionKeyError(e.to_string()))?; self.decrypt_key = Some(Vec::from(&key[0..32])); self.mac_key = Vec::from(&key[32..64]); + + Ok(()) } } @@ -103,6 +120,10 @@ impl CipherString { } fn is_valid_mac(&self, mac_key: &[u8]) -> bool { + if mac_key.len() != 32 { + return false; + } + let mut message = Vec::::new(); message.extend(&self.iv); message.extend(&self.ct); @@ -113,22 +134,27 @@ impl CipherString { mac.verify(&self.mac).is_ok() } - pub fn decrypt_raw(&self, key: &[u8], mac: &[u8]) -> Option> { - assert!(self.type_ == 2 && key.len() == 32 && mac.len() == 32); + pub fn decrypt_raw(&self, key: &[u8], mac: &[u8]) -> Result, CipherError> { + if self.type_ != 2 { + return Err(CipherError::InvalidKeyType); + } if !self.is_valid_mac(mac) { - return None; + return Err(CipherError::InvalidMac); } // Currently only one cipher (type 2) is supported/used by bitwarden: // pbkdf2/aes-cbc-256/hmac-sha256 - let cipher = Cbc::::new_var(key, &self.iv).ok()?; - cipher.decrypt_vec(&self.ct).ok() + Cbc::::new_var(key, &self.iv) + .map_err(|_| CipherError::InvalidKeyLength)? + .decrypt_vec(&self.ct) + .map_err(|_| CipherError::BlockModeError) } pub fn decrypt(&self, cipher: &CipherSuite) -> Option { self.decrypt_raw(cipher.decrypt_key.as_ref()?, &cipher.mac_key) + .ok() .and_then(|s| String::from_utf8(s).ok()) } } diff --git a/src/login.rs b/src/login.rs index 4d0b208..e0cd72d 100644 --- a/src/login.rs +++ b/src/login.rs @@ -6,8 +6,9 @@ use cursive::event::Event; use cursive::traits::*; use cursive::views::{Dialog, EditView, LinearLayout, OnEventView, TextView}; -use crate::{api, vault}; -use api::{ApiError, AuthData, VaultData}; +use crate::api::{self, ApiError, AppData, AuthData, VaultData}; +use crate::cipher::CipherSuite; +use crate::vault; pub fn ask(siv: &mut Cursive, default_email: Option) { @@ -70,21 +71,33 @@ pub fn ask(siv: &mut Cursive, default_email: Option) { fn check_master_password(siv: &mut Cursive, email: String, master_password: &str) { + if let Some(app_data) = siv.take_user_data::() { + let AppData { mut auth, vault } = app_data; + + auth.cipher = CipherSuite::from(&email, master_password, auth.kdf_iterations); + + if let Err(_) = auth.cipher.set_decrypt_key(&vault.profile.key) { + siv.add_layer(Dialog::info("Wrong vault password")); + } else { + vault::show(siv, auth, vault); + } + + return; + } + let auth_data = api::authenticate(&email, &master_password); match auth_data { Ok(mut auth_data) => { siv.pop_layer(); - let vault_data = - if let Some(vault_data) = siv.take_user_data::() { - vault_data - } else { - sync_vault_data(siv, &auth_data).unwrap() - }; + let vault = sync_vault_data(siv, &auth_data).unwrap(); - auth_data.cipher.set_decrypt_key(&vault_data.profile.key); - vault::show(siv, auth_data, vault_data); + if let Err(_) = auth_data.cipher.set_decrypt_key(&vault.profile.key) { + siv.add_layer(Dialog::info("Wrong vault password")); + } else { + vault::show(siv, auth_data, vault); + } }, Err(_) => { siv.add_layer(Dialog::info("Wrong vault password")) @@ -96,7 +109,7 @@ fn check_master_password(siv: &mut Cursive, email: String, master_password: &str fn sync_vault_data(siv: &mut Cursive, auth_data: &AuthData) -> Result { match api::sync(&auth_data) { Ok(vault_data) => { - if let Err(err) = api::save_vault_data(&vault_data) { + if let Err(err) = api::save_app_data(&auth_data, &vault_data) { siv.add_layer(Dialog::info(err.to_string())); } diff --git a/src/main.rs b/src/main.rs index 921e681..b24702e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,8 +20,8 @@ fn main() { }); let mut email = None; - if let Ok(data) = api::read_local_vault_data() { - email = Some(data.profile.email.clone()); + if let Ok(data) = api::read_app_data() { + email = Some(data.vault.profile.email.clone()); siv.set_user_data(data); }