Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub async fn login(
password: crate::locked::Password,
two_factor_token: Option<&str>,
two_factor_provider: Option<crate::api::TwoFactorProviderType>,
device_verification_code: Option<&str>,
) -> Result<(
String,
String,
Expand Down Expand Up @@ -47,6 +48,7 @@ pub async fn login(
&identity.master_password_hash,
two_factor_token,
two_factor_provider,
device_verification_code,
)
.await?;

Expand Down
14 changes: 14 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ struct ConnectTokenReq {
two_factor_token: Option<String>,
#[serde(rename = "twoFactorProvider")]
two_factor_provider: Option<u32>,
#[serde(
rename = "newDeviceOtp",
skip_serializing_if = "Option::is_none"
)]
device_verification_code: Option<String>,
#[serde(flatten)]
auth: ConnectTokenAuth,
}
Expand Down Expand Up @@ -943,6 +948,7 @@ impl Client {
device_push_token: String::new(),
two_factor_token: None,
two_factor_provider: None,
device_verification_code: None,
};
let client = self.reqwest_client().await?;
let res = client
Expand Down Expand Up @@ -979,6 +985,7 @@ impl Client {
password_hash: &crate::locked::PasswordHash,
two_factor_token: Option<&str>,
two_factor_provider: Option<TwoFactorProviderType>,
device_verification_code: Option<&str>,
) -> Result<(String, String, String)> {
let connect_req = match sso_id {
Some(sso_id) => {
Expand All @@ -1002,6 +1009,8 @@ impl Client {
.map(std::string::ToString::to_string),
two_factor_provider: two_factor_provider
.map(|ty| ty as u32),
device_verification_code: device_verification_code
.map(std::string::ToString::to_string),
}
}
None => ConnectTokenReq {
Expand All @@ -1020,6 +1029,8 @@ impl Client {
two_factor_token: two_factor_token
.map(std::string::ToString::to_string),
two_factor_provider: two_factor_provider.map(|ty| ty as u32),
device_verification_code: device_verification_code
.map(std::string::ToString::to_string),
},
};

Expand Down Expand Up @@ -1738,6 +1749,9 @@ fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error {
"invalid_client" => {
return Error::IncorrectApiKey;
}
"device_error" => {
return Error::NewDeviceVerificationRequired;
}
"" => {
// bitwarden_rs returns an empty error and error_description for
// this case, for some reason
Expand Down
134 changes: 132 additions & 2 deletions src/bin/rbw-agent/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,14 @@ pub async fn login(
)
.await
.context("failed to read password from pinentry")?;
match rbw::actions::login(&email, password.clone(), None, None)
.await
match rbw::actions::login(
&email,
password.clone(),
None,
None,
None,
)
.await
{
Ok((
access_token,
Expand All @@ -141,6 +147,37 @@ pub async fn login(
.await?;
break 'attempts;
}
Err(rbw::error::Error::NewDeviceVerificationRequired) => {
let (
access_token,
refresh_token,
kdf,
iterations,
memory,
parallelism,
protected_key,
) = device_verification(
environment,
&email,
password.clone(),
)
.await?;
login_success(
state.clone(),
access_token,
refresh_token,
kdf,
iterations,
memory,
parallelism,
protected_key,
password,
db,
email,
)
.await?;
break 'attempts;
}
Err(rbw::error::Error::TwoFactorRequired {
providers,
sso_email_2fa_session_token,
Expand Down Expand Up @@ -264,6 +301,7 @@ async fn two_factor(
password.clone(),
Some(code),
Some(provider),
None,
)
.await
{
Expand Down Expand Up @@ -316,6 +354,98 @@ async fn two_factor(
unreachable!()
}

async fn device_verification(
environment: &rbw::protocol::Environment,
email: &str,
password: rbw::locked::Password,
) -> anyhow::Result<(
String,
String,
rbw::api::KdfType,
u32,
Option<u32>,
Option<u32>,
String,
)> {
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 code = rbw::pinentry::getpin(
&config_pinentry().await?,
"New Device Verification",
"Check your email for a verification code",
err.as_deref(),
environment,
false,
)
.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.clone(),
None,
None,
Some(code),
)
.await
{
Ok((
access_token,
refresh_token,
kdf,
iterations,
memory,
parallelism,
protected_key,
)) => {
return Ok((
access_token,
refresh_token,
kdf,
iterations,
memory,
parallelism,
protected_key,
))
}
Err(rbw::error::Error::IncorrectPassword { message }) => {
if i == 3 {
return Err(rbw::error::Error::IncorrectPassword {
message,
})
.context("failed to log in to bitwarden instance");
}
err_msg = Some(message);
}
Err(rbw::error::Error::NewDeviceVerificationRequired) => {
let message =
"New device verification code is incorrect".to_string();
if i == 3 {
return Err(rbw::error::Error::IncorrectPassword {
message,
})
.context("failed to log in to bitwarden instance");
}
err_msg = Some(message);
}
Err(e) => {
return Err(e)
.context("failed to log in to bitwarden instance")
}
}
}

unreachable!()
}

async fn login_success(
state: std::sync::Arc<tokio::sync::Mutex<crate::state::State>>,
access_token: String,
Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ pub enum Error {
sso_email_2fa_session_token: Option<String>,
},

#[error("new device verification required")]
NewDeviceVerificationRequired,

#[error("unimplemented cipherstring type: {ty}")]
UnimplementedCipherStringType { ty: String },

Expand Down
Loading