diff options
author | Jesse Luehrs <doy@tozt.net> | 2020-04-05 02:17:25 -0400 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2020-04-05 02:17:25 -0400 |
commit | cd894c27e0b0d5746b95b9c2933da3ba6e9a3f5b (patch) | |
tree | 94de4da0e8ac1cea7a855f8fb1d16d6f320e7e72 /src | |
parent | 070315ce5f80e82fcb5f39c15cd7bbf1682fdf8b (diff) | |
download | rbw-cd894c27e0b0d5746b95b9c2933da3ba6e9a3f5b.tar.gz rbw-cd894c27e0b0d5746b95b9c2933da3ba6e9a3f5b.zip |
basic implementation of the cryptographic stuff
Diffstat (limited to 'src')
-rw-r--r-- | src/api.rs | 161 | ||||
-rw-r--r-- | src/error.rs | 43 | ||||
-rw-r--r-- | src/identity.rs | 44 | ||||
-rw-r--r-- | src/main.rs | 42 | ||||
-rw-r--r-- | src/prelude.rs | 2 | ||||
-rw-r--r-- | src/secret.rs | 95 |
6 files changed, 386 insertions, 1 deletions
diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..ae2e550 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,161 @@ +use crate::prelude::*; + +#[derive(serde::Serialize, Debug)] +struct PreloginReq { + email: String, +} + +#[derive(serde::Deserialize, Debug)] +struct PreloginRes { + #[serde(rename = "Kdf")] + kdf: u32, + #[serde(rename = "KdfIterations")] + kdf_iterations: u32, +} + +#[derive(serde::Serialize, Debug)] +struct ConnectReq { + grant_type: String, + username: String, + password: String, + scope: String, + client_id: String, + #[serde(rename = "deviceType")] + device_type: u32, + #[serde(rename = "deviceIdentifier")] + device_identifier: String, + #[serde(rename = "deviceName")] + device_name: String, + #[serde(rename = "devicePushToken")] + device_push_token: String, +} + +#[derive(serde::Deserialize, Debug)] +struct ConnectRes { + access_token: String, + expires_in: u32, + token_type: String, + refresh_token: String, + #[serde(rename = "Key")] + key: String, +} + +#[derive(serde::Deserialize, Debug)] +struct SyncRes { + #[serde(rename = "Ciphers")] + ciphers: Vec<Cipher>, + #[serde(rename = "Profile")] + profile: Profile, +} + +#[derive(serde::Deserialize, Debug)] +struct Profile { + #[serde(rename = "Key")] + key: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct Cipher { + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "Login")] + pub login: Login, +} + +#[derive(serde::Deserialize, Debug)] +pub struct Login { + #[serde(rename = "Username")] + pub username: String, + #[serde(rename = "Password")] + pub password: String, +} + +#[derive(Debug)] +pub struct Client { + api_url_base: String, + identity_url_base: String, +} + +impl Client { + #[allow(dead_code)] + pub fn new() -> Self { + Self { + api_url_base: "https://api.bitwarden.com".to_string(), + identity_url_base: "https://identity.bitwarden.com".to_string(), + } + } + + pub fn new_self_hosted(base_url: &str) -> Self { + Self { + api_url_base: format!("{}/api", base_url), + identity_url_base: format!("{}/identity", base_url), + } + } + + pub fn prelogin(&self, email: &str) -> Result<u32> { + let prelogin = PreloginReq { + email: email.to_string(), + }; + let client = reqwest::blocking::Client::new(); + let res = client + .post(&self.api_url("/accounts/prelogin")) + .json(&prelogin) + .send() + .context(crate::error::Reqwest)?; + let prelogin_res: PreloginRes = + res.json().context(crate::error::Reqwest)?; + Ok(prelogin_res.kdf_iterations) + } + + pub fn login( + &self, + email: &str, + master_password_hash: &[u8], + ) -> Result<(String, String, String)> { + let connect_req = ConnectReq { + grant_type: "password".to_string(), + username: email.to_string(), + password: base64::encode(&master_password_hash), + scope: "api offline_access".to_string(), + client_id: "desktop".to_string(), + device_type: 8, + device_identifier: uuid::Uuid::new_v4() + .to_hyphenated() + .to_string(), + device_name: "test cli".to_string(), + device_push_token: "".to_string(), + }; + let client = reqwest::blocking::Client::new(); + let res = client + .post(&self.identity_url("/connect/token")) + .form(&connect_req) + .send() + .context(crate::error::Reqwest)?; + let connect_res: ConnectRes = + res.json().context(crate::error::Reqwest)?; + Ok(( + connect_res.access_token, + connect_res.refresh_token, + connect_res.key, + )) + } + + pub fn sync(&self, access_token: &str) -> Result<Vec<Cipher>> { + let client = reqwest::blocking::Client::new(); + let res = client + .get(&self.api_url("/sync")) + .header("Authorization", format!("Bearer {}", access_token)) + .send() + .context(crate::error::Reqwest)?; + let sync_res: SyncRes = res.json().context(crate::error::Reqwest)?; + Ok(sync_res.ciphers) + } + + fn api_url(&self, path: &str) -> String { + format!("{}{}", self.api_url_base, path) + } + + fn identity_url(&self, path: &str) -> String { + format!("{}{}", self.identity_url_base, path) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8ebbfcd --- /dev/null +++ b/src/error.rs @@ -0,0 +1,43 @@ +#[derive(Debug, snafu::Snafu)] +#[snafu(visibility = "pub")] +pub enum Error { + #[snafu(display("failed to create block mode decryptor: {}", source))] + CreateBlockMode { + source: block_modes::InvalidKeyIvLength, + }, + + #[snafu(display("failed to decrypt: {}", source))] + Decrypt { source: block_modes::BlockModeError }, + + // no Error impl + // #[snafu(display("failed to expand with hkdf: {}", source))] + // HkdfExpand { source: hkdf::InvalidLength }, + #[snafu(display("failed to expand with hkdf"))] + HkdfExpand, + + // no Error impl + // #[snafu(display("failed to create hkdf: {}", source))] + // HkdfFromPrk { source: hkdf::InvalidPrkLength }, + #[snafu(display("failed to create hkdf"))] + HkdfFromPrk, + + #[snafu(display("invalid base64: {}", source))] + InvalidBase64 { source: base64::DecodeError }, + + #[snafu(display("invalid cipherstring"))] + InvalidCipherString, + + #[snafu(display("invalid mac"))] + InvalidMac, + + // no Error impl + // #[snafu(display("invalid mac key: {}", source))] + // InvalidMacKey { source: hmac::crypto_mac::InvalidKeyLength }, + #[snafu(display("invalid mac key"))] + InvalidMacKey, + + #[snafu(display("error making api request: {}", source))] + Reqwest { source: reqwest::Error }, +} + +pub type Result<T> = std::result::Result<T, Error>; diff --git a/src/identity.rs b/src/identity.rs new file mode 100644 index 0000000..85b4853 --- /dev/null +++ b/src/identity.rs @@ -0,0 +1,44 @@ +use crate::prelude::*; + +pub struct Identity { + pub email: String, + pub enc_key: Vec<u8>, + pub mac_key: Vec<u8>, + pub master_password_hash: Vec<u8>, +} + +impl Identity { + pub fn new(email: &str, password: &str, iterations: u32) -> Result<Self> { + let mut key = vec![0u8; 32]; + pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha256>>( + password.as_bytes(), + email.as_bytes(), + iterations as usize, + &mut key, + ); + + let mut hash = vec![0u8; 32]; + pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha256>>( + &key, + password.as_bytes(), + 1, + &mut hash, + ); + + let hkdf = hkdf::Hkdf::<sha2::Sha256>::from_prk(&key) + .map_err(|_| Error::HkdfFromPrk)?; + hkdf.expand(b"enc", &mut key) + .map_err(|_| Error::HkdfExpand)?; + + let mut mac_key = vec![0u8; 32]; + hkdf.expand(b"mac", &mut mac_key) + .map_err(|_| Error::HkdfExpand)?; + + Ok(Self { + email: email.to_string(), + enc_key: key, + mac_key, + master_password_hash: hash, + }) + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..fd57794 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,43 @@ +mod api; +mod error; +mod identity; +mod prelude; +mod secret; + fn main() { - println!("Hello, world!"); + let client = api::Client::new_self_hosted("https://bitwarden.tozt.net"); + + let email = rprompt::prompt_reply_stderr("Email: ").unwrap(); + let password = rpassword::prompt_password_stderr("Password: ").unwrap(); + + let iterations = client.prelogin(&email).unwrap(); + let identity = + identity::Identity::new(&email, &password, iterations).unwrap(); + + let (access_token, _refresh_token, protected_key) = client + .login(&identity.email, &identity.master_password_hash) + .unwrap(); + + let protected_key = secret::Secret::new(&protected_key).unwrap(); + let master_key = protected_key + .decrypt(&identity.enc_key, &identity.mac_key) + .unwrap(); + + let enc_key = &master_key[0..32]; + let mac_key = &master_key[32..64]; + + let ciphers = client.sync(&access_token).unwrap(); + for cipher in ciphers { + let secret_name = secret::Secret::new(&cipher.name).unwrap(); + let name = secret_name.decrypt(enc_key, mac_key).unwrap(); + let secret_username = + secret::Secret::new(&cipher.login.username).unwrap(); + let username = secret_username.decrypt(enc_key, mac_key).unwrap(); + let secret_password = + secret::Secret::new(&cipher.login.password).unwrap(); + let password = secret_password.decrypt(enc_key, mac_key).unwrap(); + println!("{}:", String::from_utf8(name).unwrap()); + println!(" Username: {}", String::from_utf8(username).unwrap()); + println!(" Password: {}", String::from_utf8(password).unwrap()); + } } diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..af8bbd7 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,2 @@ +pub use crate::error::{Error, Result}; +pub use snafu::{OptionExt as _, ResultExt as _}; diff --git a/src/secret.rs b/src/secret.rs new file mode 100644 index 0000000..9f896c7 --- /dev/null +++ b/src/secret.rs @@ -0,0 +1,95 @@ +use crate::prelude::*; + +use block_modes::BlockMode as _; +use hmac::Mac as _; + +pub struct Secret { + ty: u8, + iv: Vec<u8>, + ciphertext: Vec<u8>, + mac: Option<Vec<u8>>, +} + +impl Secret { + pub fn new(s: &str) -> Result<Self> { + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() != 2 { + return Err(Error::InvalidCipherString); + } + + let ty = parts[0].as_bytes(); + if ty.len() != 1 { + return Err(Error::InvalidCipherString); + } + + let ty = ty[0] - b'0'; + let contents = parts[1]; + + let parts: Vec<&str> = contents.split('|').collect(); + if parts.len() < 2 || parts.len() > 3 { + return Err(Error::InvalidCipherString); + } + + let iv = + base64::decode(parts[0]).context(crate::error::InvalidBase64)?; + let ciphertext = + base64::decode(parts[1]).context(crate::error::InvalidBase64)?; + let mac = if parts.len() > 2 { + Some( + base64::decode(parts[2]) + .context(crate::error::InvalidBase64)?, + ) + } else { + None + }; + + Ok(Self { + ty, + iv, + ciphertext, + mac, + }) + } + + pub fn decrypt(&self, enc_key: &[u8], mac_key: &[u8]) -> Result<Vec<u8>> { + if self.ty != 2 { + unimplemented!() + } + + if let Some(mac) = &self.mac { + let mut digest = hmac::Hmac::<sha2::Sha256>::new_varkey(mac_key) + .map_err(|_| Error::InvalidMacKey)?; + digest.input(&self.iv); + digest.input(&self.ciphertext); + let calculated_mac = digest.result().code(); + + if !macs_equal(mac, &calculated_mac, mac_key)? { + return Err(Error::InvalidMac); + } + } + + let cipher = block_modes::Cbc::< + aes::Aes256, + block_modes::block_padding::Pkcs7, + >::new_var(enc_key, &self.iv) + .context(crate::error::CreateBlockMode)?; + + cipher + .decrypt_vec(&self.ciphertext) + .context(crate::error::Decrypt) + } +} + +fn macs_equal(mac1: &[u8], mac2: &[u8], mac_key: &[u8]) -> Result<bool> { + let mut digest = hmac::Hmac::<sha2::Sha256>::new_varkey(mac_key) + .map_err(|_| Error::InvalidMacKey)?; + digest.input(mac1); + let hmac1 = digest.result().code(); + + let mut digest = hmac::Hmac::<sha2::Sha256>::new_varkey(mac_key) + .map_err(|_| Error::InvalidMacKey)?; + digest.input(mac2); + let hmac2 = digest.result().code(); + + Ok(hmac1 == hmac2) +} |