use super::Complete;
use crate::config;
use libpijul::key::{PublicKey, SecretKey};
use std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
use anyhow::{bail, Context};
use log::debug;
use thiserror::Error;
const FIRST_IDENTITY_MESSAGE: &str = "It doesn't look like you have any identities configured!
Each author in Pijul is identified by a unique key to provide greater security & flexibility over names/emails.
To make sure humans (including you!) can easily identify these keys, we need a few personal details.
For more information see https://pijul.org/manual/keys.html";
const MIGRATE_IDENTITY_MESSAGE: &str =
"It seems you have configured an identity in an older version of Pijul, which uses an older identity format!
Please take a moment to confirm your details are correct.";
const MISMATCHED_KEYS_MESSAGE: &str = "It seems the keys on your system are mismatched!
This is most likely the result of data corruption, please check your drive and try again.";
#[derive(Error, Debug)]
pub enum IdentityParseError {
#[error("Mismatching keys")]
MismatchingKeys,
#[error("Could not find secret key at path {0}")]
NoSecretKey(PathBuf),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
pub async fn fix_identities(no_prompt: bool) -> Result<(), anyhow::Error> {
let mut dir = config::global_config_dir().unwrap();
dir.push("identities");
std::fs::create_dir_all(&dir)?;
dir.pop();
let identities = Complete::load_all()?;
if identities.is_empty() {
let extraction_result = Complete::from_old_format();
let mut stderr = std::io::stderr();
match extraction_result {
Ok(old_identity) => {
writeln!(stderr, "{MIGRATE_IDENTITY_MESSAGE}")?;
old_identity.clone().create(no_prompt, true).await?;
let identity_path = format!("identities/{}", &old_identity.public_key.key);
let paths_to_delete =
vec!["publickey.json", "secretkey.json", identity_path.as_str()];
for path in paths_to_delete {
let file_path = dir.join(path);
if file_path.exists() {
debug!("Deleting old file: {file_path:?}");
fs::remove_file(file_path)?;
} else {
debug!("Could not delete old file (path not found): {file_path:?}");
}
}
}
Err(e) => {
match e {
IdentityParseError::MismatchingKeys => {
bail!("User must repair broken keys before continuing");
}
IdentityParseError::NoSecretKey(_) => {
if no_prompt {
bail!("No identities configured");
}
writeln!(stderr, "{FIRST_IDENTITY_MESSAGE}")?;
Complete::default()?.create(no_prompt, true).await?;
}
IdentityParseError::Other(err) => {
bail!(err);
}
}
}
}
}
for identity in Complete::load_all()? {
identity.valid_keys()?;
}
Ok(())
}
impl Complete {
fn valid_keys(&self) -> Result<bool, anyhow::Error> {
let public_key = &self.public_key;
let decryped_public_key = self.decrypt()?.0.public_key();
if public_key.key != decryped_public_key.key {
let mut stderr = std::io::stderr();
writeln!(stderr, "{MISMATCHED_KEYS_MESSAGE}")?;
writeln!(stderr, "Got the following public key signatures:")?;
writeln!(stderr, "Plaintext public key: {public_key:#?}")?;
writeln!(stderr, "Decrypted public key: {decryped_public_key:#?}")?;
return Ok(false);
}
Ok(true)
}
fn from_old_format() -> Result<Self, IdentityParseError> {
let config_dir = config::global_config_dir().unwrap();
let config_path = config_dir.join("config.toml");
let identities_path = config_dir.join("identities");
let public_key_path = config_dir.join("publickey.json");
let secret_key_path = config_dir.join("secretkey.json");
if !secret_key_path.exists() {
return Err(IdentityParseError::NoSecretKey(secret_key_path));
}
let mut secret_key_file =
fs::File::open(&secret_key_path).context("Failed to open secret key file")?;
let mut secret_key_text = String::new();
secret_key_file
.read_to_string(&mut secret_key_text)
.context("Failed to read secret key file")?;
let secret_key: SecretKey =
serde_json::from_str(&secret_key_text).context("Failed to parse secret key file")?;
let public_key: PublicKey = if public_key_path.exists() {
let mut public_key_file =
fs::File::open(&public_key_path).context("Failed to open public key file")?;
let mut public_key_text = String::new();
public_key_file
.read_to_string(&mut public_key_text)
.context("Failed to read public key file")?;
serde_json::from_str(&public_key_text).context("Failed to parse public key file")?
} else {
return Err(IdentityParseError::Other(anyhow::anyhow!(
"Public key does not exist!"
)));
};
let identity: Option<Complete> = if identities_path.exists() {
if identities_path.is_dir() {
let identities_iter =
fs::read_dir(identities_path).context("Failed to read identities directory")?;
let mut identities: Vec<Complete> = vec![];
for dir_entry in identities_iter {
let path = dir_entry.unwrap().path();
if path.is_file() {
let mut identity_file =
fs::File::open(&path).context("Failed to open identity file")?;
let mut identity_text = String::new();
identity_file
.read_to_string(&mut identity_text)
.context("Failed to read identity file")?;
let deserialization_result: Result<Complete, _> =
serde_json::from_str(&identity_text);
if deserialization_result.is_ok() {
identities.push(
deserialization_result
.context("Failed to deserialize identity file")?,
);
}
}
}
if identities.len() == 1 {
Some(identities[0].clone())
} else {
None
}
} else {
None
}
} else {
None
};
let config: super::Config = if config_path.exists() {
let mut config_file =
fs::File::open(&config_path).context("Failed to open config file")?;
let mut config_text = String::new();
config_file
.read_to_string(&mut config_text)
.context("Failed to read config file")?;
let config_data: config::Global =
toml::from_str(&config_text).context("Failed to parse config file")?;
super::Config {
key_path: config_data.author.key_path.clone(),
author: config_data.author,
}
} else {
let mut author = config::Author::default();
author.username = identity
.as_ref()
.map_or_else(String::new, |x| x.config.author.username.clone());
super::Config {
key_path: None,
author,
}
};
let identity = Self::new(
String::from("default"),
config,
public_key,
Some(super::Credentials::from(secret_key)),
);
if identity.valid_keys()? {
Ok(identity)
} else {
Err(IdentityParseError::MismatchingKeys)
}
}
}