1use std::fs;
2use std::os::unix::fs::PermissionsExt;
3use std::path::PathBuf;
4
5use nostr_sdk::prelude::*;
6use serde::Serialize;
7
8use crate::config::identity_dir;
9use crate::error::{AmError, AmResult};
10
11#[derive(Debug, Serialize)]
12pub struct IdentityInfo {
13 pub name: String,
14 pub npub: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub nsec: Option<String>,
17 pub encrypted: bool,
18}
19
20fn identity_path(name: &str) -> AmResult<PathBuf> {
21 Ok(identity_dir()?.join(format!("{name}.nsec")))
22}
23
24pub fn generate(name: Option<&str>, passphrase: Option<&str>) -> AmResult<IdentityInfo> {
25 let keys = Keys::generate();
26 let name = name.unwrap_or("default").to_string();
27 store_keys(&name, &keys, passphrase)?;
28 Ok(identity_info(&name, &keys, false, passphrase.is_some()))
29}
30
31pub fn import(nsec: &str, name: Option<&str>, passphrase: Option<&str>) -> AmResult<IdentityInfo> {
32 let secret_key = SecretKey::from_bech32(nsec).map_err(|e| AmError::Crypto(e.to_string()))?;
33 let keys = Keys::new(secret_key);
34 let name = name.unwrap_or("default").to_string();
35 store_keys(&name, &keys, passphrase)?;
36 Ok(identity_info(&name, &keys, false, passphrase.is_some()))
37}
38
39pub fn show(
40 name: Option<&str>,
41 show_secret: bool,
42 passphrase: Option<&str>,
43) -> AmResult<IdentityInfo> {
44 let name = name.unwrap_or("default");
45 let encrypted = is_encrypted(name)?;
46 let keys = load_keys(name, passphrase)?;
47 Ok(identity_info(name, &keys, show_secret, encrypted))
48}
49
50pub fn list() -> AmResult<Vec<IdentityInfo>> {
51 let dir = identity_dir()?;
52 if !dir.exists() {
53 return Ok(vec![]);
54 }
55 let mut identities = Vec::new();
56 for entry in fs::read_dir(dir)? {
57 let entry = entry?;
58 let path = entry.path();
59 if path.extension().and_then(|e| e.to_str()) == Some("nsec") {
60 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
61 let content = fs::read_to_string(&path)?.trim().to_string();
62 let encrypted = content.starts_with("ncryptsec1");
63 if encrypted {
64 identities.push(IdentityInfo {
66 name: stem.to_string(),
67 npub: "(encrypted)".to_string(),
68 nsec: None,
69 encrypted: true,
70 });
71 } else {
72 let keys = load_keys(stem, None)?;
73 identities.push(identity_info(stem, &keys, false, false));
74 }
75 }
76 }
77 }
78 identities.sort_by(|a, b| a.name.cmp(&b.name));
79 Ok(identities)
80}
81
82pub fn encrypt_existing(name: &str, passphrase: &str) -> AmResult<IdentityInfo> {
83 let path = identity_path(name)?;
84 if !path.exists() {
85 return Err(AmError::Config(format!("identity '{name}' not found")));
86 }
87 let content = fs::read_to_string(&path)?.trim().to_string();
88 if content.starts_with("ncryptsec1") {
89 return Err(AmError::Config(format!(
90 "identity '{name}' is already encrypted"
91 )));
92 }
93 let secret_key =
94 SecretKey::from_bech32(&content).map_err(|e| AmError::Crypto(e.to_string()))?;
95 let keys = Keys::new(secret_key.clone());
96 let encrypted = EncryptedSecretKey::new(&secret_key, passphrase, 16, KeySecurity::Medium)
97 .map_err(|e| AmError::Crypto(e.to_string()))?;
98 let ncryptsec = encrypted
99 .to_bech32()
100 .map_err(|e| AmError::Crypto(e.to_string()))?;
101 fs::write(&path, ncryptsec)?;
102 fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
103 Ok(identity_info(name, &keys, false, true))
104}
105
106pub fn decrypt_existing(name: &str, passphrase: &str) -> AmResult<IdentityInfo> {
107 let path = identity_path(name)?;
108 if !path.exists() {
109 return Err(AmError::Config(format!("identity '{name}' not found")));
110 }
111 let content = fs::read_to_string(&path)?.trim().to_string();
112 if !content.starts_with("ncryptsec1") {
113 return Err(AmError::Config(format!(
114 "identity '{name}' is not encrypted"
115 )));
116 }
117 let encrypted =
118 EncryptedSecretKey::from_bech32(&content).map_err(|e| AmError::Crypto(e.to_string()))?;
119 let secret_key = encrypted
120 .decrypt(passphrase)
121 .map_err(|e| AmError::Crypto(e.to_string()))?;
122 let keys = Keys::new(secret_key.clone());
123 let nsec = secret_key
124 .to_bech32()
125 .map_err(|e| AmError::Crypto(e.to_string()))?;
126 fs::write(&path, nsec)?;
127 fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
128 Ok(identity_info(name, &keys, false, false))
129}
130
131pub fn load_keys(name: &str, passphrase: Option<&str>) -> AmResult<Keys> {
132 let path = identity_path(name)?;
133 if !path.exists() {
134 return Err(AmError::Config(format!("identity '{name}' not found")));
135 }
136 let content = fs::read_to_string(&path)?.trim().to_string();
137
138 if content.starts_with("ncryptsec1") {
139 let passphrase = passphrase.ok_or_else(|| {
140 AmError::Crypto(format!(
141 "identity '{name}' is encrypted; provide --passphrase or set AM_PASSPHRASE"
142 ))
143 })?;
144 let encrypted = EncryptedSecretKey::from_bech32(&content)
145 .map_err(|e| AmError::Crypto(e.to_string()))?;
146 let secret_key = encrypted
147 .decrypt(passphrase)
148 .map_err(|e| AmError::Crypto(e.to_string()))?;
149 Ok(Keys::new(secret_key))
150 } else {
151 let secret_key =
152 SecretKey::from_bech32(&content).map_err(|e| AmError::Crypto(e.to_string()))?;
153 Ok(Keys::new(secret_key))
154 }
155}
156
157fn is_encrypted(name: &str) -> AmResult<bool> {
158 let path = identity_path(name)?;
159 if !path.exists() {
160 return Err(AmError::Config(format!("identity '{name}' not found")));
161 }
162 let content = fs::read_to_string(&path)?.trim().to_string();
163 Ok(content.starts_with("ncryptsec1"))
164}
165
166fn store_keys(name: &str, keys: &Keys, passphrase: Option<&str>) -> AmResult<()> {
167 crate::config::ensure_dirs()?;
168 let path = identity_path(name)?;
169 if path.exists() {
170 return Err(AmError::Config(format!("identity '{name}' already exists")));
171 }
172
173 let content = if let Some(pass) = passphrase {
174 let secret_key = keys.secret_key();
175 let encrypted = EncryptedSecretKey::new(secret_key, pass, 16, KeySecurity::Medium)
176 .map_err(|e| AmError::Crypto(e.to_string()))?;
177 encrypted
178 .to_bech32()
179 .map_err(|e| AmError::Crypto(e.to_string()))?
180 } else {
181 keys.secret_key()
182 .to_bech32()
183 .map_err(|e| AmError::Crypto(e.to_string()))?
184 };
185
186 fs::write(&path, content)?;
187 fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
188 Ok(())
189}
190
191fn identity_info(name: &str, keys: &Keys, show_secret: bool, encrypted: bool) -> IdentityInfo {
192 let nsec = if show_secret {
193 keys.secret_key().to_bech32().ok()
194 } else {
195 None
196 };
197 IdentityInfo {
198 name: name.to_string(),
199 npub: keys.public_key().to_bech32().unwrap_or_default(),
200 nsec,
201 encrypted,
202 }
203}