aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-04-18 01:30:32 -0400
committerJesse Luehrs <doy@tozt.net>2020-04-18 01:58:08 -0400
commit840f53b84206f9efe0a5cdea3414c472908a22e4 (patch)
treee8d95614a3f0c1ae1bfd4c9616ce6143cc3e6e0b
parentdd7ef574e84ca6c1a548422904c6865048ae8e30 (diff)
downloadrbw-840f53b84206f9efe0a5cdea3414c472908a22e4.tar.gz
rbw-840f53b84206f9efe0a5cdea3414c472908a22e4.zip
implement add command
-rw-r--r--src/actions.rs21
-rw-r--r--src/api.rs133
-rw-r--r--src/bin/rbw-agent/actions.rs2
-rw-r--r--src/bin/rbw/commands.rs70
-rw-r--r--src/bin/rbw/main.rs13
5 files changed, 231 insertions, 8 deletions
diff --git a/src/actions.rs b/src/actions.rs
index c942f32..07cf6ba 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -52,9 +52,26 @@ pub async fn sync(
client.sync(access_token).await
}
-pub async fn exchange_refresh_token(refresh_token: &str) -> Result<String> {
+pub fn add(access_token: &str, cipher: &crate::api::Cipher) -> Result<()> {
+ let config = crate::config::Config::load()?;
+ let client =
+ crate::api::Client::new(&config.base_url(), &config.identity_url());
+ client.add(access_token, cipher)?;
+ Ok(())
+}
+
+pub fn exchange_refresh_token(refresh_token: &str) -> Result<String> {
+ let config = crate::config::Config::load()?;
+ let client =
+ crate::api::Client::new(&config.base_url(), &config.identity_url());
+ client.exchange_refresh_token(refresh_token)
+}
+
+pub async fn exchange_refresh_token_async(
+ refresh_token: &str,
+) -> Result<String> {
let config = crate::config::Config::load_async().await?;
let client =
crate::api::Client::new(&config.base_url(), &config.identity_url());
- client.exchange_refresh_token(refresh_token).await
+ client.exchange_refresh_token_async(refresh_token).await
}
diff --git a/src/api.rs b/src/api.rs
index b578348..b5502e6 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -65,6 +65,79 @@ struct SyncRes {
profile: Profile,
}
+#[derive(serde::Serialize, Debug)]
+struct CiphersPostReq {
+ #[serde(rename = "type")]
+ ty: u32, // XXX what are the valid types?
+ #[serde(rename = "folderId")]
+ folder_id: Option<String>,
+ #[serde(rename = "organizationId")]
+ organization_id: Option<String>,
+ name: String,
+ notes: Option<String>,
+ favorite: bool,
+ login: CiphersPostReqLogin,
+}
+
+#[derive(serde::Serialize, Debug)]
+struct CiphersPostReqLogin {
+ uri: Option<String>,
+ username: Option<String>,
+ password: Option<String>,
+ totp: Option<String>,
+}
+
+#[derive(serde::Deserialize, Debug)]
+struct CiphersRes {
+ #[serde(rename = "FolderId")]
+ folder_id: Option<String>,
+ #[serde(rename = "Favorite")]
+ favorite: bool,
+ #[serde(rename = "Edit")]
+ edit: bool,
+ #[serde(rename = "Id")]
+ id: String,
+ #[serde(rename = "OrganizationId")]
+ organization_id: String,
+ #[serde(rename = "Type")]
+ ty: u32,
+ #[serde(rename = "Login")]
+ login: CiphersResLogin,
+ #[serde(rename = "Username")]
+ username: Option<String>,
+ #[serde(rename = "Password")]
+ password: Option<String>,
+ #[serde(rename = "Totp")]
+ totp: Option<String>,
+ #[serde(rename = "Name")]
+ name: String,
+ #[serde(rename = "Notes")]
+ notes: Option<String>,
+ #[serde(rename = "Fields")]
+ fields: Option<()>, // XXX what type is this?
+ #[serde(rename = "Attachments")]
+ attachments: Option<()>, // XXX what type is this?
+ #[serde(rename = "OrganizationUseTotp")]
+ organization_use_totp: bool,
+ #[serde(rename = "RevisionDate")]
+ revision_date: String,
+ #[serde(rename = "Object")]
+ object: String,
+}
+
+#[derive(serde::Deserialize, Debug)]
+struct CiphersResLogin {
+ uris: Vec<CiphersResLoginUri>,
+}
+
+#[derive(serde::Deserialize, Debug)]
+struct CiphersResLoginUri {
+ #[serde(rename = "Uri")]
+ uri: String,
+ #[serde(rename = "Match")]
+ mtch: Option<()>, // XXX what type is this?
+}
+
#[derive(serde::Deserialize, Debug)]
struct Profile {
#[serde(rename = "Key")]
@@ -177,7 +250,65 @@ impl Client {
}
}
- pub async fn exchange_refresh_token(
+ pub fn add(
+ &self,
+ access_token: &str,
+ cipher: &Cipher,
+ // TODO: note
+ ) -> Result<()> {
+ let req = CiphersPostReq {
+ ty: 1,
+ folder_id: None,
+ organization_id: None,
+ name: cipher.name.clone(),
+ notes: None,
+ favorite: false,
+ login: CiphersPostReqLogin {
+ uri: None,
+ username: cipher.login.username.clone(),
+ password: cipher.login.password.clone(),
+ totp: None,
+ },
+ };
+ let client = reqwest::blocking::Client::new();
+ let res = client
+ .post(&self.api_url("/ciphers"))
+ .header("Authorization", format!("Bearer {}", access_token))
+ .json(&req)
+ .send()
+ .context(crate::error::Reqwest)?;
+ match res.status() {
+ reqwest::StatusCode::OK => Ok(()),
+ reqwest::StatusCode::UNAUTHORIZED => {
+ Err(Error::RequestUnauthorized)
+ }
+ _ => Err(Error::RequestFailed {
+ status: res.status().as_u16(),
+ }),
+ }
+ }
+
+ pub fn exchange_refresh_token(
+ &self,
+ refresh_token: &str,
+ ) -> Result<String> {
+ let connect_req = ConnectRefreshTokenReq {
+ grant_type: "refresh_token".to_string(),
+ client_id: "desktop".to_string(),
+ refresh_token: refresh_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: ConnectRefreshTokenRes =
+ res.json().context(crate::error::Reqwest)?;
+ Ok(connect_res.access_token)
+ }
+
+ pub async fn exchange_refresh_token_async(
&self,
refresh_token: &str,
) -> Result<String> {
diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs
index 7289a25..9559f73 100644
--- a/src/bin/rbw-agent/actions.rs
+++ b/src/bin/rbw-agent/actions.rs
@@ -137,7 +137,7 @@ pub async fn sync(sock: &mut crate::sock::Sock) -> anyhow::Result<()> {
if let rbw::error::Error::RequestUnauthorized = e {
if let Some(refresh_token) = &db.refresh_token {
let access_token =
- rbw::actions::exchange_refresh_token(refresh_token)
+ rbw::actions::exchange_refresh_token_async(refresh_token)
.await?;
db.access_token = Some(access_token.clone());
rbw::actions::sync(&access_token).await
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs
index 5f1c2f1..360c967 100644
--- a/src/bin/rbw/commands.rs
+++ b/src/bin/rbw/commands.rs
@@ -1,5 +1,11 @@
use anyhow::Context as _;
+const HELP: &str = r#"
+# The first line of this file will be the password, and the remainder of the
+# file (after any blank lines after the password) will be stored as a note.
+# Lines with leading # will be ignored.
+"#;
+
pub fn config_show() -> anyhow::Result<()> {
let config =
rbw::config::Config::load().context("failed to load config")?;
@@ -120,10 +126,70 @@ pub fn get(name: &str, user: Option<&str>) -> anyhow::Result<()> {
Ok(())
}
-pub fn add() -> anyhow::Result<()> {
+pub fn add(name: &str, username: Option<&str>) -> anyhow::Result<()> {
unlock()?;
- todo!()
+ let email = config_email()?;
+ let mut db = rbw::db::Db::load(&email)?;
+ // unwrap is safe here because the call to unlock above is guaranteed to
+ // populate it or error
+ let access_token = db.access_token.unwrap();
+
+ let name = crate::actions::encrypt(name)?;
+
+ let username = username
+ .map(|username| crate::actions::encrypt(username))
+ .transpose()?;
+
+ let contents = rbw::edit::edit("", HELP)?;
+ let mut lines = contents.lines();
+
+ // XXX unwrap
+ let password = lines.next().unwrap();
+ let password = crate::actions::encrypt(password)?;
+
+ let mut note: String = lines
+ .skip_while(|line| *line == "")
+ .filter(|line| !line.starts_with('#'))
+ .map(|line| format!("{}\n", line))
+ .collect();
+ while note.ends_with('\n') {
+ note.pop();
+ }
+ let note = if note == "" {
+ None
+ } else {
+ Some(crate::actions::encrypt(&note)?)
+ };
+
+ let cipher = rbw::api::Cipher {
+ name,
+ login: rbw::api::Login {
+ username,
+ password: Some(password),
+ },
+ };
+
+ let res = rbw::actions::add(&access_token, &cipher);
+ if let Err(e) = &res {
+ if let rbw::error::Error::RequestUnauthorized = e {
+ if let Some(refresh_token) = &db.refresh_token {
+ let access_token =
+ rbw::actions::exchange_refresh_token(refresh_token)?;
+ db.access_token = Some(access_token.clone());
+ db.save(&email).context("failed to save database")?;
+ rbw::actions::add(&access_token, &cipher)?;
+ } else {
+ return Err(anyhow::anyhow!(
+ "failed to find refresh token in db"
+ ));
+ }
+ }
+ }
+
+ crate::actions::sync()?;
+
+ Ok(())
}
pub fn generate(
diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs
index 1260edf..136322a 100644
--- a/src/bin/rbw/main.rs
+++ b/src/bin/rbw/main.rs
@@ -27,7 +27,11 @@ fn main() {
.arg(clap::Arg::with_name("name").required(true))
.arg(clap::Arg::with_name("user")),
)
- .subcommand(clap::SubCommand::with_name("add"))
+ .subcommand(
+ clap::SubCommand::with_name("add")
+ .arg(clap::Arg::with_name("name").required(true))
+ .arg(clap::Arg::with_name("user").required(true)),
+ )
.subcommand(
clap::SubCommand::with_name("generate")
.arg(clap::Arg::with_name("len").required(true))
@@ -83,7 +87,12 @@ fn main() {
smatches.value_of("user"),
)
.context("get"),
- ("add", Some(_)) => commands::add().context("add"),
+ // this unwrap is safe because name is marked .required(true)
+ ("add", Some(smatches)) => commands::add(
+ smatches.value_of("name").unwrap(),
+ smatches.value_of("user"),
+ )
+ .context("add"),
("generate", Some(smatches)) => {
let ty = if smatches.is_present("no-symbols") {
rbw::pwgen::Type::NoSymbols