prs_lib/crypto/backend/gpgme/
raw.rs

1//! Raw interface to GPGME.
2//!
3//! This provides the most basic and bare functions to interface with the GPGME backend.
4
5use anyhow::Result;
6use gpgme::{Context, EncryptFlags, Key};
7use thiserror::Error;
8use zeroize::Zeroize;
9
10use crate::{Ciphertext, Plaintext};
11
12/// GPGME encryption flags.
13const ENCRYPT_FLAGS: EncryptFlags = EncryptFlags::ALWAYS_TRUST;
14
15/// Encrypt plaintext for the given recipients.
16///
17/// - `context`: GPGME context
18/// - `recipients`: list of recipient fingerprints to encrypt for
19/// - `plaintext`: plaintext to encrypt
20///
21/// # Panics
22///
23/// Panics if list of recipients is empty.
24pub fn encrypt(
25    context: &mut Context,
26    recipients: &[&str],
27    plaintext: Plaintext,
28) -> Result<Ciphertext> {
29    assert!(
30        !recipients.is_empty(),
31        "attempting to encrypt secret for empty list of recipients"
32    );
33
34    let mut ciphertext = vec![];
35    let keys = fingerprints_to_keys(context, recipients)?;
36    context
37        .encrypt_with_flags(
38            keys.iter(),
39            plaintext.unsecure_ref(),
40            &mut ciphertext,
41            ENCRYPT_FLAGS,
42        )
43        .map_err(Err::Encrypt)?;
44    Ok(Ciphertext::from(ciphertext))
45}
46
47/// Decrypt ciphertext.
48///
49/// - `context`: GPGME context
50/// - `ciphertext`: ciphertext to decrypt
51pub fn decrypt(context: &mut Context, ciphertext: Ciphertext) -> Result<Plaintext> {
52    let mut plaintext = vec![];
53    context
54        .decrypt(ciphertext.unsecure_ref(), &mut plaintext)
55        .map_err(Err::Decrypt)?;
56    Ok(Plaintext::from(plaintext))
57}
58
59/// Check whether we can decrypt ciphertext.
60///
61/// This checks whether whether we own the secret key to decrypt the given ciphertext.
62/// Assumes `true` if GPGME returns an error different than `NO_SECKEY`.
63///
64/// - `context`: GPGME context
65/// - `ciphertext`: ciphertext to check
66// To check this, actual decryption is attempted, see this if this can be improved:
67// https://stackoverflow.com/q/64633736/1000145
68pub fn can_decrypt(context: &mut Context, ciphertext: Ciphertext) -> Result<bool> {
69    // Try to decrypt, explicit zeroing of unsecure buffer required
70    let mut plaintext = vec![];
71    let result = context.decrypt(ciphertext.unsecure_ref(), &mut plaintext);
72    plaintext.zeroize();
73
74    match result {
75        Ok(_) => Ok(true),
76        Err(err) if gpgme::error::Error::NO_SECKEY.code() == err.code() => Ok(false),
77        Err(_) => Ok(true),
78    }
79}
80
81/// Get all public keys from keychain.
82///
83/// - `context`: GPGME context
84pub fn public_keys(context: &mut Context) -> Result<Vec<KeyId>> {
85    Ok(context
86        .keys()?
87        .filter_map(|k| k.ok())
88        .filter(|k| k.can_encrypt())
89        .map(|k| k.into())
90        .collect())
91}
92
93/// Get all private/secret keys from keychain.
94///
95/// - `context`: GPGME context
96pub fn private_keys(context: &mut Context) -> Result<Vec<KeyId>> {
97    Ok(context
98        .secret_keys()?
99        .filter_map(|k| k.ok())
100        .filter(|k| k.can_encrypt())
101        .map(|k| k.into())
102        .collect())
103}
104
105/// Import given key from bytes into keychain.
106///
107/// - `context`: GPGME context
108///
109/// # Panics
110///
111/// Panics if the provides key does not look like a public key.
112pub fn import_key(context: &mut Context, key: &[u8]) -> Result<()> {
113    // Assert we're importing a public key
114    let key_str = std::str::from_utf8(key).expect("exported key is invalid UTF-8");
115    assert!(
116        !key_str.contains("PRIVATE KEY"),
117        "imported key contains PRIVATE KEY, blocked to prevent accidentally leaked secret key"
118    );
119    assert!(
120        key_str.contains("PUBLIC KEY"),
121        "imported key must contain PUBLIC KEY, blocked to prevent accidentally leaked secret key"
122    );
123
124    // Import the key
125    context
126        .import(key)
127        .map(|_| ())
128        .map_err(|err| Err::Import(err.into()).into())
129}
130
131/// Export the given key as bytes.
132///
133/// # Panics
134///
135/// Panics if the received key does not look like a public key. This should never happen unless the
136/// gpg binary backend is broken.
137pub fn export_key(context: &mut Context, fingerprint: &str) -> Result<Vec<u8>> {
138    // Find the GPGME key to export
139    let key = context
140        .get_key(fingerprint)
141        .map_err(|err| Err::Export(Err::UnknownFingerprint(err).into()))?;
142
143    // Export key to memoy with armor enabled
144    let mut data: Vec<u8> = vec![];
145    let armor = context.armor();
146    context.set_armor(true);
147    context.export_keys(&[key], gpgme::ExportMode::empty(), &mut data)?;
148    context.set_armor(armor);
149
150    // Assert we're exporting a public key
151    let data_str = std::str::from_utf8(&data).expect("exported key is invalid UTF-8");
152    assert!(
153        !data_str.contains("PRIVATE KEY"),
154        "exported key contains PRIVATE KEY, blocked to prevent accidentally leaking secret key"
155    );
156    assert!(
157        data_str.contains("PUBLIC KEY"),
158        "exported key must contain PUBLIC KEY, blocked to prevent accidentally leaking secret key"
159    );
160
161    Ok(data)
162}
163
164/// A key identifier with a fingerprint and user IDs.
165#[derive(Clone)]
166pub struct KeyId(pub String, pub Vec<String>);
167
168impl From<Key> for KeyId {
169    fn from(key: Key) -> Self {
170        Self(
171            key.fingerprint()
172                .expect("GPGME key does not have fingerprint")
173                .to_string(),
174            key.user_ids()
175                .map(|user| {
176                    let mut parts = vec![];
177                    if let Ok(name) = user.name()
178                        && !name.trim().is_empty()
179                    {
180                        parts.push(name.into());
181                    }
182                    if let Ok(comment) = user.comment()
183                        && !comment.trim().is_empty()
184                    {
185                        parts.push(format!("({comment})"));
186                    }
187                    if let Ok(email) = user.email()
188                        && !email.trim().is_empty()
189                    {
190                        parts.push(format!("<{email}>"));
191                    }
192                    parts.join(" ")
193                })
194                .collect(),
195        )
196    }
197}
198
199/// Transform fingerprints into GPGME keys.
200///
201/// Errors if a fingerprint does not match a public key.
202fn fingerprints_to_keys(context: &mut Context, fingerprints: &[&str]) -> Result<Vec<Key>> {
203    let mut keys = vec![];
204    for fp in fingerprints {
205        keys.push(
206            context
207                .get_key(fp.to_owned())
208                .map_err(Err::UnknownFingerprint)?,
209        );
210    }
211    Ok(keys)
212}
213
214/// GnuPG binary error.
215#[derive(Debug, Error)]
216pub enum Err {
217    #[error("failed to obtain GPGME cryptography context")]
218    Context(#[source] gpgme::Error),
219
220    #[error("failed to encrypt plaintext")]
221    Encrypt(#[source] gpgme::Error),
222
223    #[error("failed to decrypt ciphertext")]
224    Decrypt(#[source] gpgme::Error),
225
226    #[error("failed to import key")]
227    Import(#[source] anyhow::Error),
228
229    #[error("failed to export key")]
230    Export(#[source] anyhow::Error),
231
232    #[error("fingerprint does not match public key in keychain")]
233    UnknownFingerprint(#[source] gpgme::Error),
234}