diff options
| author | Vlad Stepanov <8uk.8ak@gmail.com> | 2023-06-15 14:29:40 +0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-15 10:29:40 +0000 |
| commit | 4077c33adfdacaf0ed68657a1955a7b69a78d373 (patch) | |
| tree | 432d5c23992388a6c1bd4b11d41785ea00d56905 /atuin-client/src/encryption.rs | |
| parent | Add namespaces to kv store (#1052) (diff) | |
| download | atuin-4077c33adfdacaf0ed68657a1955a7b69a78d373.zip | |
Builder interface for History objects (#933)
* [feature] store env variables in History records
WIP: remove `HistoryWithoutDelete`, add some docstrings, tests
* Create History objects through builders.
Assure in compile-time that all required fields
are set for the given construction scenario
* (from #882) split Cmd::run into subfns
* Update `History` doc
* remove rmp-serde from history
* update warning
---------
Co-authored-by: Conrad Ludgate <conrad.ludgate@truelayer.com>
Diffstat (limited to 'atuin-client/src/encryption.rs')
| -rw-r--r-- | atuin-client/src/encryption.rs | 259 |
1 files changed, 215 insertions, 44 deletions
diff --git a/atuin-client/src/encryption.rs b/atuin-client/src/encryption.rs index 7ed640a5..a7aec0e7 100644 --- a/atuin-client/src/encryption.rs +++ b/atuin-client/src/encryption.rs @@ -11,8 +11,10 @@ use std::{io::prelude::*, path::PathBuf}; use base64::prelude::{Engine, BASE64_STANDARD}; -use eyre::{eyre, Context, Result}; +use chrono::{DateTime, Utc}; +use eyre::{bail, eyre, Context, Result}; use fs_err as fs; +use rmp::{decode::Bytes, Marker}; use serde::{Deserialize, Serialize}; pub use xsalsa20poly1305::Key; use xsalsa20poly1305::{ @@ -20,10 +22,7 @@ use xsalsa20poly1305::{ AeadInPlace, KeyInit, XSalsa20Poly1305, }; -use crate::{ - history::{History, HistoryWithoutDelete}, - settings::Settings, -}; +use crate::{history::History, settings::Settings}; #[derive(Debug, Serialize, Deserialize)] pub struct EncryptedHistory { @@ -75,7 +74,9 @@ pub fn load_encoded_key(settings: &Settings) -> Result<String> { } pub fn encode_key(key: &Key) -> Result<String> { - let buf = rmp_serde::to_vec(key.as_slice()).wrap_err("could not encode key to message pack")?; + let mut buf = vec![]; + rmp::encode::write_bin(&mut buf, key.as_slice()) + .wrap_err("could not encode key to message pack")?; let buf = BASE64_STANDARD.encode(buf); Ok(buf) @@ -86,23 +87,23 @@ pub fn decode_key(key: String) -> Result<Key> { .decode(key.trim_end()) .wrap_err("encryption key is not a valid base64 encoding")?; - let mbuf: Result<[u8; 32]> = - rmp_serde::from_slice(&buf).wrap_err("encryption key is not a valid message pack encoding"); - - match mbuf { - Ok(b) => Ok(*Key::from_slice(&b)), - Err(_) => { - let buf: &[u8] = rmp_serde::from_slice(&buf) - .wrap_err("encryption key is not a valid message pack encoding")?; - - Ok(*Key::from_slice(buf)) + // old code wrote the key as a fixed length array of 32 bytes + // new code writes the key with a length prefix + if buf.len() == 32 { + Ok(*Key::from_slice(&buf)) + } else { + let mut bytes = Bytes::new(&buf); + let key_len = rmp::decode::read_bin_len(&mut bytes).map_err(error_report)?; + if key_len != 32 || bytes.remaining_slice().len() != key_len as usize { + bail!("encryption key is not the correct size") } + Ok(*Key::from_slice(bytes.remaining_slice())) } } pub fn encrypt(history: &History, key: &Key) -> Result<EncryptedHistory> { // serialize with msgpack - let mut buf = rmp_serde::to_vec(history)?; + let mut buf = encode(history)?; let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng); XSalsa20Poly1305::new(key) @@ -125,25 +126,111 @@ pub fn decrypt(mut encrypted_history: EncryptedHistory, key: &Key) -> Result<His .map_err(|_| eyre!("could not encrypt"))?; let plaintext = encrypted_history.ciphertext; - let history = rmp_serde::from_slice(&plaintext); + let history = decode(&plaintext)?; + + Ok(history) +} + +fn encode(h: &History) -> Result<Vec<u8>> { + use rmp::encode; - let Ok(history) = history else { - let history: HistoryWithoutDelete = rmp_serde::from_slice(&plaintext)?; + let mut output = vec![]; + // INFO: ensure this is updated when adding new fields + encode::write_array_len(&mut output, 9)?; - return Ok(History { - id: history.id, - cwd: history.cwd, - exit: history.exit, - command: history.command, - session: history.session, - duration: history.duration, - hostname: history.hostname, - timestamp: history.timestamp, - deleted_at: None, - }); - }; + encode::write_str(&mut output, &h.id)?; + encode::write_str( + &mut output, + &dbg!(h + .timestamp + .to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)), + )?; + encode::write_sint(&mut output, h.duration)?; + encode::write_sint(&mut output, h.exit)?; + encode::write_str(&mut output, &h.command)?; + encode::write_str(&mut output, &h.cwd)?; + encode::write_str(&mut output, &h.session)?; + encode::write_str(&mut output, &h.hostname)?; + match h.deleted_at { + Some(d) => encode::write_str( + &mut output, + &d.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true), + )?, + None => encode::write_nil(&mut output)?, + } - Ok(history) + Ok(output) +} + +fn decode(bytes: &[u8]) -> Result<History> { + use rmp::decode::{self, DecodeStringError}; + + let mut bytes = Bytes::new(bytes); + + let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?; + if nfields < 8 { + bail!("malformed decrypted history") + } + if nfields > 9 { + bail!("cannot decrypt history from a newer version of atuin"); + } + + let bytes = bytes.remaining_slice(); + let (id, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?; + let (timestamp, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?; + + let mut bytes = Bytes::new(bytes); + let duration = decode::read_int(&mut bytes).map_err(error_report)?; + let exit = decode::read_int(&mut bytes).map_err(error_report)?; + + let bytes = bytes.remaining_slice(); + let (command, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?; + let (cwd, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?; + let (session, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?; + let (hostname, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?; + + // if we have more fields, try and get the deleted_at + let mut deleted_at = None; + let mut bytes = bytes; + if nfields > 8 { + bytes = match decode::read_str_from_slice(bytes) { + Ok((d, b)) => { + deleted_at = Some(d); + b + } + // we accept null here + Err(DecodeStringError::TypeMismatch(Marker::Null)) => { + // consume the null marker + let mut c = Bytes::new(bytes); + decode::read_nil(&mut c).map_err(error_report)?; + c.remaining_slice() + } + Err(err) => return Err(error_report(err)), + }; + } + + if !bytes.is_empty() { + bail!("trailing bytes in encoded history. malformed") + } + + Ok(History { + id: id.to_owned(), + timestamp: DateTime::parse_from_rfc3339(timestamp)?.with_timezone(&Utc), + duration, + exit, + command: command.to_owned(), + cwd: cwd.to_owned(), + session: session.to_owned(), + hostname: hostname.to_owned(), + deleted_at: deleted_at + .map(DateTime::parse_from_rfc3339) + .transpose()? + .map(|dt| dt.with_timezone(&Utc)), + }) +} + +fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report { + eyre!("{err:?}") } #[cfg(test)] @@ -152,23 +239,25 @@ mod test { use crate::history::History; - use super::{decrypt, encrypt}; + use super::{decode, decrypt, encode, encrypt}; #[test] fn test_encrypt_decrypt() { let key1 = XSalsa20Poly1305::generate_key(&mut OsRng); let key2 = XSalsa20Poly1305::generate_key(&mut OsRng); - let history = History::new( - chrono::Utc::now(), - "ls".to_string(), - "/home/ellie".to_string(), - 0, - 1, - Some("beep boop".to_string()), - Some("booop".to_string()), - None, - ); + let history = History::from_db() + .id("1".into()) + .timestamp(chrono::Utc::now()) + .command("ls".into()) + .cwd("/home/ellie".into()) + .exit(0) + .duration(1) + .session("beep boop".into()) + .hostname("booop".into()) + .deleted_at(None) + .build() + .into(); let e1 = encrypt(&history, &key1).unwrap(); let e2 = encrypt(&history, &key2).unwrap(); @@ -186,4 +275,86 @@ mod test { // this should err let _ = decrypt(e2, &key1).expect_err("expected an error decrypting with invalid key"); } + + #[test] + fn test_decode() { + let bytes = [ + 0x99, 0xD9, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55, 53, 51, 56, + 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 187, 50, 48, 50, 51, 45, + 48, 53, 45, 50, 56, 84, 49, 56, 58, 51, 53, 58, 52, 48, 46, 54, 51, 51, 56, 55, 50, 90, + 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116, 97, 116, 117, 115, 217, 42, + 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, + 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115, 47, 99, 111, 100, 101, 47, 97, + 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97, 51, 48, 54, 102, 50, 55, 52, 52, + 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49, 102, 57, 52, 53, 55, 187, 102, + 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58, 99, 111, 110, 114, 97, 100, 46, + 108, 117, 100, 103, 97, 116, 101, 192, + ]; + let history = History { + id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned(), + timestamp: "2023-05-28T18:35:40.633872Z".parse().unwrap(), + duration: 49206000, + exit: 0, + command: "git status".to_owned(), + cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), + session: "b97d9a306f274473a203d2eba41f9457".to_owned(), + hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), + deleted_at: None, + }; + + let h = decode(&bytes).unwrap(); + assert_eq!(history, h); + + let b = encode(&h).unwrap(); + assert_eq!(&bytes, &*b); + } + + #[test] + fn test_decode_deleted() { + let history = History { + id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned(), + timestamp: "2023-05-28T18:35:40.633872Z".parse().unwrap(), + duration: 49206000, + exit: 0, + command: "git status".to_owned(), + cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), + session: "b97d9a306f274473a203d2eba41f9457".to_owned(), + hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), + deleted_at: Some("2023-05-28T18:35:40.633872Z".parse().unwrap()), + }; + + let b = encode(&history).unwrap(); + let h = decode(&b).unwrap(); + assert_eq!(history, h); + } + + #[test] + fn test_decode_old() { + let bytes = [ + 0x98, 0xD9, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55, 53, 51, 56, + 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 187, 50, 48, 50, 51, 45, + 48, 53, 45, 50, 56, 84, 49, 56, 58, 51, 53, 58, 52, 48, 46, 54, 51, 51, 56, 55, 50, 90, + 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116, 97, 116, 117, 115, 217, 42, + 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, + 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115, 47, 99, 111, 100, 101, 47, 97, + 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97, 51, 48, 54, 102, 50, 55, 52, 52, + 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49, 102, 57, 52, 53, 55, 187, 102, + 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58, 99, 111, 110, 114, 97, 100, 46, + 108, 117, 100, 103, 97, 116, 101, + ]; + let history = History { + id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned(), + timestamp: "2023-05-28T18:35:40.633872Z".parse().unwrap(), + duration: 49206000, + exit: 0, + command: "git status".to_owned(), + cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), + session: "b97d9a306f274473a203d2eba41f9457".to_owned(), + hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), + deleted_at: None, + }; + + let h = decode(&bytes).unwrap(); + assert_eq!(history, h); + } } |
