1#![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 as config;
46use pijul_config::Author;
47
48use libpijul::key::{PublicKey, SKey, SecretKey};
49
50use std::fmt::Display;
51use std::fs;
52use std::io::{Read, Write};
53use std::path::PathBuf;
54
55use pijul_interaction::Password;
56use serde::{Deserialize, Serialize};
57use std::sync::OnceLock;
58
59#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
60pub struct Config {
61 #[serde(flatten)]
62 pub author: Author,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub key_path: Option<PathBuf>,
65}
66
67impl Default for Config {
68 fn default() -> Self {
69 Self {
70 key_path: None,
71 author: Author::default(),
72 }
73 }
74}
75
76impl From<Author> for Config {
77 fn from(author: Author) -> Self {
78 Self {
79 key_path: None,
80 author,
81 }
82 }
83}
84
85#[derive(Clone, Debug)]
86pub struct Credentials {
87 secret_key: SecretKey,
88 password: OnceLock<String>,
89}
90
91impl Credentials {
92 pub fn new(secret_key: SecretKey, password: Option<String>) -> Self {
93 Self {
94 secret_key,
95 password: if let Some(pw) = password {
96 OnceLock::from(pw)
97 } else {
98 OnceLock::new()
99 },
100 }
101 }
102}
103
104impl From<SecretKey> for Credentials {
105 fn from(secret_key: SecretKey) -> Self {
106 Self {
107 secret_key,
108 password: OnceLock::new(),
109 }
110 }
111}
112
113impl Credentials {
114 pub fn decrypt(&mut self, name: &str) -> Result<(SKey, Option<String>), anyhow::Error> {
115 if self.secret_key.encryption.is_none() {
116 self.password.take();
119 Ok((self.secret_key.load(None)?, None))
120 } else if let Ok(key) = self
121 .secret_key
122 .load(self.password.get().map(String::as_str))
123 {
124 Ok((key, self.password.get().map(|x| x.to_owned())))
126 } else {
127 let mut stderr = std::io::stderr();
129 let mut password_attempt = String::new();
130
131 if let Ok(password) = keyring::Entry::new("pijul", name).and_then(|x| x.get_password())
133 {
134 password_attempt = password;
135 }
136
137 while self.secret_key.load(Some(&password_attempt)).is_err() {
139 writeln!(stderr, "Password does not match secret key")?;
140
141 password_attempt = Password::new()?
142 .with_prompt("Password for secret key")
143 .with_allow_empty(true)
144 .interact()?;
145 }
146
147 if let Err(e) =
149 keyring::Entry::new("pijul", name).and_then(|x| x.set_password(&password_attempt))
150 {
151 warn!("Unable to set password: {e:?}");
152 }
153 self.password.set(password_attempt.clone()).unwrap();
154
155 Ok((
156 self.secret_key.load(Some(&password_attempt))?,
157 Some(password_attempt),
158 ))
159 }
160 }
161}
162
163#[derive(Clone, Debug, Serialize, Deserialize)]
164pub struct Complete {
166 #[serde(skip)]
167 pub name: String,
168 #[serde(flatten)]
169 pub config: Config,
170 pub last_modified: chrono::DateTime<chrono::Utc>,
171 pub public_key: PublicKey,
172 #[serde(skip)]
173 pub credentials: Option<Credentials>,
174}
175
176impl Complete {
177 pub fn new(
185 name: String,
186 config: Config,
187 public_key: PublicKey,
188 credentials: Option<Credentials>,
189 ) -> Self {
190 if name.is_empty() {
191 panic!("Identity name cannot be empty!");
192 }
193
194 Self {
195 name,
196 config,
197 public_key,
198 credentials,
199 last_modified: chrono::offset::Utc::now(),
200 }
201 }
202
203 pub fn default() -> Result<Self, anyhow::Error> {
205 let config_path = config::global_config_dir().unwrap().join("config.toml");
206 let author: Author = if config_path.exists() {
207 let mut config_file = fs::File::open(&config_path)?;
208 let mut config_text = String::new();
209 config_file.read_to_string(&mut config_text)?;
210
211 let global_config: config::Global = toml::from_str(&config_text)?;
212 global_config.author
213 } else {
214 Author::default()
215 };
216
217 let secret_key = SKey::generate(None);
218 let public_key = secret_key.public_key();
219
220 Ok(Self::new(
221 String::from("default"),
222 Config::from(author),
223 public_key,
224 Some(Credentials::from(secret_key.save(None))),
225 ))
226 }
227
228 pub fn secret_key(&self) -> Option<SecretKey> {
230 if let Some(credentials) = &self.credentials {
231 Some(credentials.secret_key.clone())
232 } else {
233 None
234 }
235 }
236
237 pub fn as_portable(&self) -> Self {
240 Self {
241 name: String::new(),
242 last_modified: chrono::offset::Utc::now(),
243 config: Config {
244 key_path: None,
245 author: self.config.author.clone(),
246 },
247 public_key: self.public_key.clone(),
248 credentials: None,
249 }
250 }
251
252 pub fn decrypt(&self) -> Result<(SKey, Option<String>), anyhow::Error> {
255 self.credentials.clone().unwrap().decrypt(&self.name)
256 }
257
258 fn change_password(&mut self) -> Result<(), anyhow::Error> {
259 let (decryped_key, _) = self.decrypt()?;
260
261 let user_password = Password::new()?
262 .with_prompt("New password")
263 .with_allow_empty(true)
264 .with_confirmation("Confirm password", "Password mismatch")
265 .interact()?;
266
267 let password = if user_password.is_empty() {
268 OnceLock::new()
269 } else {
270 if let Err(e) = keyring::Entry::new("pijul", &self.name)
272 .and_then(|x| x.set_password(&user_password))
273 {
274 warn!("Unable to set password: {e:?}");
275 }
276
277 OnceLock::from(user_password)
278 };
279
280 self.public_key = decryped_key.public_key();
282 self.credentials = Some(Credentials {
283 secret_key: decryped_key.save(password.get().map(String::as_str)),
284 password,
285 });
286
287 Ok(())
288 }
289}
290
291impl Display for Complete {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 let has_username = !self.config.author.username.is_empty();
296 let has_remote = !self.config.author.origin.is_empty();
297
298 let remote_details: Option<String> = if has_username && has_remote {
299 Some(format!(
300 " [{}@{}]",
301 self.config.author.username, self.config.author.origin
302 ))
303 } else if has_username {
304 Some(format!(" [@{}]", self.config.author.username))
305 } else if has_remote {
306 Some(format!(" [:{}]", self.config.author.origin))
307 } else {
308 None
309 };
310
311 write!(f, "{}{}", self.name, remote_details.unwrap_or_default())
312 }
313}