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:
parent
be062c02b5
commit
c4b5c7e074
54
src/api.rs
54
src/api.rs
|
@ -9,6 +9,7 @@ use chrono::{DateTime, Utc};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use reqwest::header::{self, HeaderMap, HeaderValue};
|
use reqwest::header::{self, HeaderMap, HeaderValue};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
use crate::cipher::{CipherSuite, CipherString};
|
use crate::cipher::{CipherSuite, CipherString};
|
||||||
|
|
||||||
|
@ -42,11 +43,16 @@ pub enum ApiError {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct AuthData {
|
pub struct AuthData {
|
||||||
access_token: String,
|
access_token: String,
|
||||||
expires_in: usize,
|
expires_in: usize,
|
||||||
token_type: String,
|
token_type: String,
|
||||||
|
|
||||||
|
kdf: usize,
|
||||||
|
pub kdf_iterations: usize,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
pub cipher: CipherSuite,
|
pub cipher: CipherSuite,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,6 +231,13 @@ pub struct VaultData {
|
||||||
domains: Option<Domains>,
|
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> {
|
fn perform_prelogin(client: &reqwest::Client, email: &str) -> Result<PreloginResponseData, ApiError> {
|
||||||
let url = format!("{}/accounts/prelogin", BASE_URL);
|
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 } =
|
let PreloginResponseData { kdf, kdf_iterations } =
|
||||||
perform_prelogin(&client, email)?;
|
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 } =
|
let LoginResponseData { access_token, expires_in, token_type } =
|
||||||
perform_token_auth(&client, email, &cipher)?;
|
perform_token_auth(&client, email, &cipher)?;
|
||||||
|
@ -295,6 +308,8 @@ pub fn authenticate(email: &str, password: &str) -> Result<AuthData, ApiError> {
|
||||||
access_token,
|
access_token,
|
||||||
expires_in,
|
expires_in,
|
||||||
token_type,
|
token_type,
|
||||||
|
kdf,
|
||||||
|
kdf_iterations,
|
||||||
cipher,
|
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")
|
let project_dirs = directories::ProjectDirs::from("", "", "bwtui")
|
||||||
.ok_or("could not retrieve data directory path")?;
|
.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();
|
let mut path = PathBuf::new();
|
||||||
path.push(target_dir);
|
path.push(target_dir);
|
||||||
path.push("data.json");
|
|
||||||
|
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn save_vault_data(vault_data: &VaultData) -> Result<(), ApiError> {
|
fn save_data_to<T>(filename: &str, data: &T) -> Result<(), ApiError>
|
||||||
let path = get_vault_data_path()
|
where T: Serialize
|
||||||
|
{
|
||||||
|
let mut path = get_app_data_path()
|
||||||
.map_err(|error| ApiError::VaultDataWriteFailed { error })?;
|
.map_err(|error| ApiError::VaultDataWriteFailed { error })?;
|
||||||
|
path.push(filename);
|
||||||
|
|
||||||
let file = File::create(path)
|
let file = File::create(path)
|
||||||
.map_err(|e| ApiError::VaultDataWriteFailed { error: e.to_string() })?;
|
.map_err(|e| ApiError::VaultDataWriteFailed { error: e.to_string() })?;
|
||||||
|
|
||||||
let writer = BufWriter::new(file);
|
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() })
|
.map_err(|e| ApiError::VaultDataWriteFailed { error: e.to_string() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn read_local_vault_data() -> Result<VaultData, ApiError> {
|
fn read_data_from<T>(filename: &str) -> Result<T, ApiError>
|
||||||
let path = get_vault_data_path()
|
where T: DeserializeOwned
|
||||||
|
{
|
||||||
|
let mut path = get_app_data_path()
|
||||||
.map_err(|error| ApiError::VaultDataReadFailed { error })?;
|
.map_err(|error| ApiError::VaultDataReadFailed { error })?;
|
||||||
|
path.push(filename);
|
||||||
|
|
||||||
let file = File::open(path)
|
let file = File::open(path)
|
||||||
.map_err(|e| ApiError::VaultDataReadFailed { error: e.to_string() })?;
|
.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() })
|
.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(())
|
||||||
|
}
|
||||||
|
|
|
@ -12,10 +12,8 @@ use serde::de::Visitor;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
pub struct CipherSuite {
|
pub struct CipherSuite {
|
||||||
kdf: usize,
|
|
||||||
kdf_iterations: usize,
|
|
||||||
|
|
||||||
master_key: Vec<u8>,
|
master_key: Vec<u8>,
|
||||||
pub master_key_hash: String,
|
pub master_key_hash: String,
|
||||||
mac_key: Vec<u8>,
|
mac_key: Vec<u8>,
|
||||||
|
@ -23,14 +21,30 @@ pub struct CipherSuite {
|
||||||
decrypt_key: Option<Vec<u8>>,
|
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 {
|
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) =
|
let (master_key, master_key_hash, mac_key) =
|
||||||
derive_master_key(email, password, kdf_iterations);
|
derive_master_key(email, password, kdf_iterations);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
kdf,
|
|
||||||
kdf_iterations,
|
|
||||||
master_key,
|
master_key,
|
||||||
master_key_hash,
|
master_key_hash,
|
||||||
mac_key,
|
mac_key,
|
||||||
|
@ -38,11 +52,14 @@ impl CipherSuite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_decrypt_key(&mut self, key: &CipherString) {
|
pub fn set_decrypt_key(&mut self, key: &CipherString) -> Result<(), CipherError> {
|
||||||
let key = key.decrypt_raw(&self.master_key, &self.mac_key).unwrap();
|
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.decrypt_key = Some(Vec::from(&key[0..32]));
|
||||||
self.mac_key = Vec::from(&key[32..64]);
|
self.mac_key = Vec::from(&key[32..64]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +120,10 @@ impl CipherString {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_valid_mac(&self, mac_key: &[u8]) -> bool {
|
fn is_valid_mac(&self, mac_key: &[u8]) -> bool {
|
||||||
|
if mac_key.len() != 32 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let mut message = Vec::<u8>::new();
|
let mut message = Vec::<u8>::new();
|
||||||
message.extend(&self.iv);
|
message.extend(&self.iv);
|
||||||
message.extend(&self.ct);
|
message.extend(&self.ct);
|
||||||
|
@ -113,22 +134,27 @@ impl CipherString {
|
||||||
mac.verify(&self.mac).is_ok()
|
mac.verify(&self.mac).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrypt_raw(&self, key: &[u8], mac: &[u8]) -> Option<Vec<u8>> {
|
pub fn decrypt_raw(&self, key: &[u8], mac: &[u8]) -> Result<Vec<u8>, CipherError> {
|
||||||
assert!(self.type_ == 2 && key.len() == 32 && mac.len() == 32);
|
if self.type_ != 2 {
|
||||||
|
return Err(CipherError::InvalidKeyType);
|
||||||
|
}
|
||||||
|
|
||||||
if !self.is_valid_mac(mac) {
|
if !self.is_valid_mac(mac) {
|
||||||
return None;
|
return Err(CipherError::InvalidMac);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently only one cipher (type 2) is supported/used by bitwarden:
|
// Currently only one cipher (type 2) is supported/used by bitwarden:
|
||||||
// pbkdf2/aes-cbc-256/hmac-sha256
|
// 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> {
|
pub fn decrypt(&self, cipher: &CipherSuite) -> Option<String> {
|
||||||
self.decrypt_raw(cipher.decrypt_key.as_ref()?, &cipher.mac_key)
|
self.decrypt_raw(cipher.decrypt_key.as_ref()?, &cipher.mac_key)
|
||||||
|
.ok()
|
||||||
.and_then(|s| String::from_utf8(s).ok())
|
.and_then(|s| String::from_utf8(s).ok())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
35
src/login.rs
35
src/login.rs
|
@ -6,8 +6,9 @@ use cursive::event::Event;
|
||||||
use cursive::traits::*;
|
use cursive::traits::*;
|
||||||
use cursive::views::{Dialog, EditView, LinearLayout, OnEventView, TextView};
|
use cursive::views::{Dialog, EditView, LinearLayout, OnEventView, TextView};
|
||||||
|
|
||||||
use crate::{api, vault};
|
use crate::api::{self, ApiError, AppData, AuthData, VaultData};
|
||||||
use api::{ApiError, AuthData, VaultData};
|
use crate::cipher::CipherSuite;
|
||||||
|
use crate::vault;
|
||||||
|
|
||||||
|
|
||||||
pub fn ask(siv: &mut Cursive, default_email: Option<String>) {
|
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) {
|
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);
|
let auth_data = api::authenticate(&email, &master_password);
|
||||||
|
|
||||||
match auth_data {
|
match auth_data {
|
||||||
Ok(mut auth_data) => {
|
Ok(mut auth_data) => {
|
||||||
siv.pop_layer();
|
siv.pop_layer();
|
||||||
|
|
||||||
let vault_data =
|
let vault = sync_vault_data(siv, &auth_data).unwrap();
|
||||||
if let Some(vault_data) = siv.take_user_data::<VaultData>() {
|
|
||||||
vault_data
|
|
||||||
} else {
|
|
||||||
sync_vault_data(siv, &auth_data).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
auth_data.cipher.set_decrypt_key(&vault_data.profile.key);
|
if let Err(_) = auth_data.cipher.set_decrypt_key(&vault.profile.key) {
|
||||||
vault::show(siv, auth_data, vault_data);
|
siv.add_layer(Dialog::info("Wrong vault password"));
|
||||||
|
} else {
|
||||||
|
vault::show(siv, auth_data, vault);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
siv.add_layer(Dialog::info("Wrong vault password"))
|
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> {
|
fn sync_vault_data(siv: &mut Cursive, auth_data: &AuthData) -> Result<VaultData, ApiError> {
|
||||||
match api::sync(&auth_data) {
|
match api::sync(&auth_data) {
|
||||||
Ok(vault_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()));
|
siv.add_layer(Dialog::info(err.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut email = None;
|
let mut email = None;
|
||||||
if let Ok(data) = api::read_local_vault_data() {
|
if let Ok(data) = api::read_app_data() {
|
||||||
email = Some(data.profile.email.clone());
|
email = Some(data.vault.profile.email.clone());
|
||||||
siv.set_user_data(data);
|
siv.set_user_data(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue