aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-04-05 02:17:25 -0400
committerJesse Luehrs <doy@tozt.net>2020-04-05 02:17:25 -0400
commitcd894c27e0b0d5746b95b9c2933da3ba6e9a3f5b (patch)
tree94de4da0e8ac1cea7a855f8fb1d16d6f320e7e72 /src
parent070315ce5f80e82fcb5f39c15cd7bbf1682fdf8b (diff)
downloadrbw-cd894c27e0b0d5746b95b9c2933da3ba6e9a3f5b.tar.gz
rbw-cd894c27e0b0d5746b95b9c2933da3ba6e9a3f5b.zip
basic implementation of the cryptographic stuff
Diffstat (limited to 'src')
-rw-r--r--src/api.rs161
-rw-r--r--src/error.rs43
-rw-r--r--src/identity.rs44
-rw-r--r--src/main.rs42
-rw-r--r--src/prelude.rs2
-rw-r--r--src/secret.rs95
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)
+}