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