isb_core/secrets/
inline.rs1use std::io::{Read, Write};
10
11use age::armor::{ArmoredReader, ArmoredWriter, Format};
12
13use super::Recipient;
14use crate::error::{Error, Result};
15
16const ARMOR_BEGIN: &str = "-----BEGIN AGE ENCRYPTED FILE-----";
17
18fn age_err(step: &str, e: impl std::fmt::Display) -> Error {
19 Error::invalid(format!("age {step}: {e}"))
20}
21
22fn encryptor(recipients: &[Recipient]) -> Result<age::Encryptor> {
23 if recipients.is_empty() {
24 return Err(Error::invalid("age encrypt: no recipients"));
25 }
26 age::Encryptor::with_recipients(recipients.iter().map(Recipient::as_age))
27 .map_err(|e| age_err("encrypt", e))
28}
29
30pub fn encrypt(value: &[u8], recipients: &[Recipient]) -> Result<Vec<u8>> {
32 let mut out = Vec::with_capacity(value.len() + 256);
33 let mut w = encryptor(recipients)?
34 .wrap_output(&mut out)
35 .map_err(|e| age_err("encrypt", e))?;
36 w.write_all(value).map_err(|e| age_err("encrypt", e))?;
37 w.finish().map_err(|e| age_err("encrypt", e))?;
38 Ok(out)
39}
40
41pub fn decrypt(ciphertext: &[u8], identities: &[&dyn age::Identity]) -> Result<Vec<u8>> {
44 let d =
45 age::Decryptor::new(ArmoredReader::new(ciphertext)).map_err(|e| age_err("decrypt", e))?;
46 if d.is_scrypt() {
47 return Err(Error::invalid(
48 "age decrypt: passphrase-encrypted values are not supported; encrypt to a recipient",
49 ));
50 }
51 let mut r = d
52 .decrypt(identities.iter().copied())
53 .map_err(|e| age_err("decrypt", e))?;
54 let mut out = Vec::new();
55 r.read_to_end(&mut out).map_err(|e| age_err("decrypt", e))?;
56 Ok(out)
57}
58
59pub fn encrypt_inline(value: &[u8], recipients: &[Recipient]) -> Result<String> {
61 let mut out = Vec::new();
62 let armor = ArmoredWriter::wrap_output(&mut out, Format::AsciiArmor)
63 .map_err(|e| age_err("encrypt", e))?;
64 let mut w = encryptor(recipients)?
65 .wrap_output(armor)
66 .map_err(|e| age_err("encrypt", e))?;
67 w.write_all(value).map_err(|e| age_err("encrypt", e))?;
68 w.finish()
69 .and_then(|a| a.finish())
70 .map_err(|e| age_err("encrypt", e))?;
71 String::from_utf8(out).map_err(|e| age_err("encrypt", e))
72}
73
74pub fn decrypt_inline(text: &str, identities: &[&dyn age::Identity]) -> Result<Vec<u8>> {
76 let t = text.trim();
77 if t.starts_with(ARMOR_BEGIN) {
78 let norm: String = t.lines().map(|l| l.trim_end().to_string() + "\n").collect();
81 return decrypt(norm.as_bytes(), identities);
82 }
83 let bin = crate::rpc::b64_decode(t).map_err(|_| {
84 Error::invalid(
85 "age: expected ASCII-armored ciphertext (age -a) or base64 of age's binary format",
86 )
87 })?;
88 decrypt(&bin, identities)
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn ssh_identity() -> impl age::Identity {
96 let key = include_bytes!("testdata/break_glass_ed25519");
97 age::ssh::Identity::from_buffer(&key[..], None)
98 .unwrap()
99 .with_callbacks(age::NoCallbacks)
100 }
101
102 #[test]
103 fn inline_round_trip_armored_and_base64() {
104 let id = age::x25519::Identity::generate();
105 let rs = vec![Recipient::X25519(id.to_public())];
106 let a = encrypt_inline(b"s3cret\n", &rs).unwrap();
107 assert!(a.starts_with(ARMOR_BEGIN));
108 assert!(!a.contains("s3cret"));
109 assert_eq!(decrypt_inline(&a, &[&id]).unwrap(), b"s3cret\n");
110 let messy: String = a.lines().map(|l| format!("{l} \n")).collect();
113 assert_eq!(decrypt_inline(&messy, &[&id]).unwrap(), b"s3cret\n");
114 let b = crate::rpc::b64_encode(&encrypt(b"bin", &rs).unwrap());
115 assert_eq!(decrypt_inline(&b, &[&id]).unwrap(), b"bin");
116 let wrapped: String = b
118 .as_bytes()
119 .chunks(40)
120 .map(|c| String::from_utf8_lossy(c).into_owned() + "\n")
121 .collect();
122 assert_eq!(decrypt_inline(&wrapped, &[&id]).unwrap(), b"bin");
123 let other = age::x25519::Identity::generate();
125 assert!(decrypt_inline(&a, &[&other]).is_err());
126 assert!(decrypt_inline("not age at all", &[&id]).is_err());
127 assert!(encrypt_inline(b"x", &[]).is_err());
128 }
129
130 #[test]
131 fn ssh_recipients_decrypt_with_the_ssh_key() {
132 let r = Recipient::parse(include_str!("testdata/break_glass_ed25519.pub")).unwrap();
133 let ct = encrypt(b"break glass", &[r]).unwrap();
134 let ssh = ssh_identity();
135 assert_eq!(decrypt(&ct, &[&ssh]).unwrap(), b"break glass");
136 }
137}