git_remote_htree/nostr_client/
identity.rs1use anyhow::{Context, Result};
2use nostr_sdk::prelude::{Keys, PublicKey, SecretKey, ToBech32};
3use tracing::{debug, info};
4
5#[derive(Debug, Clone)]
7pub struct StoredKey {
8 pub secret_hex: Option<String>,
10 pub pubkey_hex: String,
12 pub petname: Option<String>,
14}
15
16#[derive(Debug, Clone, Default)]
18pub struct StoredKeyLists {
19 pub keys_file_entries: Vec<StoredKey>,
21 pub alias_file_entries: Vec<StoredKey>,
23}
24
25impl StoredKey {
26 pub fn from_secret_hex(secret_hex: &str, petname: Option<String>) -> Result<Self> {
28 use secp256k1::{Secp256k1, SecretKey as SecpSecretKey};
29
30 let sk_bytes = hex::decode(secret_hex).context("Invalid hex in secret key")?;
31 let sk = SecpSecretKey::from_slice(&sk_bytes).context("Invalid secret key")?;
32 let secp = Secp256k1::new();
33 let pk = sk.x_only_public_key(&secp).0;
34 let pubkey_hex = hex::encode(pk.serialize());
35
36 Ok(Self {
37 secret_hex: Some(secret_hex.to_string()),
38 pubkey_hex,
39 petname,
40 })
41 }
42
43 pub fn from_nsec(nsec: &str, petname: Option<String>) -> Result<Self> {
45 let secret_key =
46 SecretKey::parse(nsec).map_err(|e| anyhow::anyhow!("Invalid nsec format: {}", e))?;
47 let secret_hex = hex::encode(secret_key.to_secret_bytes());
48 Self::from_secret_hex(&secret_hex, petname)
49 }
50
51 pub fn from_pubkey_hex(pubkey_hex: &str, petname: Option<String>) -> Result<Self> {
53 let pubkey = PublicKey::from_hex(pubkey_hex)
54 .map_err(|e| anyhow::anyhow!("Invalid pubkey hex: {}", e))?;
55
56 Ok(Self {
57 secret_hex: None,
58 pubkey_hex: hex::encode(pubkey.to_bytes()),
59 petname,
60 })
61 }
62
63 pub fn from_npub(npub: &str, petname: Option<String>) -> Result<Self> {
65 let pubkey =
66 PublicKey::parse(npub).map_err(|e| anyhow::anyhow!("Invalid npub format: {}", e))?;
67
68 Ok(Self {
69 secret_hex: None,
70 pubkey_hex: hex::encode(pubkey.to_bytes()),
71 petname,
72 })
73 }
74}
75
76#[derive(Clone, Copy)]
77enum IdentityFileKind {
78 Keys,
79 Aliases,
80}
81
82fn ensure_aliases_file_hint() {
83 let aliases_path = hashtree_config::get_aliases_path();
84 if aliases_path.exists() {
85 return;
86 }
87
88 let Some(parent) = aliases_path.parent() else {
89 return;
90 };
91
92 if !parent.exists() {
93 return;
94 }
95
96 let template = format!(
97 "# Public read-only aliases for repos you clone or fetch.\n# Format: npub1... alias\n{} {}\n",
98 hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB,
99 hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_ALIAS
100 );
101
102 let _ = std::fs::OpenOptions::new()
103 .write(true)
104 .create_new(true)
105 .open(&aliases_path)
106 .and_then(|mut file| std::io::Write::write_all(&mut file, template.as_bytes()));
107}
108
109fn parse_identity_entry(
110 raw: &str,
111 petname: Option<String>,
112 kind: IdentityFileKind,
113) -> Option<StoredKey> {
114 let key = match kind {
115 IdentityFileKind::Keys => {
116 if raw.starts_with("nsec1") {
117 StoredKey::from_nsec(raw, petname)
118 } else if raw.starts_with("npub1") {
119 StoredKey::from_npub(raw, petname)
120 } else if raw.len() == 64 {
121 StoredKey::from_secret_hex(raw, petname)
122 } else {
123 return None;
124 }
125 }
126 IdentityFileKind::Aliases => {
127 if raw.starts_with("npub1") {
128 StoredKey::from_npub(raw, petname)
129 } else if raw.len() == 64 {
130 StoredKey::from_pubkey_hex(raw, petname)
131 } else {
132 return None;
133 }
134 }
135 };
136
137 key.ok()
138}
139
140fn load_identities_from_path(path: &std::path::Path, kind: IdentityFileKind) -> Vec<StoredKey> {
141 let mut keys = Vec::new();
142
143 if let Ok(content) = std::fs::read_to_string(path) {
144 for entry in hashtree_config::parse_keys_file(&content) {
145 if let Some(key) = parse_identity_entry(&entry.secret, entry.alias, kind) {
146 debug!(
147 "Loaded identity: pubkey={}, petname={:?}, has_secret={}",
148 key.pubkey_hex,
149 key.petname,
150 key.secret_hex.is_some()
151 );
152 keys.push(key);
153 }
154 }
155 }
156
157 keys
158}
159
160pub fn resolve_self_identity(keys: &[StoredKey]) -> Option<(String, Option<String>)> {
161 keys.iter()
162 .find(|k| k.petname.as_deref() == Some("self") && k.secret_hex.is_some())
163 .or_else(|| {
164 keys.iter()
165 .find(|k| k.petname.as_deref() == Some("default") && k.secret_hex.is_some())
166 })
167 .or_else(|| keys.iter().find(|k| k.secret_hex.is_some()))
168 .map(|key| (key.pubkey_hex.clone(), key.secret_hex.clone()))
169}
170
171pub fn load_key_lists() -> StoredKeyLists {
173 ensure_aliases_file_hint();
174
175 StoredKeyLists {
176 keys_file_entries: load_identities_from_path(
177 &hashtree_config::get_keys_path(),
178 IdentityFileKind::Keys,
179 ),
180 alias_file_entries: load_identities_from_path(
181 &hashtree_config::get_aliases_path(),
182 IdentityFileKind::Aliases,
183 ),
184 }
185}
186
187pub fn load_keys() -> Vec<StoredKey> {
189 let lists = load_key_lists();
190 let mut keys = lists.keys_file_entries;
191 keys.extend(lists.alias_file_entries);
192 keys
193}
194
195pub fn resolve_identity(identifier: &str) -> Result<(String, Option<String>)> {
202 let keys = load_keys();
203
204 if identifier == "self" {
205 if let Some(resolved) = resolve_self_identity(&keys) {
206 return Ok(resolved);
207 }
208 let new_key = generate_and_save_key("self")?;
209 info!("Generated new identity: npub1{}", &new_key.pubkey_hex[..12]);
210 return Ok((new_key.pubkey_hex, new_key.secret_hex));
211 }
212
213 for key in &keys {
214 if key.petname.as_deref() == Some(identifier) {
215 return Ok((key.pubkey_hex.clone(), key.secret_hex.clone()));
216 }
217 }
218
219 if identifier.starts_with("npub1") {
220 let pk = PublicKey::parse(identifier)
221 .map_err(|e| anyhow::anyhow!("Invalid npub format: {}", e))?;
222 let pubkey_hex = hex::encode(pk.to_bytes());
223
224 let secret = keys
225 .iter()
226 .find(|k| k.pubkey_hex == pubkey_hex)
227 .and_then(|k| k.secret_hex.clone());
228
229 return Ok((pubkey_hex, secret));
230 }
231
232 if identifier.len() == 64 && hex::decode(identifier).is_ok() {
233 let secret = keys
234 .iter()
235 .find(|k| k.pubkey_hex == identifier)
236 .and_then(|k| k.secret_hex.clone());
237
238 return Ok((identifier.to_string(), secret));
239 }
240
241 anyhow::bail!(
242 "Unknown identity '{}'. Add it to ~/.hashtree/aliases (preferred) or ~/.hashtree/keys, or use a pubkey/npub.",
243 identifier
244 )
245}
246
247fn generate_and_save_key(petname: &str) -> Result<StoredKey> {
249 use std::fs::{self, OpenOptions};
250 use std::io::Write;
251
252 let keys = Keys::generate();
253 let secret_hex = hex::encode(keys.secret_key().to_secret_bytes());
254 let pubkey_hex = hex::encode(keys.public_key().to_bytes());
255
256 let keys_path = hashtree_config::get_keys_path();
257 if let Some(parent) = keys_path.parent() {
258 fs::create_dir_all(parent)?;
259 }
260 ensure_aliases_file_hint();
261
262 let mut file = OpenOptions::new()
263 .create(true)
264 .append(true)
265 .open(&keys_path)?;
266
267 let nsec = keys
268 .secret_key()
269 .to_bech32()
270 .map_err(|e| anyhow::anyhow!("Failed to encode nsec: {}", e))?;
271 writeln!(file, "{} {}", nsec, petname)?;
272
273 info!(
274 "Saved new key to {:?} with petname '{}'",
275 keys_path, petname
276 );
277
278 Ok(StoredKey {
279 secret_hex: Some(secret_hex),
280 pubkey_hex,
281 petname: Some(petname.to_string()),
282 })
283}