aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-04-18 05:18:45 -0400
committerJesse Luehrs <doy@tozt.net>2020-04-18 05:18:45 -0400
commit50ab8aabcec26d07f4b4d9652dbceac1b89a6b22 (patch)
treeeadd12b800d47e721f0941d39a6826dbdb4d6353
parent017f1b04df0701e21e7cb44e3e4f6fc505d95638 (diff)
downloadrbw-50ab8aabcec26d07f4b4d9652dbceac1b89a6b22.tar.gz
rbw-50ab8aabcec26d07f4b4d9652dbceac1b89a6b22.zip
implement edit command
-rw-r--r--src/actions.rs35
-rw-r--r--src/api.rs54
-rw-r--r--src/bin/rbw/commands.rs116
-rw-r--r--src/bin/rbw/main.rs13
4 files changed, 186 insertions, 32 deletions
diff --git a/src/actions.rs b/src/actions.rs
index 60cc61d..634fa9a 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -102,6 +102,41 @@ fn add_once(
Ok(())
}
+pub fn edit(
+ access_token: &str,
+ refresh_token: &str,
+ id: &str,
+ name: &str,
+ username: Option<&str>,
+ password: Option<&str>,
+ notes: Option<&str>,
+) -> Result<Option<String>> {
+ match edit_once(access_token, id, name, username, password, notes) {
+ Ok(()) => Ok(None),
+ Err(crate::error::Error::RequestUnauthorized) => {
+ let access_token = exchange_refresh_token(refresh_token)?;
+ edit_once(&access_token, id, name, username, password, notes)?;
+ Ok(Some(access_token))
+ }
+ Err(e) => Err(e),
+ }
+}
+
+fn edit_once(
+ access_token: &str,
+ id: &str,
+ name: &str,
+ username: Option<&str>,
+ password: Option<&str>,
+ notes: Option<&str>,
+) -> Result<()> {
+ let config = crate::config::Config::load()?;
+ let client =
+ crate::api::Client::new(&config.base_url(), &config.identity_url());
+ client.edit(access_token, id, name, username, password, notes)?;
+ Ok(())
+}
+
pub fn remove(
access_token: &str,
refresh_token: &str,
diff --git a/src/api.rs b/src/api.rs
index f40ff6d..5cf5485 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -87,6 +87,21 @@ struct CiphersPostReqLogin {
totp: Option<String>,
}
+#[derive(serde::Serialize, Debug)]
+struct CiphersPutReq {
+ #[serde(rename = "type")]
+ ty: u32, // XXX what are the valid types?
+ name: String,
+ notes: Option<String>,
+ login: CiphersPutReqLogin,
+}
+
+#[derive(serde::Serialize, Debug)]
+struct CiphersPutReqLogin {
+ username: Option<String>,
+ password: Option<String>,
+}
+
#[derive(serde::Deserialize, Debug)]
struct CiphersRes {
#[serde(rename = "FolderId")]
@@ -311,6 +326,45 @@ impl Client {
}
}
+ pub fn edit(
+ &self,
+ access_token: &str,
+ id: &str,
+ name: &str,
+ username: Option<&str>,
+ password: Option<&str>,
+ notes: Option<&str>,
+ ) -> Result<()> {
+ let req = CiphersPutReq {
+ ty: 1,
+ name: name.to_string(),
+ notes: notes.map(std::string::ToString::to_string),
+ login: CiphersPutReqLogin {
+ username: username.map(std::string::ToString::to_string),
+ password: password.map(std::string::ToString::to_string),
+ },
+ };
+ let client = reqwest::blocking::Client::new();
+ let res = client
+ .put(&self.api_url(&format!("/ciphers/{}", id)))
+ .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)
+ }
+ _ => {
+ let code = res.status().as_u16();
+ let text = res.text().unwrap();
+ eprintln!("error: {}", text);
+ Err(Error::RequestFailed { status: code })
+ }
+ }
+ }
+
pub fn remove(&self, access_token: &str, id: &str) -> Result<()> {
let client = reqwest::blocking::Client::new();
let res = client
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs
index da94ffa..42fb8ee 100644
--- a/src/bin/rbw/commands.rs
+++ b/src/bin/rbw/commands.rs
@@ -98,9 +98,9 @@ pub fn get(name: &str, user: Option<&str>) -> anyhow::Result<()> {
name
);
- let entry = find_entry(&db, name, user)
+ let (_, decrypted) = find_entry(&db, name, user)
.with_context(|| format!("couldn't find entry for '{}'", desc))?;
- if let Some(password) = entry.password {
+ if let Some(password) = decrypted.password {
println!("{}", password);
} else {
eprintln!("entry for '{}' had no password", desc);
@@ -126,32 +126,21 @@ pub fn add(name: &str, username: Option<&str>) -> anyhow::Result<()> {
.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 notes: String = lines
- .skip_while(|line| *line == "")
- .filter(|line| !line.starts_with('#'))
- .map(|line| format!("{}\n", line))
- .collect();
- while notes.ends_with('\n') {
- notes.pop();
- }
- let notes = if notes == "" {
- None
- } else {
- Some(crate::actions::encrypt(&notes)?)
- };
+ let (password, notes) = parse_editor(&contents);
+ let password = password
+ .map(|password| crate::actions::encrypt(&password))
+ .transpose()?;
+ let notes = notes
+ .map(|notes| crate::actions::encrypt(&notes))
+ .transpose()?;
if let Some(access_token) = rbw::actions::add(
&access_token,
&refresh_token,
&name,
username.as_deref(),
- Some(&password),
+ password.as_deref(),
notes.as_deref(),
)? {
db.access_token = Some(access_token);
@@ -206,10 +195,57 @@ pub fn generate(
Ok(())
}
-pub fn edit() -> anyhow::Result<()> {
+pub fn edit(name: &str, username: Option<&str>) -> anyhow::Result<()> {
unlock()?;
- todo!()
+ let email = config_email()?;
+ let mut db = rbw::db::Db::load(&email)
+ .context("failed to load password database")?;
+ let access_token = db.access_token.as_ref().unwrap();
+ let refresh_token = db.refresh_token.as_ref().unwrap();
+
+ let desc = format!(
+ "{}{}",
+ username
+ .map(|s| format!("{}@", s))
+ .unwrap_or_else(|| "".to_string()),
+ name
+ );
+
+ let (entry, decrypted) = find_entry(&db, name, username)
+ .with_context(|| format!("couldn't find entry for '{}'", desc))?;
+
+ let mut contents =
+ format!("{}\n", decrypted.password.unwrap_or_else(String::new));
+ if let Some(notes) = decrypted.notes {
+ contents.push_str(&format!("\n{}\n", notes));
+ }
+
+ let contents = rbw::edit::edit(&contents, HELP)?;
+
+ let (password, notes) = parse_editor(&contents);
+ let password = password
+ .map(|password| crate::actions::encrypt(&password))
+ .transpose()?;
+ let notes = notes
+ .map(|notes| crate::actions::encrypt(&notes))
+ .transpose()?;
+
+ if let Some(access_token) = rbw::actions::edit(
+ &access_token,
+ &refresh_token,
+ &entry.id,
+ &entry.name,
+ entry.username.as_deref(),
+ password.as_deref(),
+ notes.as_deref(),
+ )? {
+ db.access_token = Some(access_token);
+ db.save(&email).context("failed to save database")?;
+ }
+
+ crate::actions::sync()?;
+ Ok(())
}
pub fn remove(name: &str, username: Option<&str>) -> anyhow::Result<()> {
@@ -229,7 +265,7 @@ pub fn remove(name: &str, username: Option<&str>) -> anyhow::Result<()> {
name
);
- let entry = find_entry(&db, name, username)
+ let (entry, _) = find_entry(&db, name, username)
.with_context(|| format!("couldn't find entry for '{}'", desc))?;
if let Some(access_token) =
@@ -293,14 +329,16 @@ fn find_entry(
db: &rbw::db::Db,
name: &str,
username: Option<&str>,
-) -> anyhow::Result<DecryptedCipher> {
- let ciphers: anyhow::Result<Vec<DecryptedCipher>> = db
+) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> {
+ let ciphers: anyhow::Result<Vec<(rbw::db::Entry, DecryptedCipher)>> = db
.entries
.iter()
.cloned()
- .map(decrypt_cipher)
+ .map(|entry| {
+ decrypt_cipher(&entry).map(|decrypted| (entry, decrypted))
+ })
.filter(|res| {
- if let Ok(decrypted_cipher) = res {
+ if let Ok((_, decrypted_cipher)) = res {
name == decrypted_cipher.name
&& if let Some(username) = username {
decrypted_cipher.username.as_deref() == Some(username)
@@ -319,8 +357,8 @@ fn find_entry(
} else if ciphers.len() > 1 {
let users: Vec<String> = ciphers
.iter()
- .map(|cipher| {
- cipher
+ .map(|(_, decrypted)| {
+ decrypted
.username
.clone()
.unwrap_or_else(|| "(no login)".to_string())
@@ -379,6 +417,24 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> {
})
}
+fn parse_editor(contents: &str) -> (Option<String>, Option<String>) {
+ let mut lines = contents.lines();
+
+ let password = lines.next().map(std::string::ToString::to_string);
+
+ let mut notes: String = lines
+ .skip_while(|line| *line == "")
+ .filter(|line| !line.starts_with('#'))
+ .map(|line| format!("{}\n", line))
+ .collect();
+ while notes.ends_with('\n') {
+ notes.pop();
+ }
+ let notes = if notes == "" { None } else { Some(notes) };
+
+ (password, notes)
+}
+
fn config_email() -> anyhow::Result<String> {
let config = rbw::config::Config::load()?;
if let Some(email) = config.email {
diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs
index 5bd6032..b09ae2b 100644
--- a/src/bin/rbw/main.rs
+++ b/src/bin/rbw/main.rs
@@ -58,7 +58,11 @@ fn main() {
"diceware",
])),
)
- .subcommand(clap::SubCommand::with_name("edit"))
+ .subcommand(
+ clap::SubCommand::with_name("edit")
+ .arg(clap::Arg::with_name("name").required(true))
+ .arg(clap::Arg::with_name("user")),
+ )
.subcommand(
clap::SubCommand::with_name("remove")
.arg(clap::Arg::with_name("name").required(true))
@@ -127,7 +131,12 @@ fn main() {
Err(e) => Err(e.into()),
}
}
- ("edit", Some(_)) => commands::edit().context("edit"),
+ // this unwrap is safe because name is marked .required(true)
+ ("edit", Some(smatches)) => commands::edit(
+ smatches.value_of("name").unwrap(),
+ smatches.value_of("user"),
+ )
+ .context("edit"),
// this unwrap is safe because name is marked .required(true)
("remove", Some(smatches)) => commands::remove(
smatches.value_of("name").unwrap(),