use anyhow::{Context, Result, bail};
use std::io::Write;
use std::process::{Command, Stdio};
const FIELD: &str = "password";
pub fn op_available() -> Result<()> {
let status = Command::new("op")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match status {
Ok(s) if s.success() => Ok(()),
_ => bail!(
"1Password CLI `op` not found or not working. Install it from \
https://developer.1password.com/docs/cli/ and run `op signin` first."
),
}
}
fn run_op(args: &[&str], stdin_body: Option<&str>) -> Result<std::process::Output> {
let mut cmd = Command::new("op");
cmd.args(args)
.stdin(if stdin_body.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().context("failed to spawn `op`")?;
if let Some(body) = stdin_body {
child
.stdin
.take()
.context("failed to open `op` stdin")?
.write_all(body.as_bytes())
.context("failed to write to `op` stdin")?;
}
child.wait_with_output().context("failed to run `op`")
}
pub fn store(vault: Option<&str>, title: &str, blob: &str, public_key: &str) -> Result<()> {
let mut edit_args = vec!["item", "edit", title];
if let Some(v) = vault {
edit_args.push("--vault");
edit_args.push(v);
}
let assignment = format!("{FIELD}={blob}");
edit_args.push(&assignment);
edit_args.push("--format");
edit_args.push("json");
let edit = run_op(&edit_args, None)?;
if edit.status.success() {
return Ok(());
}
let template = serde_json::json!({
"title": title,
"category": "PASSWORD",
"fields": [
{ "id": "password", "label": "password", "type": "CONCEALED",
"purpose": "PASSWORD", "value": blob },
{ "label": "public_key", "type": "STRING", "value": public_key },
{ "id": "notesPlain", "label": "notesPlain", "type": "STRING", "purpose": "NOTES",
"value": "Rayfish encrypted identity backup. Restore with `ray pair restore --1password`." }
]
})
.to_string();
let mut create_args = vec!["item", "create", "--format", "json"];
if let Some(v) = vault {
create_args.push("--vault");
create_args.push(v);
}
create_args.push("-");
let create = run_op(&create_args, Some(&template))?;
if !create.status.success() {
let edit_err = String::from_utf8_lossy(&edit.stderr);
let create_err = String::from_utf8_lossy(&create.stderr);
bail!(
"failed to store backup in 1Password.\n edit: {}\n create: {}",
edit_err.trim(),
create_err.trim()
);
}
Ok(())
}
pub fn read(vault: Option<&str>, title: &str) -> Result<String> {
let fields = format!("label={FIELD}");
let mut args = vec!["item", "get", title];
if let Some(v) = vault {
args.push("--vault");
args.push(v);
}
args.push("--fields");
args.push(&fields);
args.push("--reveal");
args.push("--format");
args.push("json");
let out = run_op(&args, None)?;
if !out.status.success() {
bail!(
"failed to read backup from 1Password: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
}
let json: serde_json::Value =
serde_json::from_slice(&out.stdout).context("failed to parse `op` output")?;
let value = match &json {
serde_json::Value::Array(arr) => arr
.iter()
.find_map(|f| f.get("value").and_then(|v| v.as_str())),
other => other.get("value").and_then(|v| v.as_str()),
};
let blob = value
.context("1Password item has no `credential` field")?
.trim()
.to_string();
if blob.is_empty() {
bail!("1Password item `credential` field is empty");
}
Ok(blob)
}