use anyhow::{anyhow, Context, Result};
use bip39::{Language, Mnemonic};
use secp256k1::{ecdsa, Message, PublicKey, Secp256k1, SecretKey};
use sha2::{Digest, Sha256};
pub struct AgentIdentity {
pub mnemonic: String,
pub secret_key_hex: String,
pub public_key_hex: String,
}
impl AgentIdentity {
pub fn generate() -> Result<Self> {
for _ in 0..10 {
let mnemonic = Mnemonic::generate_in(Language::English, 12)
.context("bip39 mnemonic generation")?;
if let Some(identity) = Self::from_mnemonic_inner(&mnemonic)? {
return Ok(identity);
}
}
Err(anyhow!(
"failed to derive a valid secp256k1 keypair from 10 generated mnemonics"
))
}
pub fn from_phrase(phrase: &str) -> Result<Self> {
let mnemonic = Mnemonic::parse_in(Language::English, phrase)
.with_context(|| "parse stored BIP39 mnemonic")?;
Self::from_mnemonic_inner(&mnemonic)?.ok_or_else(|| {
anyhow!(
"stored mnemonic derives an invalid secp256k1 scalar — \
the saved session file is corrupt; delete it to re-onboard"
)
})
}
fn from_mnemonic_inner(mnemonic: &Mnemonic) -> Result<Option<Self>> {
let seed = mnemonic.to_seed_normalized("");
let secret_bytes: [u8; 32] = seed[..32]
.try_into()
.expect("bip39 seed is always 64 bytes");
let secret_key = match SecretKey::from_slice(&secret_bytes) {
Ok(sk) => sk,
Err(_) => return Ok(None),
};
let secp = Secp256k1::signing_only();
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
Ok(Some(Self {
mnemonic: mnemonic.to_string(),
secret_key_hex: hex::encode(secret_bytes),
public_key_hex: hex::encode(public_key.serialize()),
}))
}
pub fn sign_challenge(&self, challenge: &str) -> Result<String> {
let secret_bytes = hex::decode(&self.secret_key_hex)
.context("decode stored secret_key_hex")?;
let secret_key =
SecretKey::from_slice(&secret_bytes).context("rehydrate secp256k1 SecretKey")?;
let mut hasher = Sha256::new();
hasher.update(challenge.as_bytes());
let digest = hasher.finalize();
let message = Message::from_digest_slice(&digest)
.context("wrap SHA-256 digest as secp256k1 Message")?;
let secp = Secp256k1::signing_only();
let signature: ecdsa::Signature = secp.sign_ecdsa(&message, &secret_key);
Ok(hex::encode(signature.serialize_der().as_ref()))
}
}