Split out the Bitwarden API into a separate sub-crate.
This commit is contained in:
parent
bc76050d3a
commit
4063542e71
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
target/
|
||||
bitwarden/target/
|
||||
|
|
1376
Cargo.lock
generated
1376
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
31
Cargo.toml
31
Cargo.toml
|
@ -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"
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
1489
bitwarden/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
32
bitwarden/Cargo.toml
Normal file
32
bitwarden/Cargo.toml
Normal 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
22
bitwarden/LICENSE
Normal 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
13
bitwarden/README.md
Normal 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.
|
|
@ -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(())
|
||||
}
|
|
@ -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
6
bitwarden/src/lib.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pub mod api;
|
||||
pub mod cipher;
|
||||
|
||||
pub use api::*;
|
14
src/login.rs
14
src/login.rs
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
79
src/vault.rs
79
src/vault.rs
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue