From 840f53b84206f9efe0a5cdea3414c472908a22e4 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sat, 18 Apr 2020 01:30:32 -0400 Subject: implement add command --- src/actions.rs | 21 ++++++- src/api.rs | 133 ++++++++++++++++++++++++++++++++++++++++++- src/bin/rbw-agent/actions.rs | 2 +- src/bin/rbw/commands.rs | 70 ++++++++++++++++++++++- src/bin/rbw/main.rs | 13 ++++- 5 files changed, 231 insertions(+), 8 deletions(-) (limited to 'src') 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 { +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 { + 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 { 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, + #[serde(rename = "organizationId")] + organization_id: Option, + name: String, + notes: Option, + favorite: bool, + login: CiphersPostReqLogin, +} + +#[derive(serde::Serialize, Debug)] +struct CiphersPostReqLogin { + uri: Option, + username: Option, + password: Option, + totp: Option, +} + +#[derive(serde::Deserialize, Debug)] +struct CiphersRes { + #[serde(rename = "FolderId")] + folder_id: Option, + #[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, + #[serde(rename = "Password")] + password: Option, + #[serde(rename = "Totp")] + totp: Option, + #[serde(rename = "Name")] + name: String, + #[serde(rename = "Notes")] + notes: Option, + #[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, +} + +#[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 { + 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 { 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(¬e)?) + }; + + 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 -- cgit v1.2.3-54-g00ecf