inflorescence_pijul_identity/
lib.rs1#![deny(clippy::all)]
33#![warn(clippy::pedantic)]
34#![warn(clippy::nursery)]
35#![warn(clippy::cargo)]
36
37mod create;
38mod load;
39mod repair;
40
41pub use load::{choose_identity_name, public_key};
42use log::warn;
43pub use repair::fix_identities;
44
45use pijul_config::author::Author;
46
47use std::fmt::Display;
48use std::io::Write;
49use std::path::PathBuf;
50use std::sync::OnceLock;
51
52use jiff::Timestamp;
53use libpijul::key::{PublicKey, SKey, SecretKey};
54use pijul_interaction::Password;
55use serde_derive::{Deserialize, Serialize};
56
57#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
58pub struct IdentityConfig {
59 #[serde(flatten)]
60 pub author: Author,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub key_path: Option<PathBuf>,
63}
64
65impl From<Author> for IdentityConfig {
66 fn from(author: Author) -> Self {
67 Self {
68 key_path: None,
69 author,
70 }
71 }
72}
73
74#[derive(Clone, Debug)]
75pub struct Credentials {
76 secret_key: SecretKey,
77 password: OnceLock<String>,
78}
79
80impl Credentials {
81 pub fn new(secret_key: SecretKey, password: Option<String>) -> Self {
82 Self {
83 secret_key,
84 password: if let Some(pw) = password {
85 OnceLock::from(pw)
86 } else {
87 OnceLock::new()
88 },
89 }
90 }
91}
92
93impl From<SecretKey> for Credentials {
94 fn from(secret_key: SecretKey) -> Self {
95 Self {
96 secret_key,
97 password: OnceLock::new(),
98 }
99 }
100}
101
102impl Credentials {
103 pub fn decrypt(
104 &mut self,
105 config: &pijul_config::Config,
106 name: &str,
107 use_keyring: bool,
108 ) -> Result<(SKey, Option<String>), anyhow::Error> {
109 if self.secret_key.encryption.is_none() {
110 self.password.take();
113 Ok((self.secret_key.load(None)?, None))
114 } else if let Ok(key) = self
115 .secret_key
116 .load(self.password.get().map(String::as_str))
117 {
118 Ok((key, self.password.get().map(|x| x.to_owned())))
120 } else {
121 let mut stderr = std::io::stderr();
123 let mut password_attempt = String::new();
124
125 if use_keyring {
127 if let Ok(password) =
128 keyring::Entry::new("pijul", name).and_then(|x| x.get_password())
129 {
130 password_attempt = password;
131 }
132 }
133
134 while self.secret_key.load(Some(&password_attempt)).is_err() {
136 writeln!(stderr, "Password does not match secret key")?;
137
138 password_attempt = Password::new(config)?
139 .with_prompt("Password for secret key")
140 .with_allow_empty(true)
141 .interact()?;
142 }
143
144 if let Err(e) =
146 keyring::Entry::new("pijul", name).and_then(|x| x.set_password(&password_attempt))
147 {
148 warn!("Unable to set password: {e:?}");
149 }
150 self.password.set(password_attempt.clone()).unwrap();
151
152 Ok((
153 self.secret_key.load(Some(&password_attempt))?,
154 Some(password_attempt),
155 ))
156 }
157 }
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct Complete {
163 #[serde(skip)]
164 pub name: String,
165 #[serde(flatten)]
166 pub config: IdentityConfig,
167 pub last_modified: Timestamp,
168 pub public_key: PublicKey,
169 #[serde(skip)]
170 pub credentials: Option<Credentials>,
171}
172
173impl Complete {
174 pub fn new(
182 name: String,
183 config: IdentityConfig,
184 public_key: PublicKey,
185 credentials: Option<Credentials>,
186 ) -> Self {
187 if name.is_empty() {
188 panic!("Identity name cannot be empty!");
189 }
190
191 Self {
192 name,
193 config,
194 public_key,
195 credentials,
196 last_modified: Timestamp::now(),
197 }
198 }
199
200 pub fn default(config: &pijul_config::Config) -> Result<Self, anyhow::Error> {
202 let secret_key = SKey::generate(None);
215 let public_key = secret_key.public_key();
216
217 Ok(Self::new(
218 String::from("default"),
219 IdentityConfig::from(config.author.to_owned()),
220 public_key,
221 Some(Credentials::from(secret_key.save(None))),
222 ))
223 }
224
225 pub fn secret_key(&self) -> Option<SecretKey> {
227 if let Some(credentials) = &self.credentials {
228 Some(credentials.secret_key.clone())
229 } else {
230 None
231 }
232 }
233
234 pub fn as_portable(&self) -> Self {
237 Self {
238 name: String::new(),
239 last_modified: Timestamp::now(),
240 config: IdentityConfig {
241 key_path: None,
242 author: self.config.author.clone(),
243 },
244 public_key: self.public_key.clone(),
245 credentials: None,
246 }
247 }
248
249 pub fn decrypt(
252 &self,
253 config: &pijul_config::Config,
254
255 use_keyring: bool,
256 ) -> Result<(SKey, Option<String>), anyhow::Error> {
257 self.credentials
258 .clone()
259 .unwrap()
260 .decrypt(config, &self.name, use_keyring)
261 }
262
263 fn change_password(
264 &mut self,
265 config: &pijul_config::Config,
266 use_keyring: bool,
267 ) -> Result<(), anyhow::Error> {
268 let (decryped_key, _) = self.decrypt(config, use_keyring)?;
269
270 let user_password = Password::new(config)?
271 .with_prompt("New password")
272 .with_allow_empty(true)
273 .with_confirmation("Confirm password", "Password mismatch")
274 .interact()?;
275
276 let password = if user_password.is_empty() {
277 OnceLock::new()
278 } else {
279 if let Err(e) = keyring::Entry::new("pijul", &self.name)
281 .and_then(|x| x.set_password(&user_password))
282 {
283 warn!("Unable to set password: {e:?}");
284 }
285
286 OnceLock::from(user_password)
287 };
288
289 self.public_key = decryped_key.public_key();
291 self.credentials = Some(Credentials {
292 secret_key: decryped_key.save(password.get().map(String::as_str)),
293 password,
294 });
295
296 Ok(())
297 }
298}
299
300impl Display for Complete {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 let has_username = !self.config.author.username.is_empty();
305 let has_remote = !self.config.author.origin.is_empty();
306
307 let remote_details: Option<String> = if has_username && has_remote {
308 Some(format!(
309 " [{}@{}]",
310 self.config.author.username, self.config.author.origin
311 ))
312 } else if has_username {
313 Some(format!(" [@{}]", self.config.author.username))
314 } else if has_remote {
315 Some(format!(" [:{}]", self.config.author.origin))
316 } else {
317 None
318 };
319
320 write!(f, "{}{}", self.name, remote_details.unwrap_or_default())
321 }
322}