Implement offline support (withou re-sync support).

Now the local vault data is read on startup and the bitwarden API is
only contacted when no data is present.

Password validation now happens via proper error propagation from hmac
validation.

If you need/want to re-sync your vault data, vault.json must be deleted
manually from (probably) ~/.local/share/bwtui/.
This commit is contained in:
Christoph Heiss 2019-12-26 20:08:01 +01:00
parent be062c02b5
commit c4b5c7e074
Signed by: c8h4
GPG key ID: 73D5E7FDEE3DE49A
4 changed files with 111 additions and 34 deletions

View file

@ -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<Domains>,
}
#[derive(Debug)]
pub struct AppData {
pub auth: AuthData,
pub vault: VaultData,
}
fn perform_prelogin(client: &reqwest::Client, email: &str) -> Result<PreloginResponseData, ApiError> {
let url = format!("{}/accounts/prelogin", BASE_URL);
@ -286,7 +299,7 @@ pub fn authenticate(email: &str, password: &str) -> Result<AuthData, ApiError> {
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<AuthData, ApiError> {
access_token,
expires_in,
token_type,
kdf,
kdf_iterations,
cipher,
})
}
@ -336,7 +351,7 @@ pub fn sync(auth_data: &AuthData) -> Result<VaultData, ApiError> {
}
fn get_vault_data_path() -> Result<PathBuf, String> {
fn get_app_data_path() -> Result<PathBuf, String> {
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<PathBuf, String> {
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<T>(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<VaultData, ApiError> {
let path = get_vault_data_path()
fn read_data_from<T>(filename: &str) -> Result<T, ApiError>
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<VaultData, ApiError> {
.map_err(|e| ApiError::VaultDataReadFailed { error: e.to_string() })
}
pub fn read_app_data() -> Result<AppData, ApiError> {
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(())
}

View file

@ -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<u8>,
pub master_key_hash: String,
mac_key: Vec<u8>,
@ -23,14 +21,30 @@ pub struct CipherSuite {
decrypt_key: Option<Vec<u8>>,
}
#[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::<u8>::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<Vec<u8>> {
assert!(self.type_ == 2 && key.len() == 32 && mac.len() == 32);
pub fn decrypt_raw(&self, key: &[u8], mac: &[u8]) -> Result<Vec<u8>, 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::<Aes256, Pkcs7>::new_var(key, &self.iv).ok()?;
cipher.decrypt_vec(&self.ct).ok()
Cbc::<Aes256, Pkcs7>::new_var(key, &self.iv)
.map_err(|_| CipherError::InvalidKeyLength)?
.decrypt_vec(&self.ct)
.map_err(|_| CipherError::BlockModeError)
}
pub fn decrypt(&self, cipher: &CipherSuite) -> Option<String> {
self.decrypt_raw(cipher.decrypt_key.as_ref()?, &cipher.mac_key)
.ok()
.and_then(|s| String::from_utf8(s).ok())
}
}

View file

@ -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<String>) {
@ -70,21 +71,33 @@ pub fn ask(siv: &mut Cursive, default_email: Option<String>) {
fn check_master_password(siv: &mut Cursive, email: String, master_password: &str) {
if let Some(app_data) = siv.take_user_data::<AppData>() {
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::<VaultData>() {
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<VaultData, ApiError> {
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()));
}

View file

@ -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);
}