use anyhow::{Context, Result};
use nostr_sdk::prelude::{Keys, PublicKey, SecretKey, ToBech32};
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct StoredKey {
pub secret_hex: Option<String>,
pub pubkey_hex: String,
pub petname: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct StoredKeyLists {
pub keys_file_entries: Vec<StoredKey>,
pub alias_file_entries: Vec<StoredKey>,
}
impl StoredKey {
pub fn from_secret_hex(secret_hex: &str, petname: Option<String>) -> Result<Self> {
use secp256k1::{Secp256k1, SecretKey as SecpSecretKey};
let sk_bytes = hex::decode(secret_hex).context("Invalid hex in secret key")?;
let sk = SecpSecretKey::from_slice(&sk_bytes).context("Invalid secret key")?;
let secp = Secp256k1::new();
let pk = sk.x_only_public_key(&secp).0;
let pubkey_hex = hex::encode(pk.serialize());
Ok(Self {
secret_hex: Some(secret_hex.to_string()),
pubkey_hex,
petname,
})
}
pub fn from_nsec(nsec: &str, petname: Option<String>) -> Result<Self> {
let secret_key =
SecretKey::parse(nsec).map_err(|e| anyhow::anyhow!("Invalid nsec format: {}", e))?;
let secret_hex = hex::encode(secret_key.to_secret_bytes());
Self::from_secret_hex(&secret_hex, petname)
}
pub fn from_pubkey_hex(pubkey_hex: &str, petname: Option<String>) -> Result<Self> {
let pubkey = PublicKey::from_hex(pubkey_hex)
.map_err(|e| anyhow::anyhow!("Invalid pubkey hex: {}", e))?;
Ok(Self {
secret_hex: None,
pubkey_hex: hex::encode(pubkey.to_bytes()),
petname,
})
}
pub fn from_npub(npub: &str, petname: Option<String>) -> Result<Self> {
let pubkey =
PublicKey::parse(npub).map_err(|e| anyhow::anyhow!("Invalid npub format: {}", e))?;
Ok(Self {
secret_hex: None,
pubkey_hex: hex::encode(pubkey.to_bytes()),
petname,
})
}
}
#[derive(Clone, Copy)]
enum IdentityFileKind {
Keys,
Aliases,
}
fn ensure_aliases_file_hint() {
let aliases_path = hashtree_config::get_aliases_path();
if aliases_path.exists() {
return;
}
let Some(parent) = aliases_path.parent() else {
return;
};
if !parent.exists() {
return;
}
let template = format!(
"# Public read-only aliases for repos you clone or fetch.\n# Format: npub1... alias\n{} {}\n",
hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB,
hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_ALIAS
);
let _ = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&aliases_path)
.and_then(|mut file| std::io::Write::write_all(&mut file, template.as_bytes()));
}
fn parse_identity_entry(
raw: &str,
petname: Option<String>,
kind: IdentityFileKind,
) -> Option<StoredKey> {
let key = match kind {
IdentityFileKind::Keys => {
if raw.starts_with("nsec1") {
StoredKey::from_nsec(raw, petname)
} else if raw.starts_with("npub1") {
StoredKey::from_npub(raw, petname)
} else if raw.len() == 64 {
StoredKey::from_secret_hex(raw, petname)
} else {
return None;
}
}
IdentityFileKind::Aliases => {
if raw.starts_with("npub1") {
StoredKey::from_npub(raw, petname)
} else if raw.len() == 64 {
StoredKey::from_pubkey_hex(raw, petname)
} else {
return None;
}
}
};
key.ok()
}
fn load_identities_from_path(path: &std::path::Path, kind: IdentityFileKind) -> Vec<StoredKey> {
let mut keys = Vec::new();
if let Ok(content) = std::fs::read_to_string(path) {
for entry in hashtree_config::parse_keys_file(&content) {
if let Some(key) = parse_identity_entry(&entry.secret, entry.alias, kind) {
debug!(
"Loaded identity: pubkey={}, petname={:?}, has_secret={}",
key.pubkey_hex,
key.petname,
key.secret_hex.is_some()
);
keys.push(key);
}
}
}
keys
}
pub fn resolve_self_identity(keys: &[StoredKey]) -> Option<(String, Option<String>)> {
keys.iter()
.find(|k| k.petname.as_deref() == Some("self") && k.secret_hex.is_some())
.or_else(|| {
keys.iter()
.find(|k| k.petname.as_deref() == Some("default") && k.secret_hex.is_some())
})
.or_else(|| keys.iter().find(|k| k.secret_hex.is_some()))
.map(|key| (key.pubkey_hex.clone(), key.secret_hex.clone()))
}
pub fn load_key_lists() -> StoredKeyLists {
ensure_aliases_file_hint();
StoredKeyLists {
keys_file_entries: load_identities_from_path(
&hashtree_config::get_keys_path(),
IdentityFileKind::Keys,
),
alias_file_entries: load_identities_from_path(
&hashtree_config::get_aliases_path(),
IdentityFileKind::Aliases,
),
}
}
pub fn load_keys() -> Vec<StoredKey> {
let lists = load_key_lists();
let mut keys = lists.keys_file_entries;
keys.extend(lists.alias_file_entries);
keys
}
pub fn resolve_identity(identifier: &str) -> Result<(String, Option<String>)> {
let keys = load_keys();
if identifier == "self" {
if let Some(resolved) = resolve_self_identity(&keys) {
return Ok(resolved);
}
let new_key = generate_and_save_key("self")?;
info!("Generated new identity: npub1{}", &new_key.pubkey_hex[..12]);
return Ok((new_key.pubkey_hex, new_key.secret_hex));
}
for key in &keys {
if key.petname.as_deref() == Some(identifier) {
return Ok((key.pubkey_hex.clone(), key.secret_hex.clone()));
}
}
if identifier.starts_with("npub1") {
let pk = PublicKey::parse(identifier)
.map_err(|e| anyhow::anyhow!("Invalid npub format: {}", e))?;
let pubkey_hex = hex::encode(pk.to_bytes());
let secret = keys
.iter()
.find(|k| k.pubkey_hex == pubkey_hex)
.and_then(|k| k.secret_hex.clone());
return Ok((pubkey_hex, secret));
}
if identifier.len() == 64 && hex::decode(identifier).is_ok() {
let secret = keys
.iter()
.find(|k| k.pubkey_hex == identifier)
.and_then(|k| k.secret_hex.clone());
return Ok((identifier.to_string(), secret));
}
anyhow::bail!(
"Unknown identity '{}'. Add it to ~/.hashtree/aliases (preferred) or ~/.hashtree/keys, or use a pubkey/npub.",
identifier
)
}
fn generate_and_save_key(petname: &str) -> Result<StoredKey> {
use std::fs::{self, OpenOptions};
use std::io::Write;
let keys = Keys::generate();
let secret_hex = hex::encode(keys.secret_key().to_secret_bytes());
let pubkey_hex = hex::encode(keys.public_key().to_bytes());
let keys_path = hashtree_config::get_keys_path();
if let Some(parent) = keys_path.parent() {
fs::create_dir_all(parent)?;
}
ensure_aliases_file_hint();
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&keys_path)?;
let nsec = keys
.secret_key()
.to_bech32()
.map_err(|e| anyhow::anyhow!("Failed to encode nsec: {}", e))?;
writeln!(file, "{} {}", nsec, petname)?;
info!(
"Saved new key to {:?} with petname '{}'",
keys_path, petname
);
Ok(StoredKey {
secret_hex: Some(secret_hex),
pubkey_hex,
petname: Some(petname.to_string()),
})
}