use anyhow::Result;
use gpgme::{Context, EncryptFlags, Key};
use thiserror::Error;
use zeroize::Zeroize;
use crate::{Ciphertext, Plaintext};
const ENCRYPT_FLAGS: EncryptFlags = EncryptFlags::ALWAYS_TRUST;
pub fn encrypt(
context: &mut Context,
recipients: &[&str],
plaintext: Plaintext,
) -> Result<Ciphertext> {
assert!(
!recipients.is_empty(),
"attempting to encrypt secret for empty list of recipients"
);
let mut ciphertext = vec![];
let keys = fingerprints_to_keys(context, recipients)?;
context
.encrypt_with_flags(
keys.iter(),
plaintext.unsecure_ref(),
&mut ciphertext,
ENCRYPT_FLAGS,
)
.map_err(Err::Encrypt)?;
Ok(Ciphertext::from(ciphertext))
}
pub fn decrypt(context: &mut Context, ciphertext: Ciphertext) -> Result<Plaintext> {
let mut plaintext = vec![];
context
.decrypt(ciphertext.unsecure_ref(), &mut plaintext)
.map_err(Err::Decrypt)?;
Ok(Plaintext::from(plaintext))
}
pub fn can_decrypt(context: &mut Context, ciphertext: Ciphertext) -> Result<bool> {
let mut plaintext = vec![];
let result = context.decrypt(ciphertext.unsecure_ref(), &mut plaintext);
plaintext.zeroize();
match result {
Ok(_) => Ok(true),
Err(err) if gpgme::error::Error::NO_SECKEY.code() == err.code() => Ok(false),
Err(_) => Ok(true),
}
}
pub fn public_keys(context: &mut Context) -> Result<Vec<KeyId>> {
Ok(context
.keys()?
.into_iter()
.filter_map(|k| k.ok())
.filter(|k| k.can_encrypt())
.map(|k| k.into())
.collect())
}
pub fn private_keys(context: &mut Context) -> Result<Vec<KeyId>> {
Ok(context
.secret_keys()?
.into_iter()
.filter_map(|k| k.ok())
.filter(|k| k.can_encrypt())
.map(|k| k.into())
.collect())
}
pub fn import_key(context: &mut Context, key: &[u8]) -> Result<()> {
let key_str = std::str::from_utf8(&key).expect("exported key is invalid UTF-8");
assert!(
!key_str.contains("PRIVATE KEY"),
"imported key contains PRIVATE KEY, blocked to prevent accidentally leaked secret key"
);
assert!(
key_str.contains("PUBLIC KEY"),
"imported key must contain PUBLIC KEY, blocked to prevent accidentally leaked secret key"
);
context
.import(key)
.map(|_| ())
.map_err(|err| Err::Import(err.into()).into())
}
pub fn export_key(context: &mut Context, fingerprint: &str) -> Result<Vec<u8>> {
let key = context
.get_key(fingerprint)
.map_err(|err| Err::Export(Err::UnknownFingerprint(err).into()))?;
let mut data: Vec<u8> = vec![];
let armor = context.armor();
context.set_armor(true);
context.export_keys(&[key], gpgme::ExportMode::empty(), &mut data)?;
context.set_armor(armor);
let data_str = std::str::from_utf8(&data).expect("exported key is invalid UTF-8");
assert!(
!data_str.contains("PRIVATE KEY"),
"exported key contains PRIVATE KEY, blocked to prevent accidentally leaking secret key"
);
assert!(
data_str.contains("PUBLIC KEY"),
"exported key must contain PUBLIC KEY, blocked to prevent accidentally leaking secret key"
);
Ok(data)
}
#[derive(Clone)]
pub struct KeyId(pub String, pub Vec<String>);
impl From<Key> for KeyId {
fn from(key: Key) -> Self {
Self(
key.fingerprint()
.expect("GPGME key does not have fingerprint")
.to_string(),
key.user_ids()
.map(|user| {
let mut parts = vec![];
if let Ok(name) = user.name() {
if !name.trim().is_empty() {
parts.push(name.into());
}
}
if let Ok(comment) = user.comment() {
if !comment.trim().is_empty() {
parts.push(format!("({})", comment));
}
}
if let Ok(email) = user.email() {
if !email.trim().is_empty() {
parts.push(format!("<{}>", email));
}
}
parts.join(" ")
})
.collect(),
)
}
}
fn fingerprints_to_keys(context: &mut Context, fingerprints: &[&str]) -> Result<Vec<Key>> {
let mut keys = vec![];
for fp in fingerprints {
keys.push(
context
.get_key(fp.to_owned())
.map_err(Err::UnknownFingerprint)?,
);
}
Ok(keys)
}
#[derive(Debug, Error)]
pub enum Err {
#[error("failed to obtain GPGME cryptography context")]
Context(#[source] gpgme::Error),
#[error("failed to encrypt plaintext")]
Encrypt(#[source] gpgme::Error),
#[error("failed to decrypt ciphertext")]
Decrypt(#[source] gpgme::Error),
#[error("failed to import key")]
Import(#[source] anyhow::Error),
#[error("failed to export key")]
Export(#[source] anyhow::Error),
#[error("fingerprint does not match public key in keychain")]
UnknownFingerprint(#[source] gpgme::Error),
}