aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions.rs33
-rw-r--r--src/api.rs76
-rw-r--r--src/bin/rbw-agent/actions.rs109
-rw-r--r--src/bin/rbw-agent/agent.rs8
-rw-r--r--src/bin/rbw/actions.rs4
-rw-r--r--src/bin/rbw/commands.rs47
-rw-r--r--src/bin/rbw/main.rs33
-rw-r--r--src/cipherstring.rs116
-rw-r--r--src/config.rs17
-rw-r--r--src/error.rs9
-rw-r--r--src/lib.rs1
-rw-r--r--src/locked.rs37
-rw-r--r--src/pinentry.rs10
-rw-r--r--src/protocol.rs1
14 files changed, 364 insertions, 137 deletions
diff --git a/src/actions.rs b/src/actions.rs
index 0d8c88d..02ec854 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -1,37 +1,42 @@
use crate::prelude::*;
+pub async fn register(
+ email: &str,
+ apikey: crate::locked::ApiKey,
+) -> Result<()> {
+ let config = crate::config::Config::load_async().await?;
+ let client =
+ crate::api::Client::new(&config.base_url(), &config.identity_url());
+
+ client.register(email, &config.device_id, &apikey).await?;
+
+ Ok(())
+}
+
pub async fn login(
email: &str,
- password: &crate::locked::Password,
+ password: crate::locked::Password,
two_factor_token: Option<&str>,
two_factor_provider: Option<crate::api::TwoFactorProviderType>,
-) -> Result<(String, String, u32, String, crate::locked::Keys)> {
+) -> Result<(String, String, u32, String)> {
let config = crate::config::Config::load_async().await?;
let client =
crate::api::Client::new(&config.base_url(), &config.identity_url());
let iterations = client.prelogin(email).await?;
let identity =
- crate::identity::Identity::new(email, password, iterations)?;
-
+ crate::identity::Identity::new(email, &password, iterations)?;
let (access_token, refresh_token, protected_key) = client
.login(
- &identity.email,
+ email,
+ &config.device_id,
&identity.master_password_hash,
two_factor_token,
two_factor_provider,
)
.await?;
- let master_keys = crate::cipherstring::CipherString::new(&protected_key)?
- .decrypt_locked_symmetric(&identity.keys)?;
- Ok((
- access_token,
- refresh_token,
- iterations,
- protected_key,
- crate::locked::Keys::new(master_keys),
- ))
+ Ok((access_token, refresh_token, iterations, protected_key))
}
pub async fn unlock(
diff --git a/src/api.rs b/src/api.rs
index ef0f73d..14c11fd 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -148,9 +148,10 @@ struct PreloginRes {
struct ConnectPasswordReq {
grant_type: String,
username: String,
- password: String,
+ password: Option<String>,
scope: String,
client_id: String,
+ client_secret: Option<String>,
#[serde(rename = "deviceType")]
device_type: u32,
#[serde(rename = "deviceIdentifier")]
@@ -178,7 +179,7 @@ struct ConnectPasswordRes {
#[derive(serde::Deserialize, Debug)]
struct ConnectErrorRes {
error: String,
- error_description: String,
+ error_description: Option<String>,
#[serde(rename = "ErrorModel")]
error_model: Option<ConnectErrorResErrorModel>,
#[serde(rename = "TwoFactorProviders")]
@@ -575,23 +576,62 @@ impl Client {
Ok(prelogin_res.kdf_iterations)
}
+ pub async fn register(
+ &self,
+ email: &str,
+ device_id: &str,
+ apikey: &crate::locked::ApiKey,
+ ) -> Result<()> {
+ let connect_req = ConnectPasswordReq {
+ grant_type: "client_credentials".to_string(),
+ username: email.to_string(),
+ password: None,
+ scope: "api".to_string(),
+ // XXX unwraps here are not necessarily safe
+ client_id: String::from_utf8(apikey.client_id().to_vec())
+ .unwrap(),
+ client_secret: Some(
+ String::from_utf8(apikey.client_secret().to_vec()).unwrap(),
+ ),
+ device_type: 8,
+ device_identifier: device_id.to_string(),
+ device_name: "rbw".to_string(),
+ device_push_token: "".to_string(),
+ two_factor_token: None,
+ two_factor_provider: None,
+ };
+ let client = reqwest::Client::new();
+ let res = client
+ .post(&self.identity_url("/connect/token"))
+ .form(&connect_req)
+ .send()
+ .await
+ .map_err(|source| Error::Reqwest { source })?;
+ if let reqwest::StatusCode::OK = res.status() {
+ Ok(())
+ } else {
+ let code = res.status().as_u16();
+ Err(classify_login_error(&res.json_with_path().await?, code))
+ }
+ }
+
pub async fn login(
&self,
email: &str,
- master_password_hash: &crate::locked::PasswordHash,
+ device_id: &str,
+ password_hash: &crate::locked::PasswordHash,
two_factor_token: Option<&str>,
two_factor_provider: Option<TwoFactorProviderType>,
) -> Result<(String, String, String)> {
let connect_req = ConnectPasswordReq {
grant_type: "password".to_string(),
username: email.to_string(),
- password: base64::encode(master_password_hash.hash()),
+ password: Some(base64::encode(password_hash.hash())),
scope: "api offline_access".to_string(),
client_id: "desktop".to_string(),
+ client_secret: None,
device_type: 8,
- device_identifier: uuid::Uuid::new_v4()
- .to_hyphenated()
- .to_string(),
+ device_identifier: device_id.to_string(),
device_name: "rbw".to_string(),
device_push_token: "".to_string(),
two_factor_token: two_factor_token
@@ -602,6 +642,10 @@ impl Client {
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)
+ .header(
+ "auth-email",
+ base64::encode_config(email, base64::URL_SAFE_NO_PAD),
+ )
.send()
.await
.map_err(|source| Error::Reqwest { source })?;
@@ -708,7 +752,7 @@ impl Client {
password: password.clone(),
totp: totp.clone(),
uris,
- })
+ });
}
crate::db::EntryData::Card {
cardholder_name,
@@ -1046,15 +1090,17 @@ impl Client {
}
fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error {
+ let error_desc = error_res.error_description.clone();
+ let error_desc = error_desc.as_deref();
match error_res.error.as_str() {
- "invalid_grant" => match error_res.error_description.as_str() {
- "invalid_username_or_password" => {
+ "invalid_grant" => match error_desc {
+ Some("invalid_username_or_password") => {
if let Some(error_model) = error_res.error_model.as_ref() {
let message = error_model.message.as_str().to_string();
return Error::IncorrectPassword { message };
}
}
- "Two factor required." => {
+ Some("Two factor required.") => {
if let Some(providers) =
error_res.two_factor_providers.as_ref()
{
@@ -1063,12 +1109,18 @@ fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error {
};
}
}
+ Some("Captcha required.") => {
+ return Error::RegistrationRequired;
+ }
_ => {}
},
+ "invalid_client" => {
+ return Error::IncorrectApiKey;
+ }
"" => {
// bitwarden_rs returns an empty error and error_description for
// this case, for some reason
- if error_res.error_description.is_empty() {
+ if error_desc.is_none() || error_desc == Some("") {
if let Some(error_model) = error_res.error_model.as_ref() {
let message = error_model.message.as_str().to_string();
match message.as_str() {
diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs
index 4f4096e..1cc71c3 100644
--- a/src/bin/rbw-agent/actions.rs
+++ b/src/bin/rbw-agent/actions.rs
@@ -1,5 +1,84 @@
use anyhow::Context as _;
+pub async fn register(
+ sock: &mut crate::sock::Sock,
+ tty: Option<&str>,
+) -> anyhow::Result<()> {
+ let db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new());
+
+ if db.needs_login() {
+ let url_str = config_base_url().await?;
+ let url = reqwest::Url::parse(&url_str)
+ .context("failed to parse base url")?;
+ let host = if let Some(host) = url.host_str() {
+ host
+ } else {
+ return Err(anyhow::anyhow!(
+ "couldn't find host in rbw base url {}",
+ url_str
+ ));
+ };
+
+ let email = config_email().await?;
+
+ let mut err_msg = None;
+ for i in 1_u8..=3 {
+ let err = if i > 1 {
+ // this unwrap is safe because we only ever continue the loop
+ // if we have set err_msg
+ Some(format!("{} (attempt {}/3)", err_msg.unwrap(), i))
+ } else {
+ None
+ };
+ let client_id = rbw::pinentry::getpin(
+ &config_pinentry().await?,
+ "API key client__id",
+ &format!("Log in to {}", host),
+ err.as_deref(),
+ tty,
+ false,
+ )
+ .await
+ .context("failed to read client_id from pinentry")?;
+ let client_secret = rbw::pinentry::getpin(
+ &config_pinentry().await?,
+ "API key client__secret",
+ &format!("Log in to {}", host),
+ err.as_deref(),
+ tty,
+ false,
+ )
+ .await
+ .context("failed to read client_secret from pinentry")?;
+ let apikey = rbw::locked::ApiKey::new(client_id, client_secret);
+ match rbw::actions::register(&email, apikey.clone()).await {
+ Ok(()) => {
+ break;
+ }
+ Err(rbw::error::Error::IncorrectPassword { message }) => {
+ if i == 3 {
+ return Err(rbw::error::Error::IncorrectPassword {
+ message,
+ })
+ .context("failed to log in to bitwarden instance");
+ } else {
+ err_msg = Some(message);
+ continue;
+ }
+ }
+ Err(e) => {
+ return Err(e)
+ .context("failed to log in to bitwarden instance")
+ }
+ }
+ }
+ }
+
+ respond_ack(sock).await?;
+
+ Ok(())
+}
+
pub async fn login(
sock: &mut crate::sock::Sock,
state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>,
@@ -37,16 +116,18 @@ pub async fn login(
&format!("Log in to {}", host),
err.as_deref(),
tty,
+ true,
)
.await
.context("failed to read password from pinentry")?;
- match rbw::actions::login(&email, &password, None, None).await {
+ match rbw::actions::login(&email, password.clone(), None, None)
+ .await
+ {
Ok((
access_token,
refresh_token,
iterations,
protected_key,
- _,
)) => {
login_success(
sock,
@@ -74,7 +155,7 @@ pub async fn login(
) = two_factor(
tty,
&email,
- &password,
+ password.clone(),
rbw::api::TwoFactorProviderType::Authenticator,
)
.await?;
@@ -122,7 +203,7 @@ pub async fn login(
async fn two_factor(
tty: Option<&str>,
email: &str,
- password: &rbw::locked::Password,
+ password: rbw::locked::Password,
provider: rbw::api::TwoFactorProviderType,
) -> anyhow::Result<(String, String, u32, String)> {
let mut err_msg = None;
@@ -140,26 +221,21 @@ async fn two_factor(
"Enter the 6 digit verification code from your authenticator app.",
err.as_deref(),
tty,
+ true,
)
.await
.context("failed to read code from pinentry")?;
let code = std::str::from_utf8(code.password())
.context("code was not valid utf8")?;
match rbw::actions::login(
- &email,
- &password,
+ email,
+ password.clone(),
Some(code),
Some(provider),
)
.await
{
- Ok((
- access_token,
- refresh_token,
- iterations,
- protected_key,
- _,
- )) => {
+ Ok((access_token, refresh_token, iterations, protected_key)) => {
return Ok((
access_token,
refresh_token,
@@ -300,6 +376,7 @@ pub async fn unlock(
"Unlock the local database",
err.as_deref(),
tty,
+ true,
)
.await
.context("failed to read password from pinentry")?;
@@ -430,7 +507,7 @@ pub async fn decrypt(
.context("failed to parse encrypted secret")?;
let plaintext = String::from_utf8(
cipherstring
- .decrypt_symmetric(&keys)
+ .decrypt_symmetric(keys)
.context("failed to decrypt encrypted secret")?,
)
.context("failed to parse decrypted secret")?;
@@ -512,7 +589,7 @@ async fn config_email() -> anyhow::Result<String> {
async fn load_db() -> anyhow::Result<rbw::db::Db> {
let config = rbw::config::Config::load_async().await?;
if let Some(email) = &config.email {
- rbw::db::Db::load_async(&config.server_name(), &email)
+ rbw::db::Db::load_async(&config.server_name(), email)
.await
.map_err(anyhow::Error::new)
} else {
@@ -523,7 +600,7 @@ async fn load_db() -> anyhow::Result<rbw::db::Db> {
async fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> {
let config = rbw::config::Config::load_async().await?;
if let Some(email) = &config.email {
- db.save_async(&config.server_name(), &email)
+ db.save_async(&config.server_name(), email)
.await
.map_err(anyhow::Error::new)
} else {
diff --git a/src/bin/rbw-agent/agent.rs b/src/bin/rbw-agent/agent.rs
index 760a1fc..fae8c7b 100644
--- a/src/bin/rbw-agent/agent.rs
+++ b/src/bin/rbw-agent/agent.rs
@@ -133,6 +133,10 @@ async fn handle_request(
}
};
let set_timeout = match &req.action {
+ rbw::protocol::Action::Register => {
+ crate::actions::register(sock, req.tty.as_deref()).await?;
+ true
+ }
rbw::protocol::Action::Login => {
crate::actions::login(sock, state.clone(), req.tty.as_deref())
.await?;
@@ -167,7 +171,7 @@ async fn handle_request(
crate::actions::decrypt(
sock,
state.clone(),
- &cipherstring,
+ cipherstring,
org_id.as_deref(),
)
.await?;
@@ -177,7 +181,7 @@ async fn handle_request(
crate::actions::encrypt(
sock,
state.clone(),
- &plaintext,
+ plaintext,
org_id.as_deref(),
)
.await?;
diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs
index 75703f9..39fde15 100644
--- a/src/bin/rbw/actions.rs
+++ b/src/bin/rbw/actions.rs
@@ -1,6 +1,10 @@
use anyhow::Context as _;
use std::io::Read as _;
+pub fn register() -> anyhow::Result<()> {
+ simple_action(rbw::protocol::Action::Register)
+}
+
pub fn login() -> anyhow::Result<()> {
simple_action(rbw::protocol::Action::Login)
}
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs
index c1f9291..9efd966 100644
--- a/src/bin/rbw/commands.rs
+++ b/src/bin/rbw/commands.rs
@@ -465,6 +465,13 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> {
Ok(())
}
+pub fn register() -> anyhow::Result<()> {
+ ensure_agent()?;
+ crate::actions::register()?;
+
+ Ok(())
+}
+
pub fn login() -> anyhow::Result<()> {
ensure_agent()?;
crate::actions::login()?;
@@ -642,7 +649,7 @@ pub fn add(
let mut folder_id = None;
if let Some(folder_name) = folder {
let (new_access_token, folders) =
- rbw::actions::list_folders(&access_token, &refresh_token)?;
+ rbw::actions::list_folders(&access_token, refresh_token)?;
if let Some(new_access_token) = new_access_token {
access_token = new_access_token.clone();
db.access_token = Some(new_access_token);
@@ -663,7 +670,7 @@ pub fn add(
if folder_id.is_none() {
let (new_access_token, id) = rbw::actions::create_folder(
&access_token,
- &refresh_token,
+ refresh_token,
&crate::actions::encrypt(folder_name, None)?,
)?;
if let Some(new_access_token) = new_access_token {
@@ -677,7 +684,7 @@ pub fn add(
if let (Some(access_token), ()) = rbw::actions::add(
&access_token,
- &refresh_token,
+ refresh_token,
&name,
&rbw::db::EntryData::Login {
username,
@@ -735,7 +742,7 @@ pub fn generate(
let mut folder_id = None;
if let Some(folder_name) = folder {
let (new_access_token, folders) =
- rbw::actions::list_folders(&access_token, &refresh_token)?;
+ rbw::actions::list_folders(&access_token, refresh_token)?;
if let Some(new_access_token) = new_access_token {
access_token = new_access_token.clone();
db.access_token = Some(new_access_token);
@@ -758,7 +765,7 @@ pub fn generate(
if folder_id.is_none() {
let (new_access_token, id) = rbw::actions::create_folder(
&access_token,
- &refresh_token,
+ refresh_token,
&crate::actions::encrypt(folder_name, None)?,
)?;
if let Some(new_access_token) = new_access_token {
@@ -772,7 +779,7 @@ pub fn generate(
if let (Some(access_token), ()) = rbw::actions::add(
&access_token,
- &refresh_token,
+ refresh_token,
&name,
&rbw::db::EntryData::Login {
username,
@@ -880,8 +887,8 @@ pub fn edit(
};
if let (Some(access_token), ()) = rbw::actions::edit(
- &access_token,
- &refresh_token,
+ access_token,
+ refresh_token,
&entry.id,
entry.org_id.as_deref(),
&entry.name,
@@ -921,7 +928,7 @@ pub fn remove(
.with_context(|| format!("couldn't find entry for '{}'", desc))?;
if let (Some(access_token), ()) =
- rbw::actions::remove(&access_token, &refresh_token, &entry.id)?
+ rbw::actions::remove(access_token, refresh_token, &entry.id)?
{
db.access_token = Some(access_token);
save_db(&db)?;
@@ -1053,7 +1060,7 @@ fn find_entry(
Ok(_) => {
for cipher in &db.entries {
if name == cipher.id {
- return Ok((cipher.clone(), decrypt_cipher(&cipher)?));
+ return Ok((cipher.clone(), decrypt_cipher(cipher)?));
}
}
Err(anyhow::anyhow!("no entry found"))
@@ -1183,10 +1190,7 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> {
.name
.as_ref()
.map(|name| {
- crate::actions::decrypt(
- &name,
- entry.org_id.as_deref(),
- )
+ crate::actions::decrypt(name, entry.org_id.as_deref())
})
.transpose()?,
value: field
@@ -1194,7 +1198,7 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> {
.as_ref()
.map(|value| {
crate::actions::decrypt(
- &value,
+ value,
entry.org_id.as_deref(),
)
})
@@ -1444,7 +1448,7 @@ fn parse_editor(contents: &str) -> (Option<String>, Option<String>) {
fn load_db() -> anyhow::Result<rbw::db::Db> {
let config = rbw::config::Config::load()?;
if let Some(email) = &config.email {
- rbw::db::Db::load(&config.server_name(), &email)
+ rbw::db::Db::load(&config.server_name(), email)
.map_err(anyhow::Error::new)
} else {
Err(anyhow::anyhow!("failed to find email address in config"))
@@ -1454,7 +1458,7 @@ fn load_db() -> anyhow::Result<rbw::db::Db> {
fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> {
let config = rbw::config::Config::load()?;
if let Some(email) = &config.email {
- db.save(&config.server_name(), &email)
+ db.save(&config.server_name(), email)
.map_err(anyhow::Error::new)
} else {
Err(anyhow::anyhow!("failed to find email address in config"))
@@ -1464,7 +1468,7 @@ fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> {
fn remove_db() -> anyhow::Result<()> {
let config = rbw::config::Config::load()?;
if let Some(email) = &config.email {
- rbw::db::Db::remove(&config.server_name(), &email)
+ rbw::db::Db::remove(&config.server_name(), email)
.map_err(anyhow::Error::new)
} else {
Err(anyhow::anyhow!("failed to find email address in config"))
@@ -1494,8 +1498,11 @@ fn parse_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> {
} else {
secret.to_string()
};
- base32::decode(base32::Alphabet::RFC4648 { padding: false }, &secret_str.replace(" ", ""))
- .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32"))
+ base32::decode(
+ base32::Alphabet::RFC4648 { padding: false },
+ &secret_str.replace(" ", ""),
+ )
+ .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32"))
}
fn generate_totp(secret: &str) -> anyhow::Result<String> {
diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs
index 6c6c33e..85631c5 100644
--- a/src/bin/rbw/main.rs
+++ b/src/bin/rbw/main.rs
@@ -17,6 +17,17 @@ enum Opt {
config: Config,
},
+ #[structopt(
+ about = "Register this device with the Bitwarden server",
+ long_about = "Register this device with the Bitwarden server\n\n\
+ The official Bitwarden server includes bot detection to prevent \
+ brute force attacks. In order to avoid being detected as bot \
+ traffic, you will need to use this command to log in with your \
+ personal API key (instead of your password) first before regular \
+ logins will work."
+ )]
+ Register,
+
#[structopt(about = "Log in to the Bitwarden server")]
Login,
@@ -214,6 +225,7 @@ impl Opt {
Self::Config { config } => {
format!("config {}", config.subcommand_name())
}
+ Self::Register => "register".to_string(),
Self::Login => "login".to_string(),
Self::Unlock => "unlock".to_string(),
Self::Unlocked => "unlocked".to_string(),
@@ -281,22 +293,23 @@ fn main(opt: Opt) {
let res = match &opt {
Opt::Config { config } => match config {
Config::Show => commands::config_show(),
- Config::Set { key, value } => commands::config_set(&key, &value),
- Config::Unset { key } => commands::config_unset(&key),
+ Config::Set { key, value } => commands::config_set(key, value),
+ Config::Unset { key } => commands::config_unset(key),
},
+ Opt::Register => commands::register(),
Opt::Login => commands::login(),
Opt::Unlock => commands::unlock(),
Opt::Unlocked => commands::unlocked(),
Opt::Sync => commands::sync(),
- Opt::List { fields } => commands::list(&fields),
+ Opt::List { fields } => commands::list(fields),
Opt::Get {
name,
user,
folder,
full,
- } => commands::get(&name, user.as_deref(), folder.as_deref(), *full),
+ } => commands::get(name, user.as_deref(), folder.as_deref(), *full),
Opt::Code { name, user, folder } => {
- commands::code(&name, user.as_deref(), folder.as_deref())
+ commands::code(name, user.as_deref(), folder.as_deref())
}
Opt::Add {
name,
@@ -304,7 +317,7 @@ fn main(opt: Opt) {
uri,
folder,
} => commands::add(
- &name,
+ name,
user.as_deref(),
uri.iter()
// XXX not sure what the ui for specifying the match type
@@ -349,18 +362,18 @@ fn main(opt: Opt) {
)
}
Opt::Edit { name, user, folder } => {
- commands::edit(&name, user.as_deref(), folder.as_deref())
+ commands::edit(name, user.as_deref(), folder.as_deref())
}
Opt::Remove { name, user, folder } => {
- commands::remove(&name, user.as_deref(), folder.as_deref())
+ commands::remove(name, user.as_deref(), folder.as_deref())
}
Opt::History { name, user, folder } => {
- commands::history(&name, user.as_deref(), folder.as_deref())
+ commands::history(name, user.as_deref(), folder.as_deref())
}
Opt::Lock => commands::lock(),
Opt::Purge => commands::purge(),
Opt::StopAgent => commands::stop_agent(),
- Opt::GenCompletions { shell } => gen_completions(&shell),
+ Opt::GenCompletions { shell } => gen_completions(shell),
}
.context(format!("rbw {}", opt.subcommand_name()));
diff --git a/src/cipherstring.rs b/src/cipherstring.rs
index 73eeeb6..39254c7 100644
--- a/src/cipherstring.rs
+++ b/src/cipherstring.rs
@@ -4,6 +4,7 @@ use block_modes::BlockMode as _;
use block_padding::Padding as _;
use hmac::{Mac as _, NewMac as _};
use rand::RngCore as _;
+use rsa::pkcs8::FromPrivateKey as _;
use zeroize::Zeroize as _;
pub enum CipherString {
@@ -122,27 +123,27 @@ impl CipherString {
&self,
keys: &crate::locked::Keys,
) -> Result<Vec<u8>> {
- match self {
- Self::Symmetric {
+ if let Self::Symmetric {
+ iv,
+ ciphertext,
+ mac,
+ } = self
+ {
+ let cipher = decrypt_common_symmetric(
+ keys,
iv,
ciphertext,
- mac,
- } => {
- let cipher = decrypt_common_symmetric(
- keys,
- iv,
- ciphertext,
- mac.as_deref(),
- )?;
- cipher
- .decrypt_vec(ciphertext)
- .map_err(|source| Error::Decrypt { source })
- }
- _ => Err(Error::InvalidCipherString {
+ mac.as_deref(),
+ )?;
+ cipher
+ .decrypt_vec(ciphertext)
+ .map_err(|source| Error::Decrypt { source })
+ } else {
+ Err(Error::InvalidCipherString {
reason:
"found an asymmetric cipherstring, expecting symmetric"
.to_string(),
- }),
+ })
}
}
@@ -150,30 +151,30 @@ impl CipherString {
&self,
keys: &crate::locked::Keys,
) -> Result<crate::locked::Vec> {
- match self {
- Self::Symmetric {
+ if let Self::Symmetric {
+ iv,
+ ciphertext,
+ mac,
+ } = self
+ {
+ let mut res = crate::locked::Vec::new();
+ res.extend(ciphertext.iter().copied());
+ let cipher = decrypt_common_symmetric(
+ keys,
iv,
ciphertext,
- mac,
- } => {
- let mut res = crate::locked::Vec::new();
- res.extend(ciphertext.iter().copied());
- let cipher = decrypt_common_symmetric(
- keys,
- iv,
- ciphertext,
- mac.as_deref(),
- )?;
- cipher
- .decrypt(res.data_mut())
- .map_err(|source| Error::Decrypt { source })?;
- Ok(res)
- }
- _ => Err(Error::InvalidCipherString {
+ mac.as_deref(),
+ )?;
+ cipher
+ .decrypt(res.data_mut())
+ .map_err(|source| Error::Decrypt { source })?;
+ Ok(res)
+ } else {
+ Err(Error::InvalidCipherString {
reason:
"found an asymmetric cipherstring, expecting symmetric"
.to_string(),
- }),
+ })
}
}
@@ -181,34 +182,33 @@ impl CipherString {
&self,
private_key: &crate::locked::PrivateKey,
) -> Result<crate::locked::Vec> {
- match self {
- Self::Asymmetric { ciphertext } => {
- let privkey_data = private_key.private_key();
- let privkey_data = block_padding::Pkcs7::unpad(privkey_data)
- .map_err(|_| Error::Padding)?;
- let pkey = rsa::RSAPrivateKey::from_pkcs8(privkey_data)
- .map_err(|source| Error::Rsa { source })?;
- let mut bytes = pkey
- .decrypt(
- rsa::padding::PaddingScheme::new_oaep::<sha1::Sha1>(),
- ciphertext,
- )
- .map_err(|source| Error::Rsa { source })?;
+ if let Self::Asymmetric { ciphertext } = self {
+ let privkey_data = private_key.private_key();
+ let privkey_data = block_padding::Pkcs7::unpad(privkey_data)
+ .map_err(|_| Error::Padding)?;
+ let pkey = rsa::RsaPrivateKey::from_pkcs8_der(privkey_data)
+ .map_err(|source| Error::RsaPkcs8 { source })?;
+ let mut bytes = pkey
+ .decrypt(
+ rsa::padding::PaddingScheme::new_oaep::<sha1::Sha1>(),
+ ciphertext,
+ )
+ .map_err(|source| Error::Rsa { source })?;
- // XXX it'd be great if the rsa crate would let us decrypt
- // into a preallocated buffer directly to avoid the
- // intermediate vec that needs to be manually zeroized, etc
- let mut res = crate::locked::Vec::new();
- res.extend(bytes.iter().copied());
- bytes.zeroize();
+ // XXX it'd be great if the rsa crate would let us decrypt
+ // into a preallocated buffer directly to avoid the
+ // intermediate vec that needs to be manually zeroized, etc
+ let mut res = crate::locked::Vec::new();
+ res.extend(bytes.iter().copied());
+ bytes.zeroize();
- Ok(res)
- }
- _ => Err(Error::InvalidCipherString {
+ Ok(res)
+ } else {
+ Err(Error::InvalidCipherString {
reason:
"found a symmetric cipherstring, expecting asymmetric"
.to_string(),
- }),
+ })
}
}
}
diff --git a/src/config.rs b/src/config.rs
index c6e0787..bbc39f7 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -12,6 +12,8 @@ pub struct Config {
pub lock_timeout: u64,
#[serde(default = "default_pinentry")]
pub pinentry: String,
+ #[serde(default = "stub_device_id")]
+ pub device_id: String,
}
impl Default for Config {
@@ -22,6 +24,7 @@ impl Default for Config {
identity_url: Default::default(),
lock_timeout: default_lock_timeout(),
pinentry: default_pinentry(),
+ device_id: default_device_id(),
}
}
}
@@ -34,6 +37,14 @@ pub fn default_pinentry() -> String {
"pinentry".to_string()
}
+fn default_device_id() -> String {
+ uuid::Uuid::new_v4().to_hyphenated().to_string()
+}
+
+fn stub_device_id() -> String {
+ String::from("fix")
+}
+
impl Config {
pub fn new() -> Self {
Self::default()
@@ -116,10 +127,14 @@ impl Config {
}
pub fn validate() -> Result<()> {
- let config = Self::load()?;
+ let mut config = Self::load()?;
if config.email.is_none() {
return Err(Error::ConfigMissingEmail);
}
+ if config.device_id == stub_device_id() {
+ config.device_id = default_device_id();
+ config.save()?;
+ }
Ok(())
}
diff --git a/src/error.rs b/src/error.rs
index bc97087..d584e53 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -46,6 +46,9 @@ pub enum Error {
#[error("failed to expand with hkdf")]
HkdfExpand,
+ #[error("incorrect api key")]
+ IncorrectApiKey,
+
#[error("{message}")]
IncorrectPassword { message: String },
@@ -132,6 +135,9 @@ pub enum Error {
#[error("error waiting for pinentry to exit")]
PinentryWait { source: tokio::io::Error },
+ #[error("This device has not yet been registered with the Bitwarden server. Run `rbw register` first, and then try again.")]
+ RegistrationRequired,
+
#[error("failed to remove db at {}", .file.display())]
RemoveDb {
source: std::io::Error,
@@ -150,6 +156,9 @@ pub enum Error {
#[error("failed to decrypt")]
Rsa { source: rsa::errors::Error },
+ #[error("failed to decrypt")]
+ RsaPkcs8 { source: rsa::pkcs8::Error },
+
#[error("failed to save config to {}", .file.display())]
SaveConfig {
source: std::io::Error,
diff --git a/src/lib.rs b/src/lib.rs
index 91869e4..4a13e25 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -12,6 +12,7 @@
#![allow(clippy::too_many_arguments)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::type_complexity)]
+#![allow(clippy::unused_async)]
pub mod actions;
pub mod api;
diff --git a/src/locked.rs b/src/locked.rs
index 611e57e..4ddf021 100644
--- a/src/locked.rs
+++ b/src/locked.rs
@@ -32,7 +32,7 @@ impl Vec {
pub fn zero(&mut self) {
self.truncate(0);
- self.data.extend(std::iter::repeat(0).take(LEN))
+ self.data.extend(std::iter::repeat(0).take(LEN));
}
pub fn extend(&mut self, it: impl Iterator<Item = u8>) {
@@ -51,6 +51,15 @@ impl Drop for Vec {
}
}
+impl Clone for Vec {
+ fn clone(&self) -> Self {
+ let mut new_vec = Self::new();
+ new_vec.extend(self.data().iter().copied());
+ new_vec
+ }
+}
+
+#[derive(Clone)]
pub struct Password {
password: Vec,
}
@@ -65,6 +74,7 @@ impl Password {
}
}
+#[derive(Clone)]
pub struct Keys {
keys: Vec,
}
@@ -83,6 +93,7 @@ impl Keys {
}
}
+#[derive(Clone)]
pub struct PasswordHash {
hash: Vec,
}
@@ -97,6 +108,7 @@ impl PasswordHash {
}
}
+#[derive(Clone)]
pub struct PrivateKey {
private_key: Vec,
}
@@ -110,3 +122,26 @@ impl PrivateKey {
self.private_key.data()
}
}
+
+#[derive(Clone)]
+pub struct ApiKey {
+ client_id: Password,
+ client_secret: Password,
+}
+
+impl ApiKey {
+ pub fn new(client_id: Password, client_secret: Password) -> Self {
+ Self {
+ client_id,
+ client_secret,
+ }
+ }
+
+ pub fn client_id(&self) -> &[u8] {
+ self.client_id.password()
+ }
+
+ pub fn client_secret(&self) -> &[u8] {
+ self.client_secret.password()
+ }
+}
diff --git a/src/pinentry.rs b/src/pinentry.rs
index d62d4b2..b4d2bb0 100644
--- a/src/pinentry.rs
+++ b/src/pinentry.rs
@@ -8,15 +8,19 @@ pub async fn getpin(
desc: &str,
err: Option<&str>,
tty: Option<&str>,
+ grab: bool,
) -> Result<crate::locked::Password> {
let mut opts = tokio::process::Command::new(pinentry);
opts.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped());
+ let mut args = vec!["-o", "0"];
if let Some(tty) = tty {
- opts.args(&["-T", tty, "-o", "0"]);
- } else {
- opts.args(&["-o", "0"]);
+ args.extend(&["-T", tty]);
}
+ if !grab {
+ args.push("-g");
+ }
+ opts.args(args);
let mut child = opts.spawn().map_err(|source| Error::Spawn { source })?;
// unwrap is safe because we specified stdin as piped in the command opts
// above
diff --git a/src/protocol.rs b/src/protocol.rs
index a4b9722..14fa7f9 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -23,6 +23,7 @@ pub struct Request {
#[serde(tag = "type")]
pub enum Action {
Login,
+ Register,
Unlock,
CheckLock,
Lock,