1use atuin_common::record::{
2 AdditionalData, DecryptedData, EncryptedData, Encryption, HostId, RecordId, RecordIdx,
3};
4use base64::{Engine, engine::general_purpose};
5use eyre::{Context, Result, ensure};
6use rusty_paserk::{Key, KeyId, Local, PieWrappedKey};
7use rusty_paseto::core::{
8 ImplicitAssertion, Key as DataKey, Local as LocalPurpose, Paseto, PasetoNonce, Payload, V4,
9};
10use serde::{Deserialize, Serialize};
11
12#[allow(non_camel_case_types)]
14pub struct PASETO_V4;
15
16impl Encryption for PASETO_V4 {
55 fn re_encrypt(
56 mut data: EncryptedData,
57 _ad: AdditionalData,
58 old_key: &[u8; 32],
59 new_key: &[u8; 32],
60 ) -> Result<EncryptedData> {
61 let cek = Self::decrypt_cek(data.content_encryption_key, old_key)?;
62 data.content_encryption_key = Self::encrypt_cek(cek, new_key);
63 Ok(data)
64 }
65
66 fn encrypt(data: DecryptedData, ad: AdditionalData, key: &[u8; 32]) -> EncryptedData {
67 let random_key = Key::<V4, Local>::new_os_random();
70
71 let assertions = Assertions::from(ad).encode();
73
74 let payload = serde_json::to_string(&AtuinPayload {
76 data: general_purpose::URL_SAFE_NO_PAD.encode(data.0),
77 })
78 .expect("json encoding can't fail");
79 let nonce = DataKey::<32>::try_new_random().expect("could not source from random");
80 let nonce = PasetoNonce::<V4, LocalPurpose>::from(&nonce);
81
82 let token = Paseto::<V4, LocalPurpose>::builder()
83 .set_payload(Payload::from(payload.as_str()))
84 .set_implicit_assertion(ImplicitAssertion::from(assertions.as_str()))
85 .try_encrypt(&random_key.into(), &nonce)
86 .expect("error encrypting atuin data");
87
88 EncryptedData {
89 data: token,
90 content_encryption_key: Self::encrypt_cek(random_key, key),
91 }
92 }
93
94 fn decrypt(data: EncryptedData, ad: AdditionalData, key: &[u8; 32]) -> Result<DecryptedData> {
95 let token = data.data;
96 let cek = Self::decrypt_cek(data.content_encryption_key, key)?;
97
98 let assertions = Assertions::from(ad).encode();
100
101 let payload = Paseto::<V4, LocalPurpose>::try_decrypt(
103 &token,
104 &cek.into(),
105 None,
106 ImplicitAssertion::from(&*assertions),
107 )
108 .context("could not decrypt entry")?;
109
110 let payload: AtuinPayload = serde_json::from_str(&payload)?;
111 let data = general_purpose::URL_SAFE_NO_PAD.decode(payload.data)?;
112 Ok(DecryptedData(data))
113 }
114}
115
116impl PASETO_V4 {
117 fn decrypt_cek(wrapped_cek: String, key: &[u8; 32]) -> Result<Key<V4, Local>> {
118 let wrapping_key = Key::<V4, Local>::from_bytes(*key);
119
120 let AtuinFooter { kid, wpk } = serde_json::from_str(&wrapped_cek)
123 .context("wrapped cek did not contain the correct contents")?;
124
125 let current_kid = wrapping_key.to_id();
131
132 ensure!(
133 current_kid == kid,
134 "attempting to decrypt with incorrect key. currently using {current_kid}, expecting {kid}"
135 );
136
137 Ok(wpk.unwrap_key(&wrapping_key)?)
139 }
140
141 fn encrypt_cek(cek: Key<V4, Local>, key: &[u8; 32]) -> String {
142 let wrapping_key = Key::<V4, Local>::from_bytes(*key);
144
145 let wrapped_cek = AtuinFooter {
147 wpk: cek.wrap_pie(&wrapping_key),
148 kid: wrapping_key.to_id(),
149 };
150 serde_json::to_string(&wrapped_cek).expect("could not serialize wrapped cek")
151 }
152}
153
154#[derive(Serialize, Deserialize)]
155struct AtuinPayload {
156 data: String,
157}
158
159#[derive(Serialize, Deserialize)]
160struct AtuinFooter {
163 wpk: PieWrappedKey<V4, Local>,
165 kid: KeyId<V4, Local>,
167}
168
169#[derive(Debug, Copy, Clone, Serialize)]
172struct Assertions<'a> {
173 id: &'a RecordId,
174 idx: &'a RecordIdx,
175 version: &'a str,
176 tag: &'a str,
177 host: &'a HostId,
178}
179
180impl<'a> From<AdditionalData<'a>> for Assertions<'a> {
181 fn from(ad: AdditionalData<'a>) -> Self {
182 Self {
183 id: ad.id,
184 version: ad.version,
185 tag: ad.tag,
186 host: ad.host,
187 idx: ad.idx,
188 }
189 }
190}
191
192impl Assertions<'_> {
193 fn encode(&self) -> String {
194 serde_json::to_string(self).expect("could not serialize implicit assertions")
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use atuin_common::{
201 record::{Host, Record},
202 utils::uuid_v7,
203 };
204
205 use super::*;
206
207 #[test]
208 fn round_trip() {
209 let key = Key::<V4, Local>::new_os_random();
210
211 let ad = AdditionalData {
212 id: &RecordId(uuid_v7()),
213 version: "v0",
214 tag: "kv",
215 host: &HostId(uuid_v7()),
216 idx: &0,
217 };
218
219 let data = DecryptedData(vec![1, 2, 3, 4]);
220
221 let encrypted = PASETO_V4::encrypt(data.clone(), ad, &key.to_bytes());
222 let decrypted = PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap();
223 assert_eq!(decrypted, data);
224 }
225
226 #[test]
227 fn same_entry_different_output() {
228 let key = Key::<V4, Local>::new_os_random();
229
230 let ad = AdditionalData {
231 id: &RecordId(uuid_v7()),
232 version: "v0",
233 tag: "kv",
234 host: &HostId(uuid_v7()),
235 idx: &0,
236 };
237
238 let data = DecryptedData(vec![1, 2, 3, 4]);
239
240 let encrypted = PASETO_V4::encrypt(data.clone(), ad, &key.to_bytes());
241 let encrypted2 = PASETO_V4::encrypt(data, ad, &key.to_bytes());
242
243 assert_ne!(
244 encrypted.data, encrypted2.data,
245 "re-encrypting the same contents should have different output due to key randomization"
246 );
247 }
248
249 #[test]
250 fn cannot_decrypt_different_key() {
251 let key = Key::<V4, Local>::new_os_random();
252 let fake_key = Key::<V4, Local>::new_os_random();
253
254 let ad = AdditionalData {
255 id: &RecordId(uuid_v7()),
256 version: "v0",
257 tag: "kv",
258 host: &HostId(uuid_v7()),
259 idx: &0,
260 };
261
262 let data = DecryptedData(vec![1, 2, 3, 4]);
263
264 let encrypted = PASETO_V4::encrypt(data, ad, &key.to_bytes());
265 let _ = PASETO_V4::decrypt(encrypted, ad, &fake_key.to_bytes()).unwrap_err();
266 }
267
268 #[test]
269 fn cannot_decrypt_different_id() {
270 let key = Key::<V4, Local>::new_os_random();
271
272 let ad = AdditionalData {
273 id: &RecordId(uuid_v7()),
274 version: "v0",
275 tag: "kv",
276 host: &HostId(uuid_v7()),
277 idx: &0,
278 };
279
280 let data = DecryptedData(vec![1, 2, 3, 4]);
281
282 let encrypted = PASETO_V4::encrypt(data, ad, &key.to_bytes());
283
284 let ad = AdditionalData {
285 id: &RecordId(uuid_v7()),
286 ..ad
287 };
288 let _ = PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap_err();
289 }
290
291 #[test]
292 fn re_encrypt_round_trip() {
293 let key1 = Key::<V4, Local>::new_os_random();
294 let key2 = Key::<V4, Local>::new_os_random();
295
296 let ad = AdditionalData {
297 id: &RecordId(uuid_v7()),
298 version: "v0",
299 tag: "kv",
300 host: &HostId(uuid_v7()),
301 idx: &0,
302 };
303
304 let data = DecryptedData(vec![1, 2, 3, 4]);
305
306 let encrypted1 = PASETO_V4::encrypt(data.clone(), ad, &key1.to_bytes());
307 let encrypted2 =
308 PASETO_V4::re_encrypt(encrypted1.clone(), ad, &key1.to_bytes(), &key2.to_bytes())
309 .unwrap();
310
311 assert_eq!(encrypted1.data, encrypted2.data);
313 assert_ne!(
314 encrypted1.content_encryption_key,
315 encrypted2.content_encryption_key
316 );
317
318 let decrypted = PASETO_V4::decrypt(encrypted2, ad, &key2.to_bytes()).unwrap();
319
320 assert_eq!(decrypted, data);
321 }
322
323 #[test]
324 fn full_record_round_trip() {
325 let key = [0x55; 32];
326 let record = Record::builder()
327 .id(RecordId(uuid_v7()))
328 .version("v0".to_owned())
329 .tag("kv".to_owned())
330 .host(Host::new(HostId(uuid_v7())))
331 .timestamp(1687244806000000)
332 .data(DecryptedData(vec![1, 2, 3, 4]))
333 .idx(0)
334 .build();
335
336 let encrypted = record.encrypt::<PASETO_V4>(&key);
337
338 assert!(!encrypted.data.data.is_empty());
339 assert!(!encrypted.data.content_encryption_key.is_empty());
340
341 let decrypted = encrypted.decrypt::<PASETO_V4>(&key).unwrap();
342
343 assert_eq!(decrypted.data.0, [1, 2, 3, 4]);
344 }
345
346 #[test]
347 fn full_record_round_trip_fail() {
348 let key = [0x55; 32];
349 let record = Record::builder()
350 .id(RecordId(uuid_v7()))
351 .version("v0".to_owned())
352 .tag("kv".to_owned())
353 .host(Host::new(HostId(uuid_v7())))
354 .timestamp(1687244806000000)
355 .data(DecryptedData(vec![1, 2, 3, 4]))
356 .idx(0)
357 .build();
358
359 let encrypted = record.encrypt::<PASETO_V4>(&key);
360
361 let mut enc1 = encrypted.clone();
362 enc1.host = Host::new(HostId(uuid_v7()));
363 let _ = enc1
364 .decrypt::<PASETO_V4>(&key)
365 .expect_err("tampering with the host should result in auth failure");
366
367 let mut enc2 = encrypted;
368 enc2.id = RecordId(uuid_v7());
369 let _ = enc2
370 .decrypt::<PASETO_V4>(&key)
371 .expect_err("tampering with the id should result in auth failure");
372 }
373}