use std::io::{Read, Write};
use age::armor::{ArmoredReader, ArmoredWriter, Format};
use super::Recipient;
use crate::error::{Error, Result};
const ARMOR_BEGIN: &str = "-----BEGIN AGE ENCRYPTED FILE-----";
fn age_err(step: &str, e: impl std::fmt::Display) -> Error {
Error::invalid(format!("age {step}: {e}"))
}
fn encryptor(recipients: &[Recipient]) -> Result<age::Encryptor> {
if recipients.is_empty() {
return Err(Error::invalid("age encrypt: no recipients"));
}
age::Encryptor::with_recipients(recipients.iter().map(Recipient::as_age))
.map_err(|e| age_err("encrypt", e))
}
pub fn encrypt(value: &[u8], recipients: &[Recipient]) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(value.len() + 256);
let mut w = encryptor(recipients)?
.wrap_output(&mut out)
.map_err(|e| age_err("encrypt", e))?;
w.write_all(value).map_err(|e| age_err("encrypt", e))?;
w.finish().map_err(|e| age_err("encrypt", e))?;
Ok(out)
}
pub fn decrypt(ciphertext: &[u8], identities: &[&dyn age::Identity]) -> Result<Vec<u8>> {
let d =
age::Decryptor::new(ArmoredReader::new(ciphertext)).map_err(|e| age_err("decrypt", e))?;
if d.is_scrypt() {
return Err(Error::invalid(
"age decrypt: passphrase-encrypted values are not supported; encrypt to a recipient",
));
}
let mut r = d
.decrypt(identities.iter().copied())
.map_err(|e| age_err("decrypt", e))?;
let mut out = Vec::new();
r.read_to_end(&mut out).map_err(|e| age_err("decrypt", e))?;
Ok(out)
}
pub fn encrypt_inline(value: &[u8], recipients: &[Recipient]) -> Result<String> {
let mut out = Vec::new();
let armor = ArmoredWriter::wrap_output(&mut out, Format::AsciiArmor)
.map_err(|e| age_err("encrypt", e))?;
let mut w = encryptor(recipients)?
.wrap_output(armor)
.map_err(|e| age_err("encrypt", e))?;
w.write_all(value).map_err(|e| age_err("encrypt", e))?;
w.finish()
.and_then(|a| a.finish())
.map_err(|e| age_err("encrypt", e))?;
String::from_utf8(out).map_err(|e| age_err("encrypt", e))
}
pub fn decrypt_inline(text: &str, identities: &[&dyn age::Identity]) -> Result<Vec<u8>> {
let t = text.trim();
if t.starts_with(ARMOR_BEGIN) {
let norm: String = t.lines().map(|l| l.trim_end().to_string() + "\n").collect();
return decrypt(norm.as_bytes(), identities);
}
let bin = crate::rpc::b64_decode(t).map_err(|_| {
Error::invalid(
"age: expected ASCII-armored ciphertext (age -a) or base64 of age's binary format",
)
})?;
decrypt(&bin, identities)
}
#[cfg(test)]
mod tests {
use super::*;
fn ssh_identity() -> impl age::Identity {
let key = include_bytes!("testdata/break_glass_ed25519");
age::ssh::Identity::from_buffer(&key[..], None)
.unwrap()
.with_callbacks(age::NoCallbacks)
}
#[test]
fn inline_round_trip_armored_and_base64() {
let id = age::x25519::Identity::generate();
let rs = vec![Recipient::X25519(id.to_public())];
let a = encrypt_inline(b"s3cret\n", &rs).unwrap();
assert!(a.starts_with(ARMOR_BEGIN));
assert!(!a.contains("s3cret"));
assert_eq!(decrypt_inline(&a, &[&id]).unwrap(), b"s3cret\n");
let messy: String = a.lines().map(|l| format!("{l} \n")).collect();
assert_eq!(decrypt_inline(&messy, &[&id]).unwrap(), b"s3cret\n");
let b = crate::rpc::b64_encode(&encrypt(b"bin", &rs).unwrap());
assert_eq!(decrypt_inline(&b, &[&id]).unwrap(), b"bin");
let wrapped: String = b
.as_bytes()
.chunks(40)
.map(|c| String::from_utf8_lossy(c).into_owned() + "\n")
.collect();
assert_eq!(decrypt_inline(&wrapped, &[&id]).unwrap(), b"bin");
let other = age::x25519::Identity::generate();
assert!(decrypt_inline(&a, &[&other]).is_err());
assert!(decrypt_inline("not age at all", &[&id]).is_err());
assert!(encrypt_inline(b"x", &[]).is_err());
}
#[test]
fn ssh_recipients_decrypt_with_the_ssh_key() {
let r = Recipient::parse(include_str!("testdata/break_glass_ed25519.pub")).unwrap();
let ct = encrypt(b"break glass", &[r]).unwrap();
let ssh = ssh_identity();
assert_eq!(decrypt(&ct, &[&ssh]).unwrap(), b"break glass");
}
}