prs_lib/crypto/backend/gpgme/
raw.rs1use anyhow::Result;
6use gpgme::{Context, EncryptFlags, Key};
7use thiserror::Error;
8use zeroize::Zeroize;
9
10use crate::{Ciphertext, Plaintext};
11
12const ENCRYPT_FLAGS: EncryptFlags = EncryptFlags::ALWAYS_TRUST;
14
15pub 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
47pub 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
59pub fn can_decrypt(context: &mut Context, ciphertext: Ciphertext) -> Result<bool> {
69 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
81pub 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
93pub 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
105pub fn import_key(context: &mut Context, key: &[u8]) -> Result<()> {
113 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 context
126 .import(key)
127 .map(|_| ())
128 .map_err(|err| Err::Import(err.into()).into())
129}
130
131pub fn export_key(context: &mut Context, fingerprint: &str) -> Result<Vec<u8>> {
138 let key = context
140 .get_key(fingerprint)
141 .map_err(|err| Err::Export(Err::UnknownFingerprint(err).into()))?;
142
143 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 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#[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
199fn 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#[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}