diff --git a/Cargo.lock b/Cargo.lock index 1ffacd6..2ffdf59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,7 +122,7 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitwarden" -version = "0.1.0" +version = "0.2.0" dependencies = [ "aes", "base64 0.13.0", diff --git a/bitwarden/Cargo.toml b/bitwarden/Cargo.toml index c9324e1..5eed625 100644 --- a/bitwarden/Cargo.toml +++ b/bitwarden/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitwarden" -version = "0.1.0" +version = "0.2.0" authors = ["Christoph Heiss "] edition = "2018" diff --git a/bitwarden/src/api.rs b/bitwarden/src/api.rs index 1ee1960..d3ab1ec 100644 --- a/bitwarden/src/api.rs +++ b/bitwarden/src/api.rs @@ -1,35 +1,31 @@ // SPDX-License-Identifier: MIT -use std::collections::HashMap; - -use chrono::{DateTime, Utc}; use reqwest::header::{self, HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::cipher::{CipherString, CipherSuite}; +use crate::api_definition::{ + PreloginRequest, PreloginResponse, LoginRequest, LoginResponse, SyncResponse +}; +use crate::cipher::CipherSuite; const AUTH_URL: &str = "https://identity.bitwarden.com/connect/token"; const BASE_URL: &str = "https://api.bitwarden.com"; -#[derive(Debug, failure::Fail)] +#[derive(Clone, Debug, failure::Fail)] pub enum ApiError { - #[fail(display = "prelogin failed: {}", 0)] - PreloginFailed(String), + #[fail(display = "network error: failed to retrieve {}", 0)] + NetworkError(String), - #[fail(display = "authentication failed: {}", 0)] - LoginFailed(String), + #[fail(display = "authentication failed: wrong username or password")] + LoginFailed, - #[fail(display = "failed to retrieve {}: {}", endpoint, error)] - RequestFailed { endpoint: String, error: String }, - - #[fail(display = "failed to write sync data: {}", 0)] - VaultDataWriteFailed(String), - - #[fail(display = "failed to read sync data: {}", 0)] - VaultDataReadFailed(String), + #[fail(display = "failed to sync vault: {}", 0)] + SyncFailed(String), } +pub type ApiResult = Result; + #[derive(Debug, Deserialize, Serialize)] pub struct AuthData { access_token: String, @@ -43,206 +39,23 @@ pub struct AuthData { pub cipher: CipherSuite, } -#[derive(Debug, Deserialize)] -struct PreloginResponseData { - #[serde(alias = "Kdf")] - kdf: u32, - #[serde(alias = "KdfIterations")] - kdf_iterations: u32, -} - -#[derive(Debug, Deserialize)] -struct LoginResponseData { - access_token: String, - expires_in: usize, - token_type: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Profile { - #[serde(alias = "Object")] - object: String, - #[serde(alias = "Id")] - pub uuid: Uuid, - #[serde(alias = "Name")] - pub name: String, - #[serde(alias = "Email")] - pub email: String, - #[serde(alias = "EmailVerified")] - pub email_verified: bool, - #[serde(alias = "Premium")] - pub premium: bool, - #[serde(alias = "MasterPasswordHint")] - pub master_password_hint: Option, - #[serde(alias = "Culture")] - pub language: String, - #[serde(alias = "TwoFactorEnabled")] - pub tfa_enabled: bool, - #[serde(alias = "Key")] - pub key: CipherString, - #[serde(alias = "PrivateKey")] - pub private_key: CipherString, - #[serde(alias = "SecurityStamp")] - pub security_stamp: String, - #[serde(alias = "Organizations")] - pub organizations: Vec, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct Folder { - #[serde(alias = "Object")] - object: String, - #[serde(alias = "Id")] - pub uuid: Uuid, - #[serde(alias = "Name")] - pub name: CipherString, - #[serde(alias = "RevisionDate")] - pub last_changed: DateTime, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CipherEntryFields { - #[serde(alias = "Type")] - pub type_: usize, - #[serde(alias = "Name")] - pub name: CipherString, - #[serde(alias = "Value")] - pub value: CipherString, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CipherEntryHistory { - #[serde(alias = "Password")] - pub password: String, - #[serde(alias = "LastUsedDate")] - pub last_used_date: DateTime, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CipherEntryUriMatch { - #[serde(alias = "Uri")] - pub uri: CipherString, - #[serde(alias = "Match")] - pub match_: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CipherEntryData { - #[serde(alias = "Uri")] - pub uri: Option, - #[serde(alias = "Uris")] - pub uris: Option>, - #[serde(alias = "Username")] - pub username: CipherString, - #[serde(alias = "Password")] - pub password: CipherString, - #[serde(alias = "PasswordRevisionDate")] - pub assword_last_changed: Option>, - #[serde(alias = "Totp")] - pub totp: Option, - #[serde(alias = "Name")] - pub name: CipherString, - #[serde(alias = "Notes")] - pub notes: Option, - #[serde(alias = "Fields")] - pub fields: Option>, - #[serde(alias = "PasswordHistory")] - pub password_history: Option>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CipherEntry { - #[serde(alias = "Object")] - object: String, - #[serde(alias = "CollectionIds")] - pub collection_ids: Vec, - #[serde(alias = "FolderId")] - pub folder_id: Option, - #[serde(alias = "Favorite")] - pub favorite: bool, - #[serde(alias = "Edit")] - pub edit: bool, - #[serde(alias = "Id")] - pub uuid: Uuid, - #[serde(alias = "OrganizationId")] - pub organization_id: Option, - #[serde(alias = "Type")] - pub type_: usize, - #[serde(alias = "Data")] - pub data: CipherEntryData, - #[serde(alias = "Name")] - pub name: CipherString, - #[serde(alias = "Notes")] - pub notes: Option, - #[serde(alias = "Login", skip)] - pub login: Option, - #[serde(alias = "Card")] - pub card: Option, - #[serde(alias = "Identity")] - pub identity: Option, - #[serde(alias = "SecureNote")] - pub secure_note: Option, - #[serde(alias = "Fields")] - pub fields: Option>, - #[serde(alias = "PasswordHistory")] - pub password_history: Option>, - #[serde(alias = "Attachments")] - pub attachments: Option, - #[serde(alias = "OrganizationUseTotp")] - pub organization_tfa: bool, - #[serde(alias = "RevisionDate")] - pub last_changed: DateTime, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct Domains { - // TODO -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct VaultData { - #[serde(alias = "Object")] - object: String, - #[serde(alias = "Profile")] - pub profile: Profile, - #[serde(alias = "Folders")] - pub folders: Vec, - #[serde(alias = "Collections")] - pub collections: Vec, - #[serde(alias = "Ciphers")] - pub ciphers: Vec, - #[serde(alias = "Domains", skip)] - domains: Option, -} - -#[derive(Debug)] -pub struct AppData { - pub auth: AuthData, - pub vault: VaultData, -} - fn perform_prelogin( client: &reqwest::blocking::Client, email: &str, -) -> Result { +) -> ApiResult { let url = format!("{}/accounts/prelogin", BASE_URL); - let mut data = HashMap::new(); - data.insert("email", email); - + let data = PreloginRequest { email }; let response = client .post(&url) .json(&data) .send() - .map_err(|e| ApiError::PreloginFailed(e.to_string()))?; + .map_err(|err| ApiError::NetworkError(err.to_string()))?; if response.status().is_success() { - let data: PreloginResponseData = response.json() - .map_err(|e| ApiError::PreloginFailed(e.to_string()))?; - - Ok(data) + response.json::().map_err(|_| ApiError::LoginFailed) } else { - Err(ApiError::PreloginFailed(format!("{:?}", response.status()))) + Err(ApiError::LoginFailed) } } @@ -250,46 +63,44 @@ fn perform_token_auth( client: &reqwest::blocking::Client, email: &str, cipher: &CipherSuite, -) -> Result { +) -> ApiResult { let device_id = Uuid::new_v4().to_hyphenated().to_string(); - let mut data = HashMap::new(); - data.insert("grant_type", "password"); - data.insert("username", email); - data.insert("scope", "api offline_access"); - data.insert("client_id", "connector"); - data.insert("deviceType", "3"); - data.insert("deviceIdentifier", &device_id); - data.insert("deviceName", "bwtui"); - data.insert("password", &cipher.master_key_hash); + let data = LoginRequest { + grant_type: "password", + username: email, + scope: "api offline_access", + client_id: "connector", + device_type: 3, + device_id: &device_id, + device_name: "bwtui", + password: &cipher.master_key_hash, + }; let response = client .post(AUTH_URL) .form(&data) .send() - .map_err(|e| ApiError::LoginFailed(e.to_string()))?; + .map_err(|err| ApiError::NetworkError(err.to_string()))?; if response.status().is_success() { - let data: LoginResponseData = response.json() - .map_err(|e| ApiError::LoginFailed(e.to_string()))?; - - Ok(data) + response.json::().map_err(|_| ApiError::LoginFailed) } else { - Err(ApiError::LoginFailed(format!("{:?}", response.status()))) + Err(ApiError::LoginFailed) } } -pub fn authenticate(email: &str, password: &str) -> Result { +pub fn authenticate(email: &str, password: &str) -> ApiResult { let client = reqwest::blocking::Client::new(); - let PreloginResponseData { + let PreloginResponse { kdf, kdf_iterations, } = perform_prelogin(&client, email)?; let cipher = CipherSuite::from(email, password, kdf_iterations); - let LoginResponseData { + let LoginResponse { access_token, expires_in, token_type, @@ -305,14 +116,9 @@ pub fn authenticate(email: &str, password: &str) -> Result { }) } -pub fn sync(auth_data: &AuthData) -> Result { +pub fn sync(auth_data: &AuthData) -> ApiResult { let url = format!("{}/sync", BASE_URL); - let map_reqwest_err = |e: reqwest::Error| ApiError::RequestFailed { - endpoint: url.clone(), - error: e.to_string(), - }; - let mut headers = HeaderMap::new(); let auth_header = format!("{} {}", auth_data.token_type, auth_data.access_token); headers.insert( @@ -320,21 +126,16 @@ pub fn sync(auth_data: &AuthData) -> Result { HeaderValue::from_str(&auth_header).unwrap(), ); - let client = reqwest::blocking::Client::builder() - .default_headers(headers) - .build() - .map_err(map_reqwest_err)?; - - let response = client.get(&url).send().map_err(map_reqwest_err)?; + let response = reqwest::blocking::Client::new() + .get(&url) + .headers(headers) + .send() + .map_err(|_| ApiError::NetworkError(url))?; if response.status().is_success() { - let data: VaultData = response.json().map_err(map_reqwest_err)?; - - Ok(data) + response.json::() + .map_err(|_| ApiError::SyncFailed("received invalid response".to_string())) } else { - Err(ApiError::RequestFailed { - endpoint: url.clone(), - error: format!("{:?}", response.status()), - }) + Err(ApiError::SyncFailed("server reject the request".to_string())) } } diff --git a/bitwarden/src/api_definition.rs b/bitwarden/src/api_definition.rs new file mode 100644 index 0000000..ca50156 --- /dev/null +++ b/bitwarden/src/api_definition.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::cipher::CipherString; + +#[derive(Debug, Serialize)] +pub(crate) struct PreloginRequest<'a> { + pub email: &'a str, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct PreloginResponse { + #[serde(alias = "Kdf")] + pub kdf: u32, + #[serde(alias = "KdfIterations")] + pub kdf_iterations: u32, +} + +#[derive(Debug, Serialize)] +pub(crate) struct LoginRequest<'a> { + pub grant_type: &'a str, + pub username: &'a str, + pub scope: &'a str, + pub client_id: &'a str, + #[serde(alias = "deviceType")] + pub device_type: usize, + #[serde(alias = "deviceIdentifier")] + pub device_id: &'a str, + #[serde(alias = "deviceName")] + pub device_name: &'a str, + pub password: &'a str, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct LoginResponse { + pub access_token: String, + pub expires_in: usize, + pub token_type: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Profile { + #[serde(alias = "Object")] + object: String, + #[serde(alias = "Id")] + pub uuid: Uuid, + #[serde(alias = "Name")] + pub name: String, + #[serde(alias = "Email")] + pub email: String, + #[serde(alias = "EmailVerified")] + pub email_verified: bool, + #[serde(alias = "Premium")] + pub premium: bool, + #[serde(alias = "MasterPasswordHint")] + pub master_password_hint: Option, + #[serde(alias = "Culture")] + pub language: String, + #[serde(alias = "TwoFactorEnabled")] + pub tfa_enabled: bool, + #[serde(alias = "Key")] + pub key: CipherString, + #[serde(alias = "PrivateKey")] + pub private_key: CipherString, + #[serde(alias = "SecurityStamp")] + pub security_stamp: String, + #[serde(alias = "Organizations")] + pub organizations: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Folder { + #[serde(alias = "Object")] + object: String, + #[serde(alias = "Id")] + pub uuid: Uuid, + #[serde(alias = "Name")] + pub name: CipherString, + #[serde(alias = "RevisionDate")] + pub last_changed: DateTime, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CipherEntryFields { + #[serde(alias = "Type")] + pub type_: usize, + #[serde(alias = "Name")] + pub name: CipherString, + #[serde(alias = "Value")] + pub value: CipherString, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CipherEntryHistory { + #[serde(alias = "Password")] + pub password: String, + #[serde(alias = "LastUsedDate")] + pub last_used_date: DateTime, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CipherEntryUriMatch { + #[serde(alias = "Uri")] + pub uri: CipherString, + #[serde(alias = "Match")] + pub match_: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CipherEntryData { + #[serde(alias = "Uri")] + pub uri: Option, + #[serde(alias = "Uris")] + pub uris: Option>, + #[serde(alias = "Username")] + pub username: CipherString, + #[serde(alias = "Password")] + pub password: CipherString, + #[serde(alias = "PasswordRevisionDate")] + pub assword_last_changed: Option>, + #[serde(alias = "Totp")] + pub totp: Option, + #[serde(alias = "Name")] + pub name: CipherString, + #[serde(alias = "Notes")] + pub notes: Option, + #[serde(alias = "Fields")] + pub fields: Option>, + #[serde(alias = "PasswordHistory")] + pub password_history: Option>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CipherEntry { + #[serde(alias = "Object")] + object: String, + #[serde(alias = "CollectionIds")] + pub collection_ids: Vec, + #[serde(alias = "FolderId")] + pub folder_id: Option, + #[serde(alias = "Favorite")] + pub favorite: bool, + #[serde(alias = "Edit")] + pub edit: bool, + #[serde(alias = "Id")] + pub uuid: Uuid, + #[serde(alias = "OrganizationId")] + pub organization_id: Option, + #[serde(alias = "Type")] + pub type_: usize, + #[serde(alias = "Data")] + pub data: CipherEntryData, + #[serde(alias = "Name")] + pub name: CipherString, + #[serde(alias = "Notes")] + pub notes: Option, + #[serde(alias = "Login", skip)] + pub login: Option, + #[serde(alias = "Card")] + pub card: Option, + #[serde(alias = "Identity")] + pub identity: Option, + #[serde(alias = "SecureNote")] + pub secure_note: Option, + #[serde(alias = "Fields")] + pub fields: Option>, + #[serde(alias = "PasswordHistory")] + pub password_history: Option>, + #[serde(alias = "Attachments")] + pub attachments: Option, + #[serde(alias = "OrganizationUseTotp")] + pub organization_tfa: bool, + #[serde(alias = "RevisionDate")] + pub last_changed: DateTime, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Domains { + // TODO +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SyncResponse { + #[serde(alias = "Object")] + object: String, + #[serde(alias = "Profile")] + pub profile: Profile, + #[serde(alias = "Folders")] + pub folders: Vec, + #[serde(alias = "Collections")] + pub collections: Vec, + #[serde(alias = "Ciphers")] + pub ciphers: Vec, + #[serde(alias = "Domains", skip)] + domains: Option, +} diff --git a/bitwarden/src/lib.rs b/bitwarden/src/lib.rs index 1e42905..89134cd 100644 --- a/bitwarden/src/lib.rs +++ b/bitwarden/src/lib.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pub mod api; +pub mod api_definition; pub mod cipher; pub use api::*; +pub use api_definition::*; diff --git a/src/login.rs b/src/login.rs index b3aff89..688b262 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,33 +1,38 @@ // SPDX-License-Identifier: MIT +use std::sync::mpsc; +use std::thread; + use cursive::direction::Orientation; use cursive::event::Event; use cursive::traits::*; -use cursive::views::{Dialog, EditView, LinearLayout, OnEventView, TextView}; -use cursive::Cursive; +use cursive::views::{Dialog, EditView, LinearLayout, OnEventView, TextContent, TextView}; +use cursive::{CbSink as CursiveSink, Cursive}; use bitwarden::cipher::CipherSuite; -use bitwarden::{self, ApiError, AppData, AuthData, VaultData}; +use bitwarden::{self, ApiError, AuthData}; -use crate::vault; +use crate::vault::{self, VaultData}; -pub fn ask(siv: &mut Cursive, default_email: Option) { - let email_edit = EditView::new() - .content(default_email.clone().unwrap_or_else(|| "".to_owned())) - .with_name("email"); +enum LoginProgress { + Decrypting, + Syncing, + Finished(VaultData), + Error(Option, ApiError), +} +type LoginProgressSink = mpsc::Sender; +type LoginProgressRecv = mpsc::Receiver; + +pub fn create(siv: &mut Cursive) { + let email_edit = EditView::new().with_name("email"); let email_view = OnEventView::new(email_edit).on_event(Event::CtrlChar('u'), |siv| { - if let Some(mut view) = siv.find_name::("email") { - view.set_content("")(siv); - } + siv.find_name::("email").unwrap().set_content("")(siv); }); let password_edit = EditView::new().secret().with_name("master_password"); - let password_view = OnEventView::new(password_edit).on_event(Event::CtrlChar('u'), |siv| { - if let Some(mut view) = siv.find_name::("master_password") { - view.set_content("")(siv); - } + siv.find_name::("master_password").unwrap().set_content("")(siv); }); let layout = LinearLayout::new(Orientation::Vertical) @@ -36,74 +41,120 @@ pub fn ask(siv: &mut Cursive, default_email: Option) { .child(TextView::new("master password:")) .child(password_view); - siv.add_layer( - Dialog::around(layout) - .title("bitwarden vault login") - .button("Ok", |siv| { - let email = siv - .call_on_name("email", |view: &mut EditView| view.get_content()) - .unwrap() - .to_string(); + let dialog = Dialog::around(layout) + .title("bitwarden vault login") + .button("Ok", on_login) + .min_width(60); - let password = siv - .call_on_name("master_password", |view: &mut EditView| view.get_content()) - .unwrap(); + siv.clear(); + siv.add_layer(dialog); - check_master_password(siv, email, &password); - }) - .min_width(60), - ); - - if default_email.is_some() { + if let Some(email) = siv.user_data().map(|data: &mut VaultData| data.sync.profile.email.clone()) { + siv.find_name::("email").unwrap().set_content(email)(siv); siv.focus_name("master_password").unwrap(); } } -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; +fn on_login(siv: &mut Cursive) { + let email = siv.find_name::("email").unwrap().get_content().to_string(); + let password = siv.find_name::("master_password").unwrap().get_content().to_string(); - auth.cipher = CipherSuite::from(&email, master_password, auth.kdf_iterations); + siv.set_autorefresh(true); + let progress_text = TextContent::new("authenticating ..."); + let progress_view = TextView::new_with_content(progress_text.clone()); + let progress_dialog = Dialog::around(progress_view).with_name("progress_dialog"); + siv.add_layer(progress_dialog); - if auth.cipher.set_decrypt_key(&vault.profile.key).is_err() { - siv.add_layer(Dialog::info("Wrong vault password")); + let vault_data = siv.take_user_data(); + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + if let Some(data) = vault_data { + decrypt_cached(tx, data, &email, &password); } else { - vault::show(siv, auth, vault); + sync_and_decrypt(tx, &email, &password); } + }); - return; - } + let cb_sink = siv.cb_sink().clone(); + thread::spawn(move || { + process_login(cb_sink, rx, progress_text); + }); +} - let auth_data = bitwarden::authenticate(&email, &master_password); +fn decrypt_cached( + sink: LoginProgressSink, + mut vault: VaultData, + email: &str, + master_password: &str +) { + sink.send(LoginProgress::Decrypting).unwrap(); + vault.auth.cipher = CipherSuite::from(&email, master_password, vault.auth.kdf_iterations); - match auth_data { - Ok(mut auth_data) => { - siv.pop_layer(); - - let vault = sync_vault_data(siv, &auth_data).unwrap(); - - if auth_data.cipher.set_decrypt_key(&vault.profile.key).is_err() { - siv.add_layer(Dialog::info("Wrong vault password")); - } else { - vault::show(siv, auth_data, vault); - } - } - Err(_) => siv.add_layer(Dialog::info("Wrong vault password")), + if vault.auth.cipher.set_decrypt_key(&vault.sync.profile.key).is_err() { + sink.send( + LoginProgress::Error(Some(vault), ApiError::LoginFailed) + ).unwrap(); + } else { + vault::decrypt(&mut vault); + sink.send(LoginProgress::Finished(vault)).unwrap(); } } -fn sync_vault_data(siv: &mut Cursive, auth_data: &AuthData) -> Result { - match bitwarden::sync(&auth_data) { - Ok(vault_data) => { - if let Err(err) = vault::save_app_data(&auth_data, &vault_data) { - siv.add_layer(Dialog::info(err.to_string())); - } +fn sync_and_decrypt(sink: LoginProgressSink, email: &str, master_password: &str) { + match bitwarden::authenticate(&email, &master_password) { + Ok(auth) => { + sink.send(LoginProgress::Syncing).unwrap(); + let vault = sync_vault_data(auth).unwrap(); - Ok(vault_data) - } - Err(err) => { - siv.add_layer(Dialog::info(err.to_string())); - Err(err) - } + decrypt_cached(sink, vault, email, master_password); + }, + Err(err) => sink.send(LoginProgress::Error(None, err)).unwrap(), } } + +fn process_login(sink: CursiveSink, recv: LoginProgressRecv, progress_text: TextContent) { + for progress in recv.iter() { + match progress { + LoginProgress::Decrypting => { + progress_text.set_content("decrypting ..."); + }, + + LoginProgress::Syncing => { + progress_text.set_content("syncing ..."); + }, + + LoginProgress::Finished(vault) => { + sink.send(Box::new(|siv| { + siv.set_user_data(vault); + vault::create(siv); + })).unwrap(); + break; + }, + + LoginProgress::Error(vault, err) => { + sink.send(Box::new(move |siv| { + siv.pop_layer(); + siv.find_name::("master_password").unwrap().set_content("")(siv); + + if let Some(vault) = vault { + siv.set_user_data(vault); + } + + siv.add_layer(Dialog::info(err.to_string())); + })).unwrap(); + break; + }, + } + } + + sink.send(Box::new(|siv| { + siv.set_autorefresh(false); + })).unwrap(); +} + +fn sync_vault_data(auth: AuthData) -> Result { + bitwarden::sync(&auth) + .map(|sync| VaultData { auth, sync, decrypted: Vec::new() }) + .map_err(|e| e.to_string()) + .and_then(|vault| vault::save_local_data(&vault).and(Ok(vault))) +} diff --git a/src/main.rs b/src/main.rs index fccbad5..3c1e110 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT +mod login; +mod vault; + use cursive::backends::termion::Backend; use cursive::Cursive; use cursive_buffered_backend::BufferedBackend; -mod login; -mod vault; - fn main() { // We need to use a buffered backend due to flickering with termion. let mut siv = Cursive::new(|| { @@ -16,13 +16,10 @@ fn main() { Box::new(buffered) }); - let mut email = None; - if let Ok(data) = vault::read_app_data() { - email = Some(data.vault.profile.email.clone()); - siv.set_user_data(data); + if let Ok(vault) = vault::read_local_data() { + siv.set_user_data(vault); } - login::ask(&mut siv, email); - + login::create(&mut siv); siv.run(); } diff --git a/src/vault.rs b/src/vault.rs index e32d6e4..b809069 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -18,7 +18,7 @@ use serde::Serialize; use unicase::UniCase; use bitwarden::cipher::CipherSuite; -use bitwarden::{ApiError, AppData, AuthData, CipherEntry, VaultData}; +use bitwarden::{AuthData, CipherEntry, SyncResponse}; #[derive(Copy, Clone, PartialEq, Eq, Hash)] enum VaultColumn { @@ -27,18 +27,24 @@ enum VaultColumn { Username, } -#[derive(Clone, Debug)] -struct VaultEntry { +#[derive(Clone)] +pub struct VaultEntry { name: UniCase, username: UniCase, password: String, favorite: String, } +pub struct VaultData { + pub auth: AuthData, + pub sync: SyncResponse, + pub decrypted: Vec, +} + type VaultTableView = TableView; impl VaultEntry { - fn from_cipher_entry(entry: &CipherEntry, cipher: &CipherSuite) -> Option { + pub fn from_cipher_entry(entry: &CipherEntry, cipher: &CipherSuite) -> Option { let favorite = if entry.favorite { "\u{2605}" } else { @@ -75,18 +81,12 @@ impl TableViewItem for VaultEntry { } } -pub fn show(siv: &mut Cursive, auth_data: AuthData, vault_data: VaultData) { - let items = vault_data - .ciphers - .iter() - .map(|c| VaultEntry::from_cipher_entry(&c, &auth_data.cipher).unwrap()) - .collect::>(); - +pub fn create(siv: &mut Cursive) { let mut table = VaultTableView::new() .column(VaultColumn::Favorite, "", |c| c.width(1)) .column(VaultColumn::Name, "Name", |c| c.width_percent(25)) .column(VaultColumn::Username, "Username", |c| c) - .items(items.clone()); + .items(siv.user_data::().unwrap().decrypted.clone()); table.sort_by(VaultColumn::Name, Ordering::Less); table.sort_by(VaultColumn::Favorite, Ordering::Less); @@ -154,7 +154,7 @@ pub fn show(siv: &mut Cursive, auth_data: AuthData, vault_data: VaultData) { let search_field = EditView::new() .on_edit(move |siv, content, _| { - fuzzy_match_on_edit(siv, &items, content); + fuzzy_match_on_edit(siv, content); }) .with_name("search_field") .full_width(); @@ -196,27 +196,33 @@ pub fn show(siv: &mut Cursive, auth_data: AuthData, vault_data: VaultData) { .child(TextView::new("^F: fuzzy-search")), ); - siv.add_layer(layout); + siv.clear(); + siv.add_fullscreen_layer(layout); siv.focus_name("password_table").unwrap(); } -fn fuzzy_match_on_edit(siv: &mut Cursive, items: &[VaultEntry], content: &str) { +pub fn decrypt(vault: &mut VaultData) { + vault.decrypted = vault.sync + .ciphers + .iter() + .map(|c| VaultEntry::from_cipher_entry(&c, &vault.auth.cipher).unwrap()) + .collect(); +} + +fn fuzzy_match_on_edit(siv: &mut Cursive, content: &str) { let mut table = siv.find_name::("password_table").unwrap(); + let items = &siv.user_data::().unwrap().decrypted; // If no search term is present, sort by name and favorite by default if content.is_empty() { - table.set_items(items.to_vec()); - + table.set_items(items.clone()); table.sort_by(VaultColumn::Name, Ordering::Less); table.sort_by(VaultColumn::Favorite, Ordering::Less); - return; } let matcher = SkimMatcherV2::default(); - - let mut items: Vec<(i64, VaultEntry)> = items - .iter() + let mut items: Vec<(i64, VaultEntry)> = items.iter() .map(|entry| { (matcher.fuzzy_match(&entry.name, content), entry.clone()) }) @@ -227,7 +233,6 @@ fn fuzzy_match_on_edit(siv: &mut Cursive, items: &[VaultEntry], content: &str) { items.sort_by(|a, b| a.0.cmp(&b.0).reverse()); let items = items.iter().map(|(_, entry)| entry.clone()).collect(); - table.set_selected_row(0); table.set_items(items); } @@ -246,50 +251,50 @@ fn get_app_data_path() -> Result { Ok(path) } -fn save_data_to(filename: &str, data: &T) -> Result<(), ApiError> +fn save_data_to(filename: &str, data: &T) -> Result<(), String> where T: Serialize, { let mut path = get_app_data_path() - .map_err(ApiError::VaultDataWriteFailed)?; + .map_err(|err| format!("failed to write sync data: {}", err))?; path.push(filename); let file = File::create(path) - .map_err(|e| ApiError::VaultDataWriteFailed(e.to_string()))?; + .map_err(|err| format!("failed to write sync data: {}", err))?; let writer = BufWriter::new(file); serde_json::to_writer(writer, data) - .map_err(|e| ApiError::VaultDataWriteFailed(e.to_string())) + .map_err(|err| format!("failed to write sync data: {}", err)) } -fn read_data_from(filename: &str) -> Result +fn read_data_from(filename: &str) -> Result where T: DeserializeOwned, { let mut path = get_app_data_path() - .map_err(ApiError::VaultDataReadFailed)?; + .map_err(|err| format!("failed to read sync data: {}", err))?; path.push(filename); let file = File::open(path) - .map_err(|e| ApiError::VaultDataReadFailed(e.to_string()))?; + .map_err(|err| format!("failed to read sync data: {}", err))?; let reader = BufReader::new(file); serde_json::from_reader(reader) - .map_err(|e| ApiError::VaultDataReadFailed(e.to_string())) + .map_err(|err| format!("failed to read sync data: {}", err)) } -pub fn read_app_data() -> Result { +pub fn read_local_data() -> Result { let auth = read_data_from("auth.json")?; - let vault = read_data_from("vault.json")?; + let sync = read_data_from("vault.json")?; - Ok(AppData { auth, vault }) + Ok(VaultData { auth, sync, decrypted: Vec::new() }) } -pub fn save_app_data(auth: &AuthData, vault: &VaultData) -> Result<(), ApiError> { - save_data_to("auth.json", auth)?; - save_data_to("vault.json", vault)?; +pub fn save_local_data(data: &VaultData) -> Result<(), String> { + save_data_to("auth.json", &data.auth)?; + save_data_to("vault.json", &data.sync)?; Ok(()) }