Skip to main content

age/
lib.rs

1//! *Library for encrypting and decrypting age files*
2//!
3//! This crate implements file encryption according to the [age-encryption.org/v1]
4//! specification. It generates and consumes encrypted files that are compatible with the
5//! [rage] CLI tool, as well as the reference [Go] implementation.
6//!
7//! The encryption and decryption APIs are provided by [`Encryptor`] and [`Decryptor`].
8//! There are several ways to use these:
9//! - For most cases (including programmatic usage):
10//!   - Use [`Encryptor::with_recipients`] with a compatible set of recipients:
11//!     - Classic: [`x25519::Recipient`] and/or [`tag::Recipient`].
12//!     - Post-quantum: [`tagpq::Recipient`].
13//!   - Use [`Decryptor`] with [`x25519::Identity`].
14//! - For passphrase-based encryption and decryption, use [`scrypt::Recipient`] and
15//!   [`scrypt::Identity`], or the helper method [`Encryptor::with_user_passphrase`].
16//!   These should only be used with passphrases that were provided by (or generated for)
17//!   a human.
18//! - For compatibility with existing SSH keys, enable the `ssh` feature flag, and use
19//!   [`ssh::Recipient`] and [`ssh::Identity`].
20//!
21//! Age-encrypted files are binary and non-malleable. To encode them as text, use the
22//! wrapping readers and writers in the [`armor`] module, behind the `armor` feature flag.
23//!
24//! *Caution*: all crate versions prior to 1.0 are beta releases for **testing purposes
25//! only**.
26//!
27//! [age-encryption.org/v1]: https://age-encryption.org/v1
28//! [rage]: https://crates.io/crates/rage
29//! [Go]: https://filippo.io/age
30//!
31//! # Examples
32//!
33//! ## Streamlined APIs
34//!
35//! These are useful when you only need to encrypt to a single recipient, and the data is
36//! small enough to fit in memory.
37//!
38//! ### Recipient-based encryption
39//!
40//! ```
41//! # fn run_main() -> Result<(), ()> {
42//! let key = age::x25519::Identity::generate();
43//! let pubkey = key.to_public();
44//!
45//! let plaintext = b"Hello world!";
46//!
47//! # fn encrypt(pubkey: age::x25519::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
48//! let encrypted = age::encrypt(&pubkey, plaintext)?;
49//! # Ok(encrypted)
50//! # }
51//! # fn decrypt(key: age::x25519::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
52//! let decrypted = age::decrypt(&key, &encrypted)?;
53//! # Ok(decrypted)
54//! # }
55//! # let decrypted = decrypt(
56//! #     key,
57//! #     encrypt(pubkey, &plaintext[..]).map_err(|_| ())?
58//! # ).map_err(|_| ())?;
59//!
60//! assert_eq!(decrypted, plaintext);
61//! # Ok(())
62//! # }
63//! # run_main().unwrap();
64//! ```
65//!
66//! ## Passphrase-based encryption
67//!
68//! ```
69//! use age::secrecy::SecretString;
70//!
71//! # fn run_main() -> Result<(), ()> {
72//! let passphrase = SecretString::from("this is not a good passphrase".to_owned());
73//! let recipient = age::scrypt::Recipient::new(passphrase.clone());
74//! let identity = age::scrypt::Identity::new(passphrase);
75//!
76//! let plaintext = b"Hello world!";
77//!
78//! # fn encrypt(recipient: age::scrypt::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
79//! let encrypted = age::encrypt(&recipient, plaintext)?;
80//! # Ok(encrypted)
81//! # }
82//! # fn decrypt(identity: age::scrypt::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
83//! let decrypted = age::decrypt(&identity, &encrypted)?;
84//! # Ok(decrypted)
85//! # }
86//! # let decrypted = decrypt(
87//! #     identity,
88//! #     encrypt(recipient, &plaintext[..]).map_err(|_| ())?
89//! # ).map_err(|_| ())?;
90//!
91//! assert_eq!(decrypted, plaintext);
92//! # Ok(())
93//! # }
94//! # run_main().unwrap();
95//! ```
96//!
97//! ## Full APIs
98//!
99//! The full APIs support encrypting to multiple recipients, streaming the data, and have
100//! async I/O options.
101//!
102//! ### Recipient-based encryption
103//!
104//! ```
105//! use std::io::{Read, Write};
106//! use std::iter;
107//!
108//! # fn run_main() -> Result<(), ()> {
109//! let key = age::x25519::Identity::generate();
110//! let pubkey = key.to_public();
111//!
112//! let plaintext = b"Hello world!";
113//!
114//! // Encrypt the plaintext to a ciphertext...
115//! # fn encrypt(pubkey: age::x25519::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
116//! let encrypted = {
117//!     let encryptor = age::Encryptor::with_recipients(iter::once(&pubkey as _))
118//!         .expect("we provided a recipient");
119//!
120//!     let mut encrypted = vec![];
121//!     let mut writer = encryptor.wrap_output(&mut encrypted)?;
122//!     writer.write_all(plaintext)?;
123//!     writer.finish()?;
124//!
125//!     encrypted
126//! };
127//! # Ok(encrypted)
128//! # }
129//!
130//! // ... and decrypt the obtained ciphertext to the plaintext again.
131//! # fn decrypt(key: age::x25519::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
132//! let decrypted = {
133//!     let decryptor = age::Decryptor::new(&encrypted[..])?;
134//!
135//!     let mut decrypted = vec![];
136//!     let mut reader = decryptor.decrypt(iter::once(&key as &dyn age::Identity))?;
137//!     reader.read_to_end(&mut decrypted);
138//!
139//!     decrypted
140//! };
141//! # Ok(decrypted)
142//! # }
143//! # let decrypted = decrypt(
144//! #     key,
145//! #     encrypt(pubkey, &plaintext[..]).map_err(|_| ())?
146//! # ).map_err(|_| ())?;
147//!
148//! assert_eq!(decrypted, plaintext);
149//! # Ok(())
150//! # }
151//!
152//! # run_main().unwrap();
153//! ```
154//!
155//! ## Passphrase-based encryption
156//!
157//! ```
158//! use age::secrecy::SecretString;
159//! use std::io::{Read, Write};
160//! use std::iter;
161//!
162//! # fn run_main() -> Result<(), ()> {
163//! let plaintext = b"Hello world!";
164//! let passphrase = SecretString::from("this is not a good passphrase".to_owned());
165//!
166//! // Encrypt the plaintext to a ciphertext using the passphrase...
167//! # fn encrypt(passphrase: SecretString, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
168//! let encrypted = {
169//!     let encryptor = age::Encryptor::with_user_passphrase(passphrase.clone());
170//!
171//!     let mut encrypted = vec![];
172//!     let mut writer = encryptor.wrap_output(&mut encrypted)?;
173//!     writer.write_all(plaintext)?;
174//!     writer.finish()?;
175//!
176//!     encrypted
177//! };
178//! # Ok(encrypted)
179//! # }
180//!
181//! // ... and decrypt the ciphertext to the plaintext again using the same passphrase.
182//! # fn decrypt(passphrase: SecretString, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
183//! let decrypted = {
184//!     let decryptor = age::Decryptor::new(&encrypted[..])?;
185//!
186//!     let mut decrypted = vec![];
187//!     let mut reader = decryptor.decrypt(iter::once(&age::scrypt::Identity::new(passphrase) as _))?;
188//!     reader.read_to_end(&mut decrypted);
189//!
190//!     decrypted
191//! };
192//! # Ok(decrypted)
193//! # }
194//! # let decrypted = decrypt(
195//! #     passphrase.clone(),
196//! #     encrypt(passphrase, &plaintext[..]).map_err(|_| ())?
197//! # ).map_err(|_| ())?;
198//!
199//! assert_eq!(decrypted, plaintext);
200//! # Ok(())
201//! # }
202//! # run_main().unwrap();
203//! ```
204
205#![cfg_attr(docsrs, feature(doc_cfg))]
206#![forbid(unsafe_code)]
207// Catch documentation errors caused by code changes.
208#![deny(rustdoc::broken_intra_doc_links)]
209#![deny(missing_docs)]
210
211use std::collections::HashSet;
212
213// Re-export crates that are used in our public API.
214pub use age_core::secrecy;
215
216mod error;
217mod format;
218mod identity;
219mod keys;
220mod primitives;
221mod protocol;
222mod util;
223
224pub use error::{DecryptError, EncryptError, IdentityFileConvertError};
225pub use identity::IdentityFile;
226pub use primitives::stream;
227pub use protocol::{Decryptor, Encryptor};
228
229#[cfg(feature = "armor")]
230#[cfg_attr(docsrs, doc(cfg(feature = "armor")))]
231pub use primitives::armor;
232
233#[cfg(feature = "cli-common")]
234#[cfg_attr(docsrs, doc(cfg(feature = "cli-common")))]
235pub mod cli_common;
236
237mod i18n;
238pub use i18n::localizer;
239
240//
241// Simple interface
242//
243
244mod simple;
245pub use simple::{decrypt, encrypt};
246
247#[cfg(feature = "armor")]
248#[cfg_attr(docsrs, doc(cfg(feature = "armor")))]
249pub use simple::encrypt_and_armor;
250
251//
252// Identity types
253//
254
255mod native;
256pub use native::scrypt;
257pub use native::tag;
258pub use native::tagpq;
259pub use native::x25519;
260
261pub mod encrypted;
262
263#[cfg(feature = "plugin")]
264#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
265pub mod plugin;
266
267#[cfg(feature = "ssh")]
268#[cfg_attr(docsrs, doc(cfg(feature = "ssh")))]
269pub mod ssh;
270
271//
272// Core traits
273//
274
275use age_core::{
276    format::{FileKey, Stanza},
277    secrecy::SecretString,
278};
279
280/// A private key or other value that can unwrap an opaque file key from a recipient
281/// stanza.
282///
283/// # Implementation notes
284///
285/// The canonical entry point for this trait is [`Identity::unwrap_stanzas`]. The default
286/// implementation of that method is:
287/// ```ignore
288/// stanzas.iter().find_map(|stanza| self.unwrap_stanza(stanza))
289/// ```
290///
291/// The `age` crate otherwise does not call [`Identity::unwrap_stanza`] directly. As such,
292/// if you want to add file-level stanza checks, override [`Identity::unwrap_stanzas`].
293pub trait Identity {
294    /// Attempts to unwrap the given stanza with this identity.
295    ///
296    /// This method is part of the `Identity` trait to expose age's [one joint] for
297    /// external implementations. You should not need to call this directly; instead, pass
298    /// identities to [`Decryptor::decrypt`].
299    ///
300    /// The `age` crate only calls this method via [`Identity::unwrap_stanzas`].
301    ///
302    /// Returns:
303    /// - `Some(Ok(file_key))` on success.
304    /// - `Some(Err(e))` if a decryption error occurs.
305    /// - `None` if the recipient stanza does not match this key.
306    ///
307    /// [one joint]: https://www.imperialviolet.org/2016/05/16/agility.html
308    fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>>;
309
310    /// Attempts to unwrap any of the given stanzas, which are assumed to come from the
311    /// same age file header, and therefore contain the same file key.
312    ///
313    /// This method is part of the `Identity` trait to expose age's [one joint] for
314    /// external implementations. You should not need to call this directly; instead, pass
315    /// identities to [`Decryptor::decrypt`].
316    ///
317    /// Returns:
318    /// - `Some(Ok(file_key))` on success.
319    /// - `Some(Err(e))` if a decryption error occurs.
320    /// - `None` if none of the recipient stanzas match this identity.
321    ///
322    /// [one joint]: https://www.imperialviolet.org/2016/05/16/agility.html
323    fn unwrap_stanzas(&self, stanzas: &[Stanza]) -> Option<Result<FileKey, DecryptError>> {
324        stanzas.iter().find_map(|stanza| self.unwrap_stanza(stanza))
325    }
326}
327
328/// A public key or other value that can wrap an opaque file key to a recipient stanza.
329///
330/// Implementations of this trait might represent more than one recipient.
331pub trait Recipient {
332    /// Wraps the given file key, returning stanzas to be placed in an age file header,
333    /// and labels that constrain how the stanzas may be combined with those from other
334    /// recipients.
335    ///
336    /// Implementations may return more than one stanza per "actual recipient", e.g. to
337    /// support multiple formats, to build group aliases, or to act as a proxy.
338    ///
339    /// This method is part of the `Recipient` trait to expose age's [one joint] for
340    /// external implementations. You should not need to call this directly; instead, pass
341    /// recipients to [`Encryptor::with_recipients`].
342    ///
343    /// [one joint]: https://www.imperialviolet.org/2016/05/16/agility.html
344    ///
345    /// # Labels
346    ///
347    /// [`Encryptor`] will succeed at encrypting only if every recipient returns the same
348    /// set of labels. Subsets or partial overlapping sets are not allowed; all sets must
349    /// be identical. Labels are compared exactly, and are case-sensitive.
350    ///
351    /// Label sets can be used to ensure a recipient is only encrypted to alongside other
352    /// recipients with equivalent properties, or to ensure a recipient is always used
353    /// alone. A recipient with no particular properties to enforce should return an empty
354    /// label set.
355    ///
356    /// Labels can have any value that is a valid arbitrary string (`1*VCHAR` in ABNF),
357    /// but usually take one of several forms:
358    ///   - *Common public label* - used by multiple recipients to permit their stanzas to
359    ///     be used only together. Examples include:
360    ///     - `postquantum` - indicates that the recipient stanzas being generated are
361    ///       postquantum-secure, and that they can only be combined with other stanzas
362    ///       that are also postquantum-secure.
363    ///   - *Common private label* - used by recipients created by the same private entity
364    ///     to permit their recipient stanzas to be used only together. For example,
365    ///     private recipients used in a corporate environment could all send the same
366    ///     private label in order to prevent compliant age clients from simultaneously
367    ///     wrapping file keys with other recipients.
368    ///   - *Random label* - used by recipients that want to ensure their stanzas are not
369    ///     used with any other recipient stanzas. This can be used to produce a file key
370    ///     that is only encrypted to a single recipient stanza, for example to preserve
371    ///     its authentication properties.
372    fn wrap_file_key(
373        &self,
374        file_key: &FileKey,
375    ) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError>;
376}
377
378/// Callbacks that might be triggered during encryption or decryption.
379///
380/// Structs that implement this trait should be given directly to the individual
381/// `Recipient` or `Identity` implementations that require them.
382pub trait Callbacks: Clone + Send + Sync + 'static {
383    /// Shows a message to the user.
384    ///
385    /// This can be used to prompt the user to take some physical action, such as
386    /// inserting a hardware key.
387    ///
388    /// No guarantee is provided that the user sees this message (for example, if there is
389    /// no UI for displaying messages).
390    fn display_message(&self, message: &str);
391
392    /// Requests that the user provides confirmation for some action.
393    ///
394    /// This can be used to, for example, request that a hardware key the plugin wants to
395    /// try either be plugged in, or skipped.
396    ///
397    /// - `message` is the request or call-to-action to be displayed to the user.
398    /// - `yes_string` and (optionally) `no_string` will be displayed on buttons or next
399    ///   to selection options in the user's UI.
400    ///
401    /// Returns:
402    /// - `Some(true)` if the user selected the option marked with `yes_string`.
403    /// - `Some(false)` if the user selected the option marked with `no_string` (or the
404    ///   default negative confirmation label).
405    /// - `None` if the confirmation request could not be given to the user (for example,
406    ///   if there is no UI for displaying messages).
407    fn confirm(&self, message: &str, yes_string: &str, no_string: Option<&str>) -> Option<bool>;
408
409    /// Requests non-private input from the user.
410    ///
411    /// To request private inputs, use [`Callbacks::request_passphrase`].
412    ///
413    /// Returns:
414    /// - `Some(input)` with the user-provided input.
415    /// - `None` if no input could be requested from the user (for example, if there is no
416    ///   UI for displaying messages or typing inputs).
417    fn request_public_string(&self, description: &str) -> Option<String>;
418
419    /// Requests a passphrase to decrypt a key.
420    ///
421    /// Returns:
422    /// - `Some(passphrase)` with the user-provided passphrase.
423    /// - `None` if no passphrase could be requested from the user (for example, if there
424    ///   is no UI for displaying messages or typing inputs).
425    fn request_passphrase(&self, description: &str) -> Option<SecretString>;
426}
427
428/// An implementation of [`Callbacks`] that does not allow callbacks.
429///
430/// No user interaction will occur; [`Recipient`] or [`Identity`] implementations will
431/// receive `None` from the callbacks that return responses, and will act accordingly.
432#[derive(Clone, Copy, Debug)]
433pub struct NoCallbacks;
434
435impl Callbacks for NoCallbacks {
436    fn display_message(&self, _: &str) {}
437
438    fn confirm(&self, _: &str, _: &str, _: Option<&str>) -> Option<bool> {
439        None
440    }
441
442    fn request_public_string(&self, _: &str) -> Option<String> {
443        None
444    }
445
446    fn request_passphrase(&self, _: &str) -> Option<SecretString> {
447        None
448    }
449}
450
451//
452// Fuzzing APIs
453//
454
455/// Helper for fuzzing the Header parser and serializer.
456#[cfg(fuzzing)]
457pub fn fuzz_header(data: &[u8]) {
458    if let Ok(header) = format::Header::read(data) {
459        let mut buf = Vec::with_capacity(data.len());
460        header.write(&mut buf).expect("can write header");
461        assert_eq!(&buf[..], &data[..buf.len()]);
462    }
463}