pijul_identity/
repair.rs

1use super::Complete;
2use pijul_config as config;
3
4use libpijul::key::{PublicKey, SecretKey};
5
6use std::fs;
7use std::io::{Read, Write};
8use std::path::PathBuf;
9
10use anyhow::{bail, Context};
11use log::debug;
12use thiserror::Error;
13
14const FIRST_IDENTITY_MESSAGE: &str = "It doesn't look like you have any identities configured!
15Each author in Pijul is identified by a unique key to provide greater security & flexibility over names/emails.
16To make sure humans (including you!) can easily identify these keys, we need a few personal details.
17For more information see https://pijul.org/manual/keys.html";
18
19const MIGRATE_IDENTITY_MESSAGE: &str =
20    "It seems you have configured an identity in an older version of Pijul, which uses an older identity format!
21Please take a moment to confirm your details are correct.";
22
23const MISMATCHED_KEYS_MESSAGE: &str = "It seems the keys on your system are mismatched!
24This is most likely the result of data corruption, please check your drive and try again.";
25
26#[derive(Error, Debug)]
27pub enum IdentityParseError {
28    #[error("Mismatching keys")]
29    MismatchingKeys,
30    #[error("Could not find secret key at path {0}")]
31    NoSecretKey(PathBuf),
32    #[error(transparent)]
33    Other(#[from] anyhow::Error),
34}
35
36/// Ensure that the user has at least one valid identity on disk.
37///
38/// This function performs the following:
39/// * Migrate users from the old identity format
40/// * Validate all identity key pairs
41/// * Create a new identity if none exist
42pub async fn fix_identities() -> Result<(), anyhow::Error> {
43    let mut dir = config::global_config_dir().unwrap();
44    dir.push("identities");
45    std::fs::create_dir_all(&dir)?;
46    dir.pop();
47
48    let identities = Complete::load_all()?;
49
50    if identities.is_empty() {
51        // This could be because the old format exists on disk, but if the
52        // extraction fails then we can be fairly sure the user simply isn't set up
53        let extraction_result = Complete::from_old_format();
54
55        let mut stderr = std::io::stderr();
56
57        match extraction_result {
58            Ok(old_identity) => {
59                // Migrate to new format
60                writeln!(stderr, "{MIGRATE_IDENTITY_MESSAGE}")?;
61
62                // Confirm details then write to disk
63                old_identity.clone().create(true).await?;
64
65                // The identity is stored as the public key's signature on disk
66                let identity_path = format!("identities/{}", &old_identity.public_key.key);
67
68                // Try to delete what remains of the old identities
69                let paths_to_delete =
70                    vec!["publickey.json", "secretkey.json", identity_path.as_str()];
71                for path in paths_to_delete {
72                    let file_path = dir.join(path);
73                    if file_path.exists() {
74                        debug!("Deleting old file: {file_path:?}");
75                        fs::remove_file(file_path)?;
76                    } else {
77                        debug!("Could not delete old file (path not found): {file_path:?}");
78                    }
79                }
80            }
81            Err(e) => {
82                match e {
83                    IdentityParseError::MismatchingKeys => {
84                        bail!("User must repair broken keys before continuing");
85                    }
86                    IdentityParseError::NoSecretKey(_) => {
87                        // This is the user's first time setting up an identity
88                        writeln!(stderr, "{FIRST_IDENTITY_MESSAGE}")?;
89                        Complete::default()?.create(true).await?;
90                    }
91                    IdentityParseError::Other(err) => {
92                        bail!(err);
93                    }
94                }
95            }
96        }
97    }
98
99    // Sanity check to make sure everything is in order
100    for identity in Complete::load_all()? {
101        identity.valid_keys()?;
102    }
103
104    Ok(())
105}
106
107impl Complete {
108    /// Checks if the key pair on disk is valid
109    fn valid_keys(&self) -> Result<bool, anyhow::Error> {
110        let public_key = &self.public_key;
111        let decryped_public_key = self.decrypt()?.0.public_key();
112
113        if public_key.key != decryped_public_key.key {
114            let mut stderr = std::io::stderr();
115            writeln!(stderr, "{MISMATCHED_KEYS_MESSAGE}")?;
116            writeln!(stderr, "Got the following public key signatures:")?;
117            writeln!(stderr, "Plaintext public key: {public_key:#?}")?;
118            writeln!(stderr, "Decrypted public key: {decryped_public_key:#?}")?;
119
120            return Ok(false);
121        }
122
123        Ok(true)
124    }
125
126    /// Migrate user from old to new identity format.
127    ///
128    /// # Arguments
129    /// * `password` - The password used to decrypt the secret key
130    ///
131    /// # Data format
132    /// Data stored in the old format should look as follows:
133    /// ```md
134    ///    .config/pijul/ (or applicable global config directory)
135    ///    ├── config.toml
136    ///    │   ├── Username
137    ///    │   ├── Full name
138    ///    │   └── Email
139    ///    ├── secretkey.json
140    ///    │   ├── Version
141    ///    │   ├── Algorithm
142    ///    │   └── Key
143    ///    ├── publickey.json
144    ///    │   ├── Version
145    ///    │   ├── Algorithm
146    ///    │   ├── Signature
147    ///    │   └── Key
148    ///    └── identities/
149    ///        └── <PUBLIC KEY> (JSON, no extension)
150    ///            ├── Public key
151    ///            │   ├── Version
152    ///            │   ├── Algorithm
153    ///            │   ├── Signature
154    ///            │   └── Key
155    ///            ├── Login
156    ///            └── Last modified
157    ///```
158    ///
159    /// As you can see, there is a lot of redundant data. We can leverage this
160    /// information to repair partially corrupted state. For example, we can
161    /// reconstruct `publickey.json` using the identity file. We are also able
162    /// to reconstruct the public key from the private key, so the steps should
163    /// look roughly as follows:
164    /// 1. Extract secret key
165    /// 2. Extract public key from (in order):
166    ///     1. publickey.json
167    ///     2. File in identities/
168    ///     3. secretkey.json
169    /// 3. Extract login info from (in order):
170    ///     1. File in identities/
171    ///     2. config.toml
172    /// 4. Validate extracted data (query user to fill in blanks)
173    fn from_old_format() -> Result<Self, IdentityParseError> {
174        let config_dir = config::global_config_dir().unwrap();
175
176        let config_path = config_dir.join("config.toml");
177        let identities_path = config_dir.join("identities");
178        let public_key_path = config_dir.join("publickey.json");
179        let secret_key_path = config_dir.join("secretkey.json");
180
181        // If we don't have the private key, there is no chance of repairing
182        // the data. This will also trigger if the data is not in the old format
183        if !secret_key_path.exists() {
184            return Err(IdentityParseError::NoSecretKey(secret_key_path));
185        }
186        // From this point, we can be in 2 states:
187        // - Old identity format
188        // - Broken/missing data
189
190        // Extract data from secretkey.json
191        let mut secret_key_file =
192            fs::File::open(&secret_key_path).context("Failed to open secret key file")?;
193        let mut secret_key_text = String::new();
194        secret_key_file
195            .read_to_string(&mut secret_key_text)
196            .context("Failed to read secret key file")?;
197        let secret_key: SecretKey =
198            serde_json::from_str(&secret_key_text).context("Failed to parse secret key file")?;
199
200        // Extract data from publickey.json
201        // TODO: handle public key not existing
202        let public_key: PublicKey = if public_key_path.exists() {
203            let mut public_key_file =
204                fs::File::open(&public_key_path).context("Failed to open public key file")?;
205            let mut public_key_text = String::new();
206            public_key_file
207                .read_to_string(&mut public_key_text)
208                .context("Failed to read public key file")?;
209
210            serde_json::from_str(&public_key_text).context("Failed to parse public key file")?
211        } else {
212            return Err(IdentityParseError::Other(anyhow::anyhow!(
213                "Public key does not exist!"
214            )));
215        };
216
217        // Extract valid identities
218        let identity: Option<Complete> = if identities_path.exists() {
219            if identities_path.is_dir() {
220                let identities_iter =
221                    fs::read_dir(identities_path).context("Failed to read identities directory")?;
222                let mut identities: Vec<Complete> = vec![];
223
224                // We only need to keep the valid files
225                for dir_entry in identities_iter {
226                    let path = dir_entry.unwrap().path();
227
228                    if path.is_file() {
229                        // Try and deserialize the data. If it fails, there is
230                        // a fairly high chance it's not what we need
231                        let mut identity_file =
232                            fs::File::open(&path).context("Failed to open identity file")?;
233                        let mut identity_text = String::new();
234                        identity_file
235                            .read_to_string(&mut identity_text)
236                            .context("Failed to read identity file")?;
237                        let deserialization_result: Result<Complete, _> =
238                            serde_json::from_str(&identity_text);
239
240                        if deserialization_result.is_ok() {
241                            identities.push(
242                                deserialization_result
243                                    .context("Failed to deserialize identity file")?,
244                            );
245                        }
246                    }
247                }
248
249                if identities.len() == 1 {
250                    Some(identities[0].clone())
251                } else {
252                    None
253                }
254            } else {
255                None
256            }
257        } else {
258            None
259        };
260
261        let config: super::Config = if config_path.exists() {
262            let mut config_file =
263                fs::File::open(&config_path).context("Failed to open config file")?;
264            let mut config_text = String::new();
265            config_file
266                .read_to_string(&mut config_text)
267                .context("Failed to read config file")?;
268
269            let config_data: config::Global =
270                toml::from_str(&config_text).context("Failed to parse config file")?;
271
272            super::Config {
273                key_path: config_data.author.key_path.clone(),
274                author: config_data.author,
275            }
276        } else {
277            let mut author = config::Author::default();
278            author.username = identity
279                .as_ref()
280                .map_or_else(String::new, |x| x.config.author.username.clone());
281
282            super::Config {
283                key_path: None,
284                author,
285            }
286        };
287
288        let identity = Self::new(
289            String::from("default"),
290            config,
291            public_key,
292            Some(super::Credentials::from(secret_key)),
293        );
294
295        if identity.valid_keys()? {
296            Ok(identity)
297        } else {
298            Err(IdentityParseError::MismatchingKeys)
299        }
300    }
301}