1use aead::{Aead as _, Generate as _, KeyInit as _};
2use anyhow::Context as _;
3use base64ct::{Base64Url, Encoding as _};
4use chacha20poly1305::XChaCha20Poly1305;
5use rand::TryRng as _;
6
7pub type SealingKey = chacha20poly1305::Key;
13
14pub fn random_alphanumeric() -> String {
17 use rand::RngExt as _;
18
19 rand::rand_core::UnwrapErr(rand::rngs::SysRng)
20 .sample_iter(&rand::distr::Alphanumeric)
21 .take(22)
22 .map(char::from)
23 .collect()
24}
25
26pub fn random_32_bytes() -> [u8; 32] {
27 random_bytes()
28}
29
30pub fn random_64_bytes() -> [u8; 64] {
33 random_bytes()
34}
35
36fn random_bytes<const N: usize>() -> [u8; N] {
37 let mut bytes: [u8; N] = [0; N];
38
39 rand::rngs::SysRng::try_fill_bytes(&mut rand::rngs::SysRng, bytes.as_mut_slice()).unwrap();
40
41 bytes
42}
43
44pub fn url_seal<T: serde::Serialize>(
47 obj: &T,
48 key: &SealingKey,
49 aad: impl AsRef<[u8]>,
50) -> anyhow::Result<String> {
51 let buf: Vec<u8> = seal(obj, key, aad)?;
52
53 Ok(Base64Url::encode_string(&buf))
54}
55
56pub fn url_unseal<T: serde::de::DeserializeOwned>(
58 envelope: impl AsRef<str>,
59 key: &chacha20poly1305::Key,
60 aad: impl AsRef<[u8]>,
61) -> Result<T, crate::misc::error::Opaque> {
62 let buf = Base64Url::decode_vec(envelope.as_ref())?;
63
64 unseal(&buf, key, aad)
65}
66
67pub fn seal<T: serde::Serialize>(
73 obj: &T,
74 key: &SealingKey,
75 aad: impl AsRef<[u8]>,
76) -> anyhow::Result<Vec<u8>> {
77 let plaintext = postcard::to_stdvec(obj).context("serializing")?;
78
79 let nonce = aead::Nonce::<XChaCha20Poly1305>::generate();
83 let ciphertext = XChaCha20Poly1305::new(key)
84 .encrypt(
85 &nonce,
86 aead::Payload {
87 msg: plaintext.as_slice(),
88 aad: aad.as_ref(),
89 },
90 )
91 .map_err(|e| anyhow::anyhow!(e))
92 .context("encrypting")?;
93
94 let mut buf = Vec::with_capacity(nonce.len() + ciphertext.len());
95 buf.extend_from_slice(&nonce);
96 buf.extend_from_slice(&ciphertext);
97
98 Ok(buf)
99}
100
101pub fn unseal<T: serde::de::DeserializeOwned>(
103 envelope: impl AsRef<[u8]>,
104 key: &SealingKey,
105 aad: impl AsRef<[u8]>,
106) -> Result<T, crate::misc::error::Opaque> {
107 let nonce_len: usize = size_of::<chacha20poly1305::XNonce>();
108 let envelope = envelope.as_ref();
109
110 if envelope.len() < nonce_len {
111 log::debug!("unseal: envelope does not contain nonce");
112 return Err(crate::misc::error::OPAQUE);
113 }
114
115 let nonce: &chacha20poly1305::XNonce = envelope[..nonce_len]
117 .try_into()
118 .expect("nonce slice of the correct length");
119
120 let plaintext = XChaCha20Poly1305::new(key)
121 .decrypt(
122 nonce,
123 aead::Payload {
124 msg: &envelope[nonce_len..],
125 aad: aad.as_ref(),
126 },
127 )
128 .map_err(|err| {
129 log::debug!("unseal: decrypting: {err}");
130 crate::misc::error::OPAQUE
131 })?;
132
133 postcard::from_bytes(&plaintext).map_err(|err| {
134 log::debug!("unseal: decoding: {err}");
135 crate::misc::error::OPAQUE
136 })
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 const PINNED_SEALED: &str =
147 "GM71akKn6Pked-X642S2vP70zv9FioTJeni4r6U44xVDfT0IRcxoe12NcpDjCm9e0cuS6UzgK7c=";
148
149 fn pinned_key() -> SealingKey {
150 [7u8; 32].into()
151 }
152
153 #[test]
154 fn unseal_pinned() {
155 let obj: (u32, String) = url_unseal(PINNED_SEALED, &pinned_key(), b"test-aad").unwrap();
156
157 assert_eq!(obj, (1234, "hello pubhubs".to_string()));
158 }
159
160 #[test]
161 fn seal_unseal_roundtrip() {
162 let key = pinned_key();
163 let obj: (u32, String) = (1234, "hello pubhubs".to_string());
164
165 let sealed = url_seal(&obj, &key, b"test-aad").unwrap();
166
167 assert_eq!(
168 url_unseal::<(u32, String)>(&sealed, &key, b"test-aad").unwrap(),
169 obj
170 );
171 }
172
173 #[test]
174 fn unseal_rejects_wrong_aad_and_key() {
175 assert!(url_unseal::<(u32, String)>(PINNED_SEALED, &pinned_key(), b"other-aad").is_err());
176 assert!(
177 url_unseal::<(u32, String)>(PINNED_SEALED, &[8u8; 32].into(), b"test-aad").is_err()
178 );
179 }
180
181 #[test]
182 fn unseal_rejects_envelope_shorter_than_nonce() {
183 let short = Base64Url::encode_string(&[0u8; 8]);
184
185 assert!(url_unseal::<(u32, String)>(short, &pinned_key(), b"").is_err());
186 }
187}