Skip to content

Commit

Permalink
age: Add streamlined APIs for encryption and decryption
Browse files Browse the repository at this point in the history
Closes #333.
  • Loading branch information
str4d committed Aug 30, 2024
1 parent 9af4790 commit ef92286
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 1 deletion.
5 changes: 5 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ to 1.0.0 are beta releases.

## [Unreleased]
### Added
- New streamlined APIs for use with a single recipient or identity and a small
amount of data (that can fit entirely in memory):
- `age::encrypt`
- `age::encrypt_and_armor`
- `age::decrypt`
- `age::Decryptor::{decrypt, decrypt_async, is_scrypt}`
- `age::IdentityFile::to_recipients`
- `age::IdentityFile::with_callbacks`
Expand Down
89 changes: 88 additions & 1 deletion age/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,76 @@
//!
//! # Examples
//!
//! ## Recipient-based encryption
//! ## Streamlined APIs
//!
//! These are useful when you only need to encrypt to a single recipient, and the data is
//! small enough to fit in memory.
//!
//! ### Recipient-based encryption
//!
//! ```
//! # fn run_main() -> Result<(), ()> {
//! let key = age::x25519::Identity::generate();
//! let pubkey = key.to_public();
//!
//! let plaintext = b"Hello world!";
//!
//! # fn encrypt(pubkey: age::x25519::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
//! let encrypted = age::encrypt(&pubkey, plaintext)?;
//! # Ok(encrypted)
//! # }
//! # fn decrypt(key: age::x25519::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
//! let decrypted = age::decrypt(&key, &encrypted)?;
//! # Ok(decrypted)
//! # }
//! # let decrypted = decrypt(
//! # key,
//! # encrypt(pubkey, &plaintext[..]).map_err(|_| ())?
//! # ).map_err(|_| ())?;
//!
//! assert_eq!(decrypted, plaintext);
//! # Ok(())
//! # }
//! # run_main().unwrap();
//! ```
//!
//! ## Passphrase-based encryption
//!
//! ```
//! use age::secrecy::Secret;
//!
//! # fn run_main() -> Result<(), ()> {
//! let passphrase = Secret::new("this is not a good passphrase".to_owned());
//! let recipient = age::scrypt::Recipient::new(passphrase.clone());
//! let identity = age::scrypt::Identity::new(passphrase);
//!
//! let plaintext = b"Hello world!";
//!
//! # fn encrypt(recipient: age::scrypt::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
//! let encrypted = age::encrypt(&recipient, plaintext)?;
//! # Ok(encrypted)
//! # }
//! # fn decrypt(identity: age::scrypt::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
//! let decrypted = age::decrypt(&identity, &encrypted)?;
//! # Ok(decrypted)
//! # }
//! # let decrypted = decrypt(
//! # identity,
//! # encrypt(recipient, &plaintext[..]).map_err(|_| ())?
//! # ).map_err(|_| ())?;
//!
//! assert_eq!(decrypted, plaintext);
//! # Ok(())
//! # }
//! # run_main().unwrap();
//! ```
//!
//! ## Full APIs
//!
//! The full APIs support encrypting to multiple recipients, streaming the data, and have
//! async I/O options.
//!
//! ### Recipient-based encryption
//!
//! ```
//! use std::io::{Read, Write};
Expand Down Expand Up @@ -164,6 +233,16 @@ pub mod cli_common;
mod i18n;
pub use i18n::localizer;

//
// Simple interface
//

mod simple;
pub use simple::{decrypt, encrypt};

#[cfg(feature = "armor")]
pub use simple::encrypt_and_armor;

//
// Identity types
//
Expand All @@ -180,6 +259,10 @@ pub mod plugin;
#[cfg_attr(docsrs, doc(cfg(feature = "ssh")))]
pub mod ssh;

//
// Core traits
//

use age_core::{
format::{FileKey, Stanza},
secrecy::SecretString,
Expand Down Expand Up @@ -342,6 +425,10 @@ impl Callbacks for NoCallbacks {
}
}

//
// Fuzzing APIs
//

/// Helper for fuzzing the Header parser and serializer.
#[cfg(fuzzing)]
pub fn fuzz_header(data: &[u8]) {
Expand Down
106 changes: 106 additions & 0 deletions age/src/simple.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use std::io::{Read, Write};
use std::iter;

use crate::{
error::{DecryptError, EncryptError},
Decryptor, Encryptor, Identity, Recipient,
};

#[cfg(feature = "armor")]
use crate::armor::{ArmoredReader, ArmoredWriter, Format};

/// Encrypts the given plaintext to the given recipient.
///
/// To encrypt to more than one recipient, use [`Encryptor::with_recipients`].
///
/// This function returns binary ciphertext. To obtain an ASCII-armored text string, use
/// [`encrypt_and_armor`].
pub fn encrypt(recipient: &impl Recipient, plaintext: &[u8]) -> Result<Vec<u8>, EncryptError> {
let encryptor =
Encryptor::with_recipients(iter::once(recipient as _)).expect("we provided a recipient");

let mut ciphertext = Vec::with_capacity(plaintext.len());
let mut writer = encryptor.wrap_output(&mut ciphertext)?;
writer.write_all(plaintext)?;
writer.finish()?;

Ok(ciphertext)
}

/// Encrypts the given plaintext to the given recipient, and wraps the ciphertext in ASCII
/// armor.
///
/// To encrypt to more than one recipient, use [`Encryptor::with_recipients`] along with
/// [`ArmoredWriter`].
#[cfg(feature = "armor")]
pub fn encrypt_and_armor(
recipient: &impl Recipient,
plaintext: &[u8],
) -> Result<String, EncryptError> {
let encryptor =
Encryptor::with_recipients(iter::once(recipient as _)).expect("we provided a recipient");

let mut ciphertext = Vec::with_capacity(plaintext.len());
let mut writer = encryptor.wrap_output(ArmoredWriter::wrap_output(
&mut ciphertext,
Format::AsciiArmor,
)?)?;
writer.write_all(plaintext)?;
writer.finish()?.finish()?;

Ok(String::from_utf8(ciphertext).expect("is armored"))
}

/// Decrypts the given ciphertext with the given identity.
///
/// If the `armor` feature flag is enabled, this will also handle armored age ciphertexts.
///
/// To attempt decryption with more than one identity, use [`Decryptor`] (as well as
/// [`ArmoredReader`] if the `armor` feature flag is enabled).
pub fn decrypt(identity: &impl Identity, ciphertext: &[u8]) -> Result<Vec<u8>, DecryptError> {
#[cfg(feature = "armor")]
let decryptor = Decryptor::new_buffered(ArmoredReader::new(ciphertext))?;

#[cfg(not(feature = "armor"))]
let decryptor = Decryptor::new_buffered(ciphertext)?;

let mut plaintext = vec![];
let mut reader = decryptor.decrypt(iter::once(identity as _))?;
reader.read_to_end(&mut plaintext)?;

Ok(plaintext)
}

#[cfg(test)]
mod tests {
use super::{decrypt, encrypt};
use crate::x25519;

#[cfg(feature = "armor")]
use super::encrypt_and_armor;

#[test]
fn x25519_round_trip() {
let sk: x25519::Identity = crate::x25519::tests::TEST_SK.parse().unwrap();
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
let test_msg = b"This is a test message. For testing.";

let encrypted = encrypt(&pk, test_msg).unwrap();
let decrypted = decrypt(&sk, &encrypted).unwrap();
assert_eq!(&decrypted[..], &test_msg[..]);
}

#[cfg(feature = "armor")]
#[test]
fn x25519_round_trip_armor() {
let sk: x25519::Identity = crate::x25519::tests::TEST_SK.parse().unwrap();
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
let test_msg = b"This is a test message. For testing.";

let encrypted = encrypt_and_armor(&pk, test_msg).unwrap();
assert!(encrypted.starts_with("-----BEGIN AGE ENCRYPTED FILE-----"));

let decrypted = decrypt(&sk, encrypted.as_bytes()).unwrap();
assert_eq!(&decrypted[..], &test_msg[..]);
}
}

0 comments on commit ef92286

Please sign in to comment.