Skip to main content

age_core/
primitives.rs

1//! Primitive cryptographic operations used across various `age` components.
2
3use core::fmt;
4
5use bech32::primitives::decode::CheckedHrpstring;
6use chacha20poly1305::{
7    aead::{self, generic_array::typenum::Unsigned, Aead, AeadCore, KeyInit},
8    ChaCha20Poly1305,
9};
10use hkdf::Hkdf;
11use sha2::Sha256;
12
13/// `encrypt[key](plaintext)` - encrypts a message with a one-time key.
14///
15/// ChaCha20-Poly1305 from [RFC 7539] with a zero nonce.
16///
17/// [RFC 7539]: https://tools.ietf.org/html/rfc7539
18pub fn aead_encrypt(key: &[u8; 32], plaintext: &[u8]) -> Vec<u8> {
19    let c = ChaCha20Poly1305::new(key.into());
20    c.encrypt(&[0; 12].into(), plaintext)
21        .expect("we won't overflow the ChaCha20 block counter")
22}
23
24/// `decrypt[key](ciphertext)` - decrypts a message of an expected fixed size.
25///
26/// ChaCha20-Poly1305 from [RFC 7539] with a zero nonce.
27///
28/// The message size is limited to mitigate multi-key attacks, where a ciphertext can be
29/// crafted that decrypts successfully under multiple keys. Short ciphertexts can only
30/// target two keys, which has limited impact.
31///
32/// [RFC 7539]: https://tools.ietf.org/html/rfc7539
33pub fn aead_decrypt(
34    key: &[u8; 32],
35    size: usize,
36    ciphertext: &[u8],
37) -> Result<Vec<u8>, aead::Error> {
38    if ciphertext.len() != size + <ChaCha20Poly1305 as AeadCore>::TagSize::to_usize() {
39        return Err(aead::Error);
40    }
41
42    let c = ChaCha20Poly1305::new(key.into());
43    c.decrypt(&[0; 12].into(), ciphertext)
44}
45
46/// `HKDF[salt, label](key, 32)`
47///
48/// HKDF from [RFC 5869] with SHA-256.
49///
50/// [RFC 5869]: https://tools.ietf.org/html/rfc5869
51pub fn hkdf(salt: &[u8], label: &[u8], ikm: &[u8]) -> [u8; 32] {
52    let mut okm = [0; 32];
53    Hkdf::<Sha256>::new(Some(salt), ikm)
54        .expand(label, &mut okm)
55        .expect("okm is the correct length");
56    okm
57}
58
59/// The bech32 checksum algorithm, defined in [BIP-173].
60///
61/// This is identical to [`bech32::Bech32`] except it does not enforce the length
62/// restriction, allowing for a reduction in error-correcting properties.
63///
64/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
65#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66enum Bech32Long {}
67impl bech32::Checksum for Bech32Long {
68    type MidstateRepr = u32;
69    const CODE_LENGTH: usize = usize::MAX;
70    const CHECKSUM_LENGTH: usize = bech32::Bech32::CHECKSUM_LENGTH;
71    const GENERATOR_SH: [u32; 5] = bech32::Bech32::GENERATOR_SH;
72    const TARGET_RESIDUE: u32 = bech32::Bech32::TARGET_RESIDUE;
73}
74
75/// Encodes data as a Bech32-encoded string with the given HRP.
76///
77/// This implements Bech32 as defined in [BIP-173], except it does not enforce the length
78/// restriction, allowing for a reduction in error-correcting properties.
79///
80/// [BIP-173]: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
81pub fn bech32_encode(hrp: bech32::Hrp, data: &[u8]) -> String {
82    bech32::encode_lower::<Bech32Long>(hrp, data).expect("we don't enforce the Bech32 length limit")
83}
84
85/// Encodes data to a format writer as a Bech32-encoded string with the given HRP.
86///
87/// This implements Bech32 as defined in [BIP-173], except it does not enforce the length
88/// restriction, allowing for a reduction in error-correcting properties.
89///
90/// [BIP-173]: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
91pub fn bech32_encode_to_fmt(f: &mut impl fmt::Write, hrp: bech32::Hrp, data: &[u8]) -> fmt::Result {
92    bech32::encode_lower_to_fmt::<Bech32Long, _>(f, hrp, data).map_err(|e| match e {
93        bech32::EncodeError::Fmt(error) => error,
94        bech32::EncodeError::TooLong(_) => unreachable!("we don't enforce the Bech32 length limit"),
95        _ => panic!("Unexpected error: {e}"),
96    })
97}
98
99/// Decodes a Bech32-encoded string, checks its HRP, and returns its contained data.
100///
101/// This implements Bech32 as defined in [BIP-173], except it does not enforce the length
102/// restriction, allowing for a reduction in error-correcting properties.
103///
104/// [BIP-173]: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
105pub fn bech32_decode<E, F, G, H, T>(
106    s: &str,
107    parse_err: F,
108    hrp_filter: G,
109    data_parse: H,
110) -> Result<T, E>
111where
112    F: FnOnce(bech32::primitives::decode::CheckedHrpstringError) -> E,
113    G: FnOnce(bech32::Hrp) -> Result<(), E>,
114    H: FnOnce(bech32::Hrp, bech32::primitives::decode::ByteIter) -> Result<T, E>,
115{
116    CheckedHrpstring::new::<Bech32Long>(s)
117        .map_err(parse_err)
118        .and_then(|parsed| {
119            hrp_filter(parsed.hrp()).and_then(|()| data_parse(parsed.hrp(), parsed.byte_iter()))
120        })
121}
122
123/// `HPKE.SealBase(pk_recip, info, aad = "", plaintext)`
124///
125/// HPKE from [RFC 9180] with:
126/// - KDF: HKDF-SHA256
127/// - AEAD: ChaCha20Poly1305
128/// - `aad = ""` (empty)
129///
130/// # Panics
131///
132/// Panics if the configured `Kem` produces an error. The native age recipient types that
133/// use HPKE are configured with parameters that ensure errors either cannot occur or are
134/// cryptographically negligible. If you are using this method for an age plugin, ensure
135/// that you choose a KEM with equivalent properties.
136///
137/// [RFC 9180]: https://tools.ietf.org/html/rfc9180
138pub fn hpke_seal<Kem: hpke::Kem, R: rand::RngCore + rand::CryptoRng>(
139    pk_recip: &Kem::PublicKey,
140    info: &[u8],
141    plaintext: &[u8],
142    rng: &mut R,
143) -> (Kem::EncappedKey, Vec<u8>) {
144    hpke::single_shot_seal::<hpke::aead::ChaCha20Poly1305, hpke::kdf::HkdfSha256, Kem, R>(
145        &hpke::OpModeS::Base,
146        pk_recip,
147        info,
148        plaintext,
149        &[],
150        rng,
151    )
152    .expect("no errors should occur with these HPKE parameters")
153}
154
155/// `HPKE.OpenBase(enc, sk_recip, info, aad = "", ciphertext)`
156///
157/// HPKE from [RFC 9180] with:
158/// - KDF: HKDF-SHA256
159/// - AEAD: ChaCha20Poly1305
160/// - `aad = ""` (empty)
161///
162/// [RFC 9180]: https://tools.ietf.org/html/rfc9180
163pub fn hpke_open<Kem: hpke::Kem>(
164    encapped_key: &Kem::EncappedKey,
165    sk_recip: &Kem::PrivateKey,
166    info: &[u8],
167    ciphertext: &[u8],
168) -> Result<Vec<u8>, hpke::HpkeError> {
169    hpke::single_shot_open::<hpke::aead::ChaCha20Poly1305, hpke::kdf::HkdfSha256, Kem>(
170        &hpke::OpModeR::Base,
171        sk_recip,
172        encapped_key,
173        info,
174        ciphertext,
175        &[],
176    )
177}
178
179#[cfg(test)]
180mod tests {
181    use hpke::Kem;
182    use rand::rngs::OsRng;
183
184    use super::{aead_decrypt, aead_encrypt, bech32_decode, bech32_encode, hpke_open, hpke_seal};
185
186    #[test]
187    fn aead_round_trip() {
188        let key = [14; 32];
189        let plaintext = b"12345678";
190        let encrypted = aead_encrypt(&key, plaintext);
191        let decrypted = aead_decrypt(&key, plaintext.len(), &encrypted).unwrap();
192        assert_eq!(decrypted, plaintext);
193    }
194
195    #[test]
196    fn bech32_round_trip() {
197        let hrp = bech32::Hrp::parse_unchecked("12345678");
198        let data = [14; 32];
199        let encoded = bech32_encode(hrp, &data);
200        let decoded = bech32_decode(
201            &encoded,
202            |_| (),
203            |parsed_hrp| (parsed_hrp == hrp).then_some(()).ok_or(()),
204            |_, bytes| Ok(bytes.collect::<Vec<_>>()),
205        )
206        .unwrap();
207        assert_eq!(decoded, data);
208    }
209
210    #[test]
211    fn hpke_round_trip() {
212        type Kem = hpke::kem::DhP256HkdfSha256;
213        let mut rng = OsRng;
214
215        let (sk_recip, pk_recip) = Kem::gen_keypair(&mut rng);
216
217        let info = b"foobar";
218        let plaintext = b"12345678";
219
220        let (encapped_key, ciphertext) = hpke_seal::<Kem, _>(&pk_recip, info, plaintext, &mut rng);
221        let decrypted = hpke_open::<Kem>(&encapped_key, &sk_recip, info, &ciphertext).unwrap();
222
223        assert_eq!(decrypted, plaintext);
224    }
225}