aboutsummaryrefslogtreecommitdiffstats
path: root/src/api.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api.rs')
-rw-r--r--src/api.rs362
1 files changed, 319 insertions, 43 deletions
diff --git a/src/api.rs b/src/api.rs
index 75ab80e..9a58f9e 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -8,6 +8,8 @@ use crate::json::{
DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _,
};
+use tokio::io::AsyncReadExt as _;
+
#[derive(
serde_repr::Serialize_repr,
serde_repr::Deserialize_repr,
@@ -39,7 +41,7 @@ impl std::fmt::Display for UriMatchType {
RegularExpression => "regular_expression",
Never => "never",
};
- write!(f, "{}", s)
+ write!(f, "{s}")
}
}
@@ -55,6 +57,33 @@ pub enum TwoFactorProviderType {
WebAuthn = 7,
}
+impl TwoFactorProviderType {
+ #[must_use]
+ pub fn message(&self) -> &str {
+ match *self {
+ Self::Authenticator => "Enter the 6 digit verification code from your authenticator app.",
+ Self::Yubikey => "Insert your Yubikey and push the button.",
+ Self::Email => "Enter the PIN you received via email.",
+ _ => "Enter the code."
+ }
+ }
+
+ #[must_use]
+ pub fn header(&self) -> &str {
+ match *self {
+ Self::Authenticator => "Authenticator App",
+ Self::Yubikey => "Yubikey",
+ Self::Email => "Email Code",
+ _ => "Two Factor Authentication",
+ }
+ }
+
+ #[must_use]
+ pub fn grab(&self) -> bool {
+ !matches!(self, Self::Email)
+ }
+}
+
impl<'de> serde::Deserialize<'de> for TwoFactorProviderType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
@@ -111,7 +140,7 @@ impl std::convert::TryFrom<u64> for TwoFactorProviderType {
6 => Ok(Self::OrganizationDuo),
7 => Ok(Self::WebAuthn),
_ => Err(Error::InvalidTwoFactorProvider {
- ty: format!("{}", ty),
+ ty: format!("{ty}"),
}),
}
}
@@ -135,6 +164,96 @@ impl std::str::FromStr for TwoFactorProviderType {
}
}
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum KdfType {
+ Pbkdf2 = 0,
+ Argon2id = 1,
+}
+
+impl<'de> serde::Deserialize<'de> for KdfType {
+ fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct KdfTypeVisitor;
+ impl<'de> serde::de::Visitor<'de> for KdfTypeVisitor {
+ type Value = KdfType;
+
+ fn expecting(
+ &self,
+ formatter: &mut std::fmt::Formatter,
+ ) -> std::fmt::Result {
+ formatter.write_str("kdf id")
+ }
+
+ fn visit_str<E>(
+ self,
+ value: &str,
+ ) -> std::result::Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ value.parse().map_err(serde::de::Error::custom)
+ }
+
+ fn visit_u64<E>(
+ self,
+ value: u64,
+ ) -> std::result::Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ std::convert::TryFrom::try_from(value)
+ .map_err(serde::de::Error::custom)
+ }
+ }
+
+ deserializer.deserialize_any(KdfTypeVisitor)
+ }
+}
+
+impl std::convert::TryFrom<u64> for KdfType {
+ type Error = Error;
+
+ fn try_from(ty: u64) -> Result<Self> {
+ match ty {
+ 0 => Ok(Self::Pbkdf2),
+ 1 => Ok(Self::Argon2id),
+ _ => Err(Error::InvalidKdfType {
+ ty: format!("{ty}"),
+ }),
+ }
+ }
+}
+
+impl std::str::FromStr for KdfType {
+ type Err = Error;
+
+ fn from_str(ty: &str) -> Result<Self> {
+ match ty {
+ "0" => Ok(Self::Pbkdf2),
+ "1" => Ok(Self::Argon2id),
+ _ => Err(Error::InvalidKdfType { ty: ty.to_string() }),
+ }
+ }
+}
+
+impl serde::Serialize for KdfType {
+ fn serialize<S>(
+ &self,
+ serializer: S,
+ ) -> std::result::Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let s = match self {
+ Self::Pbkdf2 => "0",
+ Self::Argon2id => "1",
+ };
+ serializer.serialize_str(s)
+ }
+}
+
#[derive(serde::Serialize, Debug)]
struct PreloginReq {
email: String,
@@ -142,8 +261,14 @@ struct PreloginReq {
#[derive(serde::Deserialize, Debug)]
struct PreloginRes {
+ #[serde(rename = "Kdf", alias = "kdf")]
+ kdf: KdfType,
#[serde(rename = "KdfIterations", alias = "kdfIterations")]
kdf_iterations: u32,
+ #[serde(rename = "KdfMemory", alias = "kdfMemory")]
+ kdf_memory: Option<u32>,
+ #[serde(rename = "KdfParallelism", alias = "kdfParallelism")]
+ kdf_parallelism: Option<u32>,
}
#[derive(serde::Serialize, Debug)]
@@ -237,7 +362,7 @@ struct SyncResCipher {
#[serde(rename = "PasswordHistory", alias = "passwordHistory")]
password_history: Option<Vec<SyncResPasswordHistory>>,
#[serde(rename = "Fields", alias = "fields")]
- fields: Option<Vec<SyncResField>>,
+ fields: Option<Vec<CipherField>>,
#[serde(rename = "DeletedDate", alias = "deletedDate")]
deleted_date: Option<String>,
}
@@ -338,8 +463,10 @@ impl SyncResCipher {
fields
.iter()
.map(|field| crate::db::Field {
+ ty: field.ty,
name: field.name.clone(),
value: field.value.clone(),
+ linked_id: field.linked_id,
})
.collect()
});
@@ -457,6 +584,75 @@ struct CipherIdentity {
username: Option<String>,
}
+#[derive(
+ serde_repr::Serialize_repr,
+ serde_repr::Deserialize_repr,
+ Debug,
+ Clone,
+ Copy,
+ PartialEq,
+ Eq,
+)]
+#[repr(u16)]
+pub enum FieldType {
+ Text = 0,
+ Hidden = 1,
+ Boolean = 2,
+ Linked = 3,
+}
+
+#[derive(
+ serde_repr::Serialize_repr,
+ serde_repr::Deserialize_repr,
+ Debug,
+ Clone,
+ Copy,
+ PartialEq,
+ Eq,
+)]
+#[repr(u16)]
+pub enum LinkedIdType {
+ LoginUsername = 100,
+ LoginPassword = 101,
+ CardCardholderName = 300,
+ CardExpMonth = 301,
+ CardExpYear = 302,
+ CardCode = 303,
+ CardBrand = 304,
+ CardNumber = 305,
+ IdentityTitle = 400,
+ IdentityMiddleName = 401,
+ IdentityAddress1 = 402,
+ IdentityAddress2 = 403,
+ IdentityAddress3 = 404,
+ IdentityCity = 405,
+ IdentityState = 406,
+ IdentityPostalCode = 407,
+ IdentityCountry = 408,
+ IdentityCompany = 409,
+ IdentityEmail = 410,
+ IdentityPhone = 411,
+ IdentitySsn = 412,
+ IdentityUsername = 413,
+ IdentityPassportNumber = 414,
+ IdentityLicenseNumber = 415,
+ IdentityFirstName = 416,
+ IdentityLastName = 417,
+ IdentityFullName = 418,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
+struct CipherField {
+ #[serde(rename = "Type", alias = "type")]
+ ty: FieldType,
+ #[serde(rename = "Name", alias = "name")]
+ name: Option<String>,
+ #[serde(rename = "Value", alias = "value")]
+ value: Option<String>,
+ #[serde(rename = "LinkedId", alias = "linkedId")]
+ linked_id: Option<LinkedIdType>,
+}
+
// this is just a name and some notes, both of which are already on the cipher
// object
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
@@ -470,16 +666,6 @@ struct SyncResPasswordHistory {
password: Option<String>,
}
-#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
-struct SyncResField {
- #[serde(rename = "Type", alias = "type")]
- ty: u32,
- #[serde(rename = "Name", alias = "name")]
- name: Option<String>,
- #[serde(rename = "Value", alias = "value")]
- value: Option<String>,
-}
-
#[derive(serde::Serialize, Debug)]
struct CiphersPostReq {
#[serde(rename = "type")]
@@ -508,6 +694,7 @@ struct CiphersPutReq {
login: Option<CipherLogin>,
card: Option<CipherCard>,
identity: Option<CipherIdentity>,
+ fields: Vec<CipherField>,
#[serde(rename = "secureNote")]
secure_note: Option<CipherSecureNote>,
#[serde(rename = "passwordHistory")]
@@ -551,22 +738,70 @@ struct FoldersPostReq {
pub struct Client {
base_url: String,
identity_url: String,
+ client_cert_path: Option<std::path::PathBuf>,
}
impl Client {
#[must_use]
- pub fn new(base_url: &str, identity_url: &str) -> Self {
+ pub fn new(
+ base_url: &str,
+ identity_url: &str,
+ client_cert_path: Option<&std::path::Path>,
+ ) -> Self {
Self {
base_url: base_url.to_string(),
identity_url: identity_url.to_string(),
+ client_cert_path: client_cert_path
+ .map(std::path::Path::to_path_buf),
+ }
+ }
+
+ async fn reqwest_client(&self) -> Result<reqwest::Client> {
+ if let Some(client_cert_path) = self.client_cert_path.as_ref() {
+ let mut buf = Vec::new();
+ let mut f = tokio::fs::File::open(client_cert_path)
+ .await
+ .map_err(|e| Error::LoadClientCert {
+ source: e,
+ file: client_cert_path.clone(),
+ })?;
+ f.read_to_end(&mut buf).await.map_err(|e| {
+ Error::LoadClientCert {
+ source: e,
+ file: client_cert_path.clone(),
+ }
+ })?;
+ let pem = reqwest::Identity::from_pem(&buf)
+ .map_err(|e| Error::CreateReqwestClient { source: e })?;
+ Ok(reqwest::Client::builder()
+ .user_agent(format!(
+ "{}/{}",
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_VERSION")
+ ))
+ .identity(pem)
+ .build()
+ .map_err(|e| Error::CreateReqwestClient { source: e })?)
+ } else {
+ Ok(reqwest::Client::builder()
+ .user_agent(format!(
+ "{}/{}",
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_VERSION")
+ ))
+ .build()
+ .map_err(|e| Error::CreateReqwestClient { source: e })?)
}
}
- pub async fn prelogin(&self, email: &str) -> Result<u32> {
+ pub async fn prelogin(
+ &self,
+ email: &str,
+ ) -> Result<(KdfType, u32, Option<u32>, Option<u32>)> {
let prelogin = PreloginReq {
email: email.to_string(),
};
- let client = reqwest::Client::new();
+ let client = self.reqwest_client().await?;
let res = client
.post(&self.api_url("/accounts/prelogin"))
.json(&prelogin)
@@ -574,7 +809,12 @@ impl Client {
.await
.map_err(|source| Error::Reqwest { source })?;
let prelogin_res: PreloginRes = res.json_with_path().await?;
- Ok(prelogin_res.kdf_iterations)
+ Ok((
+ prelogin_res.kdf,
+ prelogin_res.kdf_iterations,
+ prelogin_res.kdf_memory,
+ prelogin_res.kdf_parallelism,
+ ))
}
pub async fn register(
@@ -597,11 +837,11 @@ impl Client {
device_type: 8,
device_identifier: device_id.to_string(),
device_name: "rbw".to_string(),
- device_push_token: "".to_string(),
+ device_push_token: String::new(),
two_factor_token: None,
two_factor_provider: None,
};
- let client = reqwest::Client::new();
+ let client = self.reqwest_client().await?;
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)
@@ -612,7 +852,19 @@ impl Client {
Ok(())
} else {
let code = res.status().as_u16();
- Err(classify_login_error(&res.json_with_path().await?, code))
+ match res.text().await {
+ Ok(body) => match body.clone().json_with_path() {
+ Ok(json) => Err(classify_login_error(&json, code)),
+ Err(e) => {
+ log::warn!("{e}: {body}");
+ Err(Error::RequestFailed { status: code })
+ }
+ },
+ Err(e) => {
+ log::warn!("failed to read response body: {e}");
+ Err(Error::RequestFailed { status: code })
+ }
+ }
}
}
@@ -627,28 +879,25 @@ impl Client {
let connect_req = ConnectPasswordReq {
grant_type: "password".to_string(),
username: email.to_string(),
- password: Some(base64::encode(password_hash.hash())),
+ password: Some(crate::base64::encode(password_hash.hash())),
scope: "api offline_access".to_string(),
client_id: "desktop".to_string(),
client_secret: None,
device_type: 8,
device_identifier: device_id.to_string(),
device_name: "rbw".to_string(),
- device_push_token: "".to_string(),
+ device_push_token: String::new(),
two_factor_token: two_factor_token
.map(std::string::ToString::to_string),
- // enum casts are safe, and i don't think there's a better way to
- // write it without some explicit impls
- #[allow(clippy::as_conversions)]
two_factor_provider: two_factor_provider.map(|ty| ty as u32),
};
- let client = reqwest::Client::new();
+ let client = self.reqwest_client().await?;
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)
.header(
"auth-email",
- base64::encode_config(email, base64::URL_SAFE_NO_PAD),
+ crate::base64::encode_url_safe_no_pad(email),
)
.send()
.await
@@ -663,7 +912,19 @@ impl Client {
))
} else {
let code = res.status().as_u16();
- Err(classify_login_error(&res.json_with_path().await?, code))
+ match res.text().await {
+ Ok(body) => match body.clone().json_with_path() {
+ Ok(json) => Err(classify_login_error(&json, code)),
+ Err(e) => {
+ log::warn!("{e}: {body}");
+ Err(Error::RequestFailed { status: code })
+ }
+ },
+ Err(e) => {
+ log::warn!("failed to read response body: {e}");
+ Err(Error::RequestFailed { status: code })
+ }
+ }
}
}
@@ -676,10 +937,10 @@ impl Client {
std::collections::HashMap<String, String>,
Vec<crate::db::Entry>,
)> {
- let client = reqwest::Client::new();
+ let client = self.reqwest_client().await?;
let res = client
.get(&self.api_url("/sync"))
- .header("Authorization", format!("Bearer {}", access_token))
+ .header("Authorization", format!("Bearer {access_token}"))
.send()
.await
.map_err(|source| Error::Reqwest { source })?;
@@ -820,8 +1081,8 @@ impl Client {
}
let client = reqwest::blocking::Client::new();
let res = client
- .post(&self.api_url("/ciphers"))
- .header("Authorization", format!("Bearer {}", access_token))
+ .post(self.api_url("/ciphers"))
+ .header("Authorization", format!("Bearer {access_token}"))
.json(&req)
.send()
.map_err(|source| Error::Reqwest { source })?;
@@ -843,12 +1104,18 @@ impl Client {
org_id: Option<&str>,
name: &str,
data: &crate::db::EntryData,
+ fields: &[crate::db::Field],
notes: Option<&str>,
folder_uuid: Option<&str>,
history: &[crate::db::HistoryEntry],
) -> Result<()> {
let mut req = CiphersPutReq {
- ty: 1,
+ ty: match data {
+ crate::db::EntryData::Login { .. } => 1,
+ crate::db::EntryData::SecureNote { .. } => 2,
+ crate::db::EntryData::Card { .. } => 3,
+ crate::db::EntryData::Identity { .. } => 4,
+ },
folder_id: folder_uuid.map(std::string::ToString::to_string),
organization_id: org_id.map(std::string::ToString::to_string),
name: name.to_string(),
@@ -857,6 +1124,15 @@ impl Client {
card: None,
identity: None,
secure_note: None,
+ fields: fields
+ .iter()
+ .map(|field| CipherField {
+ ty: field.ty,
+ name: field.name.clone(),
+ value: field.value.clone(),
+ linked_id: field.linked_id,
+ })
+ .collect(),
password_history: history
.iter()
.map(|entry| CiphersPutReqHistory {
@@ -953,8 +1229,8 @@ impl Client {
}
let client = reqwest::blocking::Client::new();
let res = client
- .put(&self.api_url(&format!("/ciphers/{}", id)))
- .header("Authorization", format!("Bearer {}", access_token))
+ .put(self.api_url(&format!("/ciphers/{id}")))
+ .header("Authorization", format!("Bearer {access_token}"))
.json(&req)
.send()
.map_err(|source| Error::Reqwest { source })?;
@@ -972,8 +1248,8 @@ impl Client {
pub fn remove(&self, access_token: &str, id: &str) -> Result<()> {
let client = reqwest::blocking::Client::new();
let res = client
- .delete(&self.api_url(&format!("/ciphers/{}", id)))
- .header("Authorization", format!("Bearer {}", access_token))
+ .delete(self.api_url(&format!("/ciphers/{id}")))
+ .header("Authorization", format!("Bearer {access_token}"))
.send()
.map_err(|source| Error::Reqwest { source })?;
match res.status() {
@@ -993,8 +1269,8 @@ impl Client {
) -> Result<Vec<(String, String)>> {
let client = reqwest::blocking::Client::new();
let res = client
- .get(&self.api_url("/folders"))
- .header("Authorization", format!("Bearer {}", access_token))
+ .get(self.api_url("/folders"))
+ .header("Authorization", format!("Bearer {access_token}"))
.send()
.map_err(|source| Error::Reqwest { source })?;
match res.status() {
@@ -1025,8 +1301,8 @@ impl Client {
};
let client = reqwest::blocking::Client::new();
let res = client
- .post(&self.api_url("/folders"))
- .header("Authorization", format!("Bearer {}", access_token))
+ .post(self.api_url("/folders"))
+ .header("Authorization", format!("Bearer {access_token}"))
.json(&req)
.send()
.map_err(|source| Error::Reqwest { source })?;
@@ -1055,7 +1331,7 @@ impl Client {
};
let client = reqwest::blocking::Client::new();
let res = client
- .post(&self.identity_url("/connect/token"))
+ .post(self.identity_url("/connect/token"))
.form(&connect_req)
.send()
.map_err(|source| Error::Reqwest { source })?;
@@ -1072,7 +1348,7 @@ impl Client {
client_id: "desktop".to_string(),
refresh_token: refresh_token.to_string(),
};
- let client = reqwest::Client::new();
+ let client = self.reqwest_client().await?;
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)