Split out the Bitwarden API into a separate sub-crate.

This commit is contained in:
Christoph Heiss 2020-11-24 01:47:34 +01:00
parent bc76050d3a
commit 4063542e71
Signed by: c8h4
GPG key ID: 8D9166DCF6A28E57
14 changed files with 2349 additions and 803 deletions

4
.gitignore vendored
View file

@ -1,2 +1,2 @@
/target
**/*.rs.bk
target/
bitwarden/target/

1376
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,39 +10,24 @@ repository = "https://github.com/christoph-heiss/bwtui"
description = "terminal-based vault browser for bitwarden"
[dependencies]
aes = "0.3.2"
base64 = "0.12.0"
block-modes = "0.3.3"
clipboard = "0.5.0"
cursive_buffered_backend = "0.3.1"
cursive_table_view = "0.12.0"
cursive_buffered_backend = "0.4.0"
directories = "2.0.2"
failure = "0.1.7"
fuzzy-matcher = "0.3.4"
hkdf = "0.8.0"
hmac = "0.7.1"
pbkdf2 = "0.3.0"
serde_json = "1.0.51"
sha2 = "0.8.1"
serde_json = "1.0.53"
unicase = "2.6.0"
[dependencies.chrono]
version = "0.4.11"
features = ["serde"]
[dependencies.cursive]
version = "0.14.0"
version = "0.15.0"
default-features = false
features = ["termion-backend"]
[dependencies.reqwest]
version = "0.10.4"
features = ["blocking", "json"]
[dependencies.cursive_table_view]
git = "https://github.com/BonsaiDen/cursive_table_view.git"
[dependencies.serde]
version = "1.0.106"
version = "1.0.111"
features = ["derive"]
[dependencies.uuid]
version = "0.8.1"
features = ["v4", "serde"]
[dependencies.bitwarden]
path = "bitwarden"

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Christoph Heiss
Copyright (c) 2019-2020 Christoph Heiss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

1489
bitwarden/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

32
bitwarden/Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "bitwarden"
version = "0.1.0"
authors = ["Christoph Heiss <me@christoph-heiss.me>"]
edition = "2018"
[dependencies]
aes = "0.3.2"
base64 = "0.12.1"
block-modes = "0.3.3"
failure = "0.1.8"
hkdf = "0.8.0"
hmac = "0.7.1"
pbkdf2 = "0.3.0"
sha2 = "0.8.2"
[dependencies.chrono]
version = "0.4.11"
features = ["serde"]
[dependencies.reqwest]
version = "0.10.6"
features = ["blocking", "json"]
[dependencies.serde]
version = "1.0.111"
features = ["derive"]
[dependencies.uuid]
version = "0.8.1"
features = ["v4", "serde"]

22
bitwarden/LICENSE Normal file
View file

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2019-2020 Christoph Heiss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13
bitwarden/README.md Normal file
View file

@ -0,0 +1,13 @@
# Bitwarden API
This library implements an interface to the Bitwarden API.
## License
Licensed under MIT license ([LICENSE](LICENSE) or https://opensource.org/licenses/MIT).
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you shall be licensed by MIT license as above, without any
additional terms or conditions.

View file

@ -1,23 +1,17 @@
// SPDX-License-Identifier: MIT
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{BufWriter, BufReader};
use std::path::{PathBuf};
use chrono::{DateTime, Utc};
use uuid::Uuid;
use reqwest::header::{self, HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned;
use uuid::Uuid;
use crate::cipher::{CipherSuite, CipherString};
const AUTH_URL: &str = "https://identity.bitwarden.com/connect/token";
const BASE_URL: &str = "https://api.bitwarden.com";
#[derive(Debug, failure::Fail)]
pub enum ApiError {
#[fail(display = "prelogin failed: {}", error)]
@ -349,67 +343,3 @@ pub fn sync(auth_data: &AuthData) -> Result<VaultData, ApiError> {
})
}
}
fn get_app_data_path() -> Result<PathBuf, String> {
let project_dirs = directories::ProjectDirs::from("", "", "bwtui")
.ok_or("could not retrieve data directory path")?;
let target_dir = project_dirs.data_local_dir();
fs::create_dir_all(target_dir)
.map_err(|_| "could not create data directory")?;
let mut path = PathBuf::new();
path.push(target_dir);
Ok(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, data)
.map_err(|e| ApiError::VaultDataWriteFailed { error: e.to_string() })
}
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() })?;
let reader = BufReader::new(file);
serde_json::from_reader(reader)
.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

@ -11,7 +11,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::de::Visitor;
use sha2::Sha256;
#[derive(Debug, Default)]
pub struct CipherSuite {
master_key: Vec<u8>,
@ -63,7 +62,6 @@ impl CipherSuite {
}
}
fn derive_master_key(email: &str, password: &str, iter_count: usize) -> (Vec<u8>, String, Vec<u8>) {
let mut master_key = vec![0u8; 32];
pbkdf2::<Hmac<Sha256>>(
@ -85,7 +83,6 @@ fn derive_master_key(email: &str, password: &str, iter_count: usize) -> (Vec<u8>
(master_key, base64::encode(&master_key_hash), mac_key)
}
#[derive(Clone, Debug)]
pub struct CipherString {
type_: usize,
@ -95,7 +92,6 @@ pub struct CipherString {
mac: Vec<u8>,
}
impl CipherString {
fn from_str(text: &str) -> Option<CipherString> {
let type_end = text.find('.')?;
@ -159,7 +155,6 @@ impl CipherString {
}
}
struct CipherStringVisitor;
impl<'de> Visitor<'de> for CipherStringVisitor {

6
bitwarden/src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pub mod api;
pub mod cipher;
pub use api::*;

View file

@ -6,10 +6,10 @@ use cursive::event::Event;
use cursive::traits::*;
use cursive::views::{Dialog, EditView, LinearLayout, OnEventView, TextView};
use crate::api::{self, ApiError, AppData, AuthData, VaultData};
use crate::cipher::CipherSuite;
use crate::vault;
use bitwarden::{self, ApiError, AppData, AuthData, VaultData};
use bitwarden::cipher::CipherSuite;
use crate::vault;
pub fn ask(siv: &mut Cursive, default_email: Option<String>) {
let email_edit = EditView::new()
@ -69,7 +69,6 @@ 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;
@ -85,7 +84,7 @@ fn check_master_password(siv: &mut Cursive, email: String, master_password: &str
return;
}
let auth_data = api::authenticate(&email, &master_password);
let auth_data = bitwarden::authenticate(&email, &master_password);
match auth_data {
Ok(mut auth_data) => {
@ -105,11 +104,10 @@ 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) {
match bitwarden::sync(&auth_data) {
Ok(vault_data) => {
if let Err(err) = api::save_app_data(&auth_data, &vault_data) {
if let Err(err) = vault::save_app_data(&auth_data, &vault_data) {
siv.add_layer(Dialog::info(err.to_string()));
}

View file

@ -1,15 +1,12 @@
// SPDX-License-Identifier: MIT
use cursive::backend::termion::Backend;
use cursive::Cursive;
use cursive::backends::termion::Backend;
use cursive_buffered_backend::BufferedBackend;
mod api;
mod cipher;
mod login;
mod vault;
fn main() {
// We need to use a buffered backend due to flickering with termion.
let mut siv = Cursive::new(|| {
@ -20,7 +17,7 @@ fn main() {
});
let mut email = None;
if let Ok(data) = api::read_app_data() {
if let Ok(data) = vault::read_app_data() {
email = Some(data.vault.profile.email.clone());
siv.set_user_data(data);
}

View file

@ -1,23 +1,24 @@
// SPDX-License-Identifier: MIT
use std::cmp::Ordering;
use std::fs::{self, File};
use std::io::{BufWriter, BufReader};
use std::path::PathBuf;
use clipboard::ClipboardProvider;
use clipboard::ClipboardContext;
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
use clipboard::ClipboardProvider;
use cursive::Cursive;
use cursive::event::{Event, Key};
use cursive::traits::*;
use cursive::views::{Dialog, DummyView, EditView, LinearLayout, OnEventView, TextView};
use cursive_table_view::{TableView, TableViewItem};
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
use serde::de::DeserializeOwned;
use serde::Serialize;
use unicase::UniCase;
use crate::api::{AuthData, CipherEntry, VaultData};
use crate::cipher::CipherSuite;
use bitwarden::{ApiError, AppData, AuthData, CipherEntry, VaultData};
use bitwarden::cipher::CipherSuite;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
enum VaultColumn {
@ -36,7 +37,6 @@ struct VaultEntry {
type VaultTableView = TableView::<VaultEntry, VaultColumn>;
impl VaultEntry {
fn from_cipher_entry(entry: &CipherEntry, cipher: &CipherSuite) -> Option<VaultEntry> {
let favorite = if entry.favorite { "\u{2605}" } else { "\u{2606}" };
@ -70,7 +70,6 @@ impl TableViewItem<VaultColumn> for VaultEntry {
}
}
pub fn show(siv: &mut Cursive, auth_data: AuthData, vault_data: VaultData) {
let items = vault_data.ciphers
.iter()
@ -206,7 +205,6 @@ pub fn show(siv: &mut Cursive, auth_data: AuthData, vault_data: VaultData) {
siv.focus_name("password_table").unwrap();
}
fn fuzzy_match_on_edit(siv: &mut Cursive, items: &Vec<VaultEntry>, content: &str) {
let mut table = siv.find_name::<VaultTableView>("password_table").unwrap();
@ -241,3 +239,62 @@ fn fuzzy_match_on_edit(siv: &mut Cursive, items: &Vec<VaultEntry>, content: &str
table.set_selected_row(0);
table.set_items(items);
}
fn get_app_data_path() -> Result<PathBuf, String> {
let project_dirs = directories::ProjectDirs::from("", "", "bwtui")
.ok_or("could not retrieve data directory path")?;
let target_dir = project_dirs.data_local_dir();
fs::create_dir_all(target_dir)
.map_err(|_| "could not create data directory")?;
let mut path = PathBuf::new();
path.push(target_dir);
Ok(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, data)
.map_err(|e| ApiError::VaultDataWriteFailed { error: e.to_string() })
}
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() })?;
let reader = BufReader::new(file);
serde_json::from_reader(reader)
.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(())
}