use anyhow::Result;
use serde::Serialize;
use crate::{
args::Args,
parsers::parse_address,
progress::Progress,
storage::{Identity, Storage},
transport::agent_base_url,
};
use relay_lib::{
core::chrono::{DateTime, Utc},
crypto::{PublicKey, SigningKey, StaticSecret, hex},
prelude::{Address, KeyRecord},
};
#[derive(Serialize)]
struct ClaimUserReq {
pub address: String,
#[serde(with = "hex::serde")]
pub ed25519: [u8; 32],
#[serde(with = "hex::serde")]
pub x25519: [u8; 32],
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, clap::Parser)]
#[clap(about = "Claim a new identity on the Agent")]
pub struct ClaimCmd {
#[arg(value_parser = parse_address)]
pub address: Address,
#[clap(short, long, default_value_t = false)]
pub replace: bool,
}
impl ClaimCmd {
pub fn execute(&self, _: Args, storage: &mut Storage) -> Result<()> {
let mut progress = Progress::new(false);
if self.address.inbox().is_some() {
progress.commit();
progress.error("Cannot claim identity with an inbox");
progress.warn("Please provide an address without an inbox to claim an identity, or use `claim-inbox` to claim an inbox.");
progress.abort("Claim command only works for addresses without inboxes");
}
let mut address = self.address.clone();
address.inbox = None;
progress.step(
&format!("Checking existing identity for `{}`", address),
"Checked existing identity",
);
if storage.root.get_identity(&self.address).is_some() {
if !self.replace {
progress.error(&format!(
"Identity for address `{}` already exists locally. Use --replace to overwrite.",
address,
));
progress.warn("THIS WILL DELETE THE EXISTING IDENTITY AND ITS PRIVATE KEYS!");
progress.warn("THIS CAN NOT BE UNDONE!");
return Ok(());
} else {
progress.warn(&format!(
"Replacing existing identity for address `{}`",
address,
));
}
}
progress.step("Generating new identity", "Generated new identity");
let identity = Self::generate_identity(&address);
progress.step("Requsting identity claim", "Identity claimed");
let resp = reqwest::blocking::Client::new()
.post(agent_base_url(address.agent())?.join("/user/claim/user")?)
.json(&ClaimUserReq {
address: address.canonical(),
ed25519: identity.pub_record.ed25519,
x25519: identity.pub_record.x25519,
expires_at: identity.pub_record.expires_at,
})
.send()
.map_err(|_| {
progress.abort("Failed to send identity claim request");
})?;
if resp.status().as_u16() == 409 {
progress.abort("Identity already claimed on the Agent");
}
if !resp.status().is_success() {
let status = resp.status();
progress.abort(&format!("Failed to claim identity. Status: {}", status));
}
progress.step("Storing identity locally", "Identity stored locally");
storage.root.identities.insert(address.clone(), identity);
progress.commit();
progress.arrow(&format!("Successfully claimed identity `{}`", address));
Ok(())
}
fn generate_identity(address: &Address) -> Identity {
let mut rng = relay_lib::crypto::OsRng;
let signing_key = SigningKey::generate(&mut rng);
let static_secret = StaticSecret::random_from_rng(rng);
let public_key = PublicKey::from(&static_secret);
let pub_record = KeyRecord {
id: address.canonical().to_string(),
version: 1,
ed25519: signing_key.verifying_key().to_bytes(),
x25519: public_key.to_bytes(),
created_at: Utc::now(),
expires_at: None,
};
Identity {
pub_record,
inboxes: vec![],
signing_key: signing_key.to_bytes(),
static_secret: signing_key.to_bytes(),
}
}
}