Skip to main content

pubhubs/misc/
crypto.rs

1use aead::{Aead as _, AeadCore as _, KeyInit as _};
2use anyhow::Context as _;
3use base64ct::{Base64Url, Encoding as _};
4use chacha20poly1305::XChaCha20Poly1305;
5use rand::TryRng as _;
6
7/// Key used by [`seal`] and co.
8///
9// TODO: nice-to-have: make SealingKey zeroize::Zeroize
10// This will likely be possible when chacha20poly1305 move away from generic-array
11pub type SealingKey = chacha20poly1305::Key;
12
13/// Generates a random 22 character alphanumeric string (`[a-zA-Z0-9]{22}`),
14/// having > 128 bits of randomness.
15pub fn random_alphanumeric() -> String {
16    use rand::RngExt as _;
17
18    rand::rand_core::UnwrapErr(rand::rngs::SysRng)
19        .sample_iter(&rand::distr::Alphanumeric)
20        .take(22)
21        .map(char::from)
22        .collect()
23}
24
25pub fn random_32_bytes() -> [u8; 32] {
26    let mut bytes: [u8; 32] = [0; 32];
27
28    rand::rngs::SysRng::try_fill_bytes(&mut rand::rngs::SysRng, bytes.as_mut_slice()).unwrap();
29
30    bytes
31}
32
33/// Like [`seal`], but returns an urlsafe base64 encoded string.
34/// and returns it as urlsafe base64 string.  Use [`url_unseal`] to revert.
35pub fn url_seal<T: serde::Serialize>(
36    obj: &T,
37    key: &SealingKey,
38    aad: impl AsRef<[u8]>,
39) -> anyhow::Result<String> {
40    let buf: Vec<u8> = seal(obj, key, aad)?;
41
42    Ok(Base64Url::encode_string(&buf))
43}
44
45/// Reverse of the [`url_seal`] operation.
46pub fn url_unseal<T: serde::de::DeserializeOwned>(
47    envelope: impl AsRef<str>,
48    key: &chacha20poly1305::Key,
49    aad: impl AsRef<[u8]>,
50) -> Result<T, crate::misc::error::Opaque> {
51    let buf = Base64Url::decode_vec(envelope.as_ref()).map_err(|_| crate::misc::error::OPAQUE)?;
52
53    unseal(&buf, key, aad)
54}
55
56/// Encodes and encrypts the given `obj` with additional associated data (or `b""` if `None`).
57/// Use [`unseal`] to revert.
58///
59/// Uses a non self-describing encoding format for `T`, so [`seal`] is not suitable for long-lived
60/// data that might change.
61pub fn seal<T: serde::Serialize>(
62    obj: &T,
63    key: &SealingKey,
64    aad: impl AsRef<[u8]>,
65) -> anyhow::Result<Vec<u8>> {
66    let plaintext = postcard::to_stdvec(obj).context("serializing")?;
67
68    // NOTE: generally it's a bad idea to permit an unlimited amount of initialization vectors for
69    // the same AEAD key, but we use XChaCha20Poly1305, a variant of ChaCha20Poly1305 specifically
70    // made for this exact use case.
71    let nonce = XChaCha20Poly1305::generate_nonce(&mut aead::OsRng);
72    let ciphertext = XChaCha20Poly1305::new(key)
73        .encrypt(
74            &nonce,
75            aead::Payload {
76                msg: plaintext.as_slice(),
77                aad: aad.as_ref(),
78            },
79        )
80        .map_err(|e| anyhow::anyhow!(e))
81        .context("encrypting")?;
82
83    let mut buf = Vec::with_capacity(nonce.len() + ciphertext.len());
84    buf.extend_from_slice(&nonce);
85    buf.extend_from_slice(&ciphertext);
86
87    Ok(buf)
88}
89
90/// Reverse of the [`seal`] operation.
91pub fn unseal<T: serde::de::DeserializeOwned>(
92    envelope: impl AsRef<[u8]>,
93    key: &SealingKey,
94    aad: impl AsRef<[u8]>,
95) -> Result<T, crate::misc::error::Opaque> {
96    let nonce_len: usize = chacha20poly1305::XNonce::len();
97    let envelope = envelope.as_ref();
98
99    if envelope.len() < nonce_len {
100        log::debug!("unseal: envelope does not contain nonce");
101        return Err(crate::misc::error::OPAQUE);
102    }
103
104    let plaintext = XChaCha20Poly1305::new(key)
105        .decrypt(
106            (&envelope[..nonce_len]).into(),
107            aead::Payload {
108                msg: &envelope[nonce_len..],
109                aad: aad.as_ref(),
110            },
111        )
112        .map_err(|err| {
113            log::debug!("unseal: decrypting: {err}");
114            crate::misc::error::OPAQUE
115        })?;
116
117    postcard::from_bytes(&plaintext).map_err(|err| {
118        log::debug!("unseal: decoding: {err}");
119        crate::misc::error::OPAQUE
120    })
121}
122
123/// Implements the `generic_array` version `1.2`
124/// [`len()`](https://docs.rs/generic-array/latest/generic_array/struct.GenericArray.html#method.len)
125/// method for the older `generic_array` version currently(?) used by
126/// rust crypto (e.g. [`aead`]).
127pub trait GenericArrayExt {
128    fn len() -> usize;
129}
130
131impl<T, U: aead::generic_array::ArrayLength<T>> GenericArrayExt
132    for aead::generic_array::GenericArray<T, U>
133{
134    fn len() -> usize {
135        <U as typenum::marker_traits::Unsigned>::USIZE
136    }
137}