use std::{
cmp::PartialEq,
collections::{HashMap, HashSet},
fs,
io::prelude::*,
path::{Path, PathBuf},
};
use hex::FromHex;
use crate::crypto::FindSigningFingerprintStrategy;
pub use crate::error::{Error, Result};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SignatureStatus {
Good,
AlmostGood,
Bad,
}
impl From<gpgme::SignatureSummary> for SignatureStatus {
fn from(s: gpgme::SignatureSummary) -> Self {
if s.contains(gpgme::SignatureSummary::VALID) {
Self::Good
} else if s.contains(gpgme::SignatureSummary::GREEN) {
Self::AlmostGood
} else {
Self::Bad
}
}
}
pub fn parse_signing_keys(
password_store_signing_key: &Option<String>,
crypto: &(dyn crate::crypto::Crypto + Send),
) -> Result<Vec<[u8; 20]>> {
if password_store_signing_key.is_none() {
return Ok(vec![]);
}
let mut signing_keys = vec![];
for key in password_store_signing_key.as_ref().unwrap().split(',') {
let trimmed = key.trim().to_owned();
if trimmed.len() != 40 && (trimmed.len() != 42 && trimmed.starts_with("0x")) {
return Err(Error::Generic(
"signing key isn't in full 40 character id format",
));
}
let key_res = crypto.get_key(&trimmed);
if let Some(err) = key_res.err() {
return Err(Error::GenericDyn(format!(
"signing key not found in keyring, error: {err}",
)));
}
if trimmed.len() == 40 {
signing_keys.push(<[u8; 20]>::from_hex(trimmed)?);
} else {
signing_keys.push(<[u8; 20]>::from_hex(&trimmed[2..])?);
}
}
Ok(signing_keys)
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum OwnerTrustLevel {
Ultimate,
Full,
Marginal,
Never,
Undefined,
Unknown,
}
impl From<&gpgme::Validity> for OwnerTrustLevel {
fn from(level: &gpgme::Validity) -> Self {
match level {
gpgme::Validity::Unknown => Self::Unknown,
gpgme::Validity::Undefined => Self::Undefined,
gpgme::Validity::Never => Self::Never,
gpgme::Validity::Marginal => Self::Marginal,
gpgme::Validity::Full => Self::Full,
gpgme::Validity::Ultimate => Self::Ultimate,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum KeyRingStatus {
InKeyRing,
NotInKeyRing,
}
struct IdComment {
pub id: String,
pub pre_comment: Vec<String>,
pub post_comment: Option<String>,
}
impl std::hash::Hash for IdComment {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl std::cmp::PartialEq for IdComment {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl std::cmp::Eq for IdComment {}
#[derive(Clone, Debug)]
pub struct Comment {
pub pre_comment: Option<String>,
pub post_comment: Option<String>,
}
#[derive(Clone, Debug)]
pub struct Recipient {
pub name: String,
pub comment: Comment,
pub key_id: String,
pub fingerprint: Option<[u8; 20]>,
pub key_ring_status: KeyRingStatus,
pub trust_level: OwnerTrustLevel,
pub not_usable: bool,
}
impl Recipient {
fn new(
name: String,
comment: Comment,
key_id: String,
fingerprint: Option<[u8; 20]>,
key_ring_status: KeyRingStatus,
trust_level: OwnerTrustLevel,
not_usable: bool,
) -> Self {
Self {
name,
comment,
key_id,
fingerprint,
key_ring_status,
trust_level,
not_usable,
}
}
pub fn from(
key_id: &str,
pre_comment: &[String],
post_comment: Option<String>,
crypto: &(dyn crate::crypto::Crypto + Send),
) -> Result<Self> {
let comment_opt = match pre_comment.len() {
0 => None,
_ => Some(pre_comment.join("\n")),
};
let comment = Comment {
pre_comment: comment_opt,
post_comment,
};
let key_result = crypto.get_key(key_id);
if key_result.is_err() {
return Ok(Recipient::new(
"key id not in keyring".to_owned(),
comment,
key_id.to_owned(),
None,
KeyRingStatus::NotInKeyRing,
OwnerTrustLevel::Unknown,
true,
));
}
let real_key = key_result?;
let mut names = real_key.user_id_names();
let name = match names.len() {
0 => "?".to_owned(),
_ => names.pop().unwrap(),
};
let trusts: HashMap<[u8; 20], OwnerTrustLevel> = crypto.get_all_trust_items()?;
let fingerprint = real_key.fingerprint()?;
Ok(Self::new(
name,
comment,
key_id.to_owned(),
Some(fingerprint),
KeyRingStatus::InKeyRing,
(*trusts
.get(&real_key.fingerprint()?)
.unwrap_or(&OwnerTrustLevel::Unknown))
.clone(),
real_key.is_not_usable(),
))
}
pub fn all_recipients(
recipients_file: &Path,
crypto: &(dyn crate::crypto::Crypto + Send),
) -> Result<Vec<Self>> {
let contents = fs::read_to_string(recipients_file)?;
let mut recipients: Vec<Recipient> = Vec::new();
let mut unique_recipients_keys: HashSet<IdComment> = HashSet::new();
let mut comment_buf = vec![];
for key in contents.split('\n') {
if key.len() > 1 {
if key.starts_with('#') {
comment_buf.push(key.chars().skip(1).collect());
} else if key.contains('#') {
let mut splitter = key.splitn(2, '#');
let key = splitter.next().unwrap().trim();
let comment = splitter.next().unwrap();
unique_recipients_keys.insert(IdComment {
id: key.to_owned(),
pre_comment: comment_buf.clone(),
post_comment: Some(comment.to_owned()),
});
comment_buf.clear();
} else {
unique_recipients_keys.insert(IdComment {
id: key.to_owned(),
pre_comment: comment_buf.clone(),
post_comment: None,
});
comment_buf.clear();
}
}
}
for key in unique_recipients_keys {
let recipient =
match Self::from(&key.id, &key.pre_comment, key.post_comment.clone(), crypto) {
Ok(r) => r,
Err(err) => {
let comment_opt = match key.pre_comment.len() {
0 => None,
_ => Some(key.pre_comment.join("\n")),
};
Self::new(
err.to_string(),
Comment {
pre_comment: comment_opt,
post_comment: key.post_comment,
},
key.id.clone(),
None,
KeyRingStatus::NotInKeyRing,
OwnerTrustLevel::Unknown,
true,
)
}
};
recipients.push(recipient)
}
Ok(recipients)
}
pub fn write_recipients_file(
recipients: &[Self],
recipients_file: &Path,
valid_gpg_signing_keys: &[[u8; 20]],
crypto: &(dyn crate::crypto::Crypto + Send),
) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(recipients_file)?;
let mut file_content = String::new();
let mut sorted_recipients = recipients.to_owned();
sorted_recipients.sort_by(|a, b| a.fingerprint.cmp(&b.fingerprint));
for recipient in sorted_recipients {
let to_add = match recipient.fingerprint {
Some(f) => hex::encode_upper(f),
None => recipient.key_id,
};
if recipient.comment.pre_comment.is_some() {
for line in recipient.comment.pre_comment.as_ref().unwrap().split('\n') {
file_content.push('#');
file_content.push_str(line);
file_content.push('\n');
}
}
if !to_add.starts_with("0x") {
file_content.push_str("0x");
}
file_content.push_str(&to_add);
if recipient.comment.post_comment.is_some() {
file_content.push_str(" #");
file_content.push_str(recipient.comment.post_comment.as_ref().unwrap());
}
file_content.push('\n');
}
file.write_all(file_content.as_bytes())?;
if !valid_gpg_signing_keys.is_empty() {
let output = crypto.sign_string(
&file_content,
valid_gpg_signing_keys,
&FindSigningFingerprintStrategy::GPG,
)?;
let recipient_sig_filename: PathBuf = {
let rf = recipients_file.to_path_buf();
let mut sig = rf.into_os_string();
sig.push(".sig");
sig.into()
};
let mut recipient_sig_file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(recipient_sig_filename)?;
recipient_sig_file.write_all(output.as_bytes())?;
}
Ok(())
}
pub fn remove_recipient_from_file(
s: &Self,
recipients_file: &Path,
valid_gpg_signing_keys: &[[u8; 20]],
crypto: &(dyn crate::crypto::Crypto + Send),
) -> Result<()> {
let mut recipients: Vec<Recipient> = Self::all_recipients(recipients_file, crypto)?;
recipients.retain(|vs| {
if vs.fingerprint.is_some() && s.fingerprint.is_some() {
vs.fingerprint != s.fingerprint
} else {
vs.key_id != s.key_id
}
});
if recipients.is_empty() {
return Err(Error::Generic("Can't delete the last encryption key"));
}
Recipient::write_recipients_file(
&recipients,
recipients_file,
valid_gpg_signing_keys,
crypto,
)
}
pub fn add_recipient_to_file(
recipient: &Self,
recipients_file: &Path,
valid_gpg_signing_keys: &[[u8; 20]],
crypto: &(dyn crate::crypto::Crypto + Send),
) -> Result<()> {
let mut recipients: Vec<Self> = Self::all_recipients(recipients_file, crypto)?;
for r in &recipients {
if r == recipient {
return Err(Error::Generic(
"Team member is already in the list of key ids",
));
}
}
recipients.push((*recipient).clone());
Recipient::write_recipients_file(
&recipients,
recipients_file,
valid_gpg_signing_keys,
crypto,
)
}
}
impl PartialEq for Recipient {
fn eq(&self, other: &Self) -> bool {
if self.fingerprint.is_none() || other.fingerprint.is_none() {
return false;
}
return self.fingerprint.as_ref().unwrap() == other.fingerprint.as_ref().unwrap();
}
}
#[cfg(test)]
#[path = "tests/signature.rs"]
mod signature_tests;