// The general idea is that we NEVER send cleartext history to the server // This way the odds of anything private ending up where it should not are // very low // The server authenticates via the usual username and password. This has // nothing to do with the encryption, and is purely authentication! The client // generates its own secret key, and encrypts all shell history with libsodium's // secretbox. The data is then sent to the server, where it is stored. All // clients must share the secret in order to be able to sync, as it is needed // to decrypt use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; use eyre::{eyre, Result}; use sodiumoxide::crypto::secretbox; use crate::history::History; use crate::settings::Settings; #[derive(Debug, Serialize, Deserialize)] pub struct EncryptedHistory { pub ciphertext: Vec, pub nonce: secretbox::Nonce, } pub fn new_key(settings: &Settings) -> Result { let path = settings.key_path.as_str(); let key = secretbox::gen_key(); let encoded = encode_key(key.clone())?; let mut file = File::create(path)?; file.write_all(encoded.as_bytes())?; Ok(key) } // Loads the secret key, will create + save if it doesn't exist pub fn load_key(settings: &Settings) -> Result { let path = settings.key_path.as_str(); let key = if PathBuf::from(path).exists() { let key = std::fs::read_to_string(path)?; decode_key(key)? } else { new_key(settings)? }; Ok(key) } pub fn load_encoded_key(settings: &Settings) -> Result { let path = settings.key_path.as_str(); if PathBuf::from(path).exists() { let key = std::fs::read_to_string(path)?; Ok(key) } else { let key = secretbox::gen_key(); let encoded = encode_key(key)?; let mut file = File::create(path)?; file.write_all(encoded.as_bytes())?; Ok(encoded) } } pub fn encode_key(key: secretbox::Key) -> Result { let buf = rmp_serde::to_vec(&key)?; let buf = base64::encode(buf); Ok(buf) } pub fn decode_key(key: String) -> Result { let buf = base64::decode(key)?; let buf: secretbox::Key = rmp_serde::from_read_ref(&buf)?; Ok(buf) } pub fn encrypt(history: &History, key: &secretbox::Key) -> Result { // serialize with msgpack let buf = rmp_serde::to_vec(history)?; let nonce = secretbox::gen_nonce(); let ciphertext = secretbox::seal(&buf, &nonce, key); Ok(EncryptedHistory { ciphertext, nonce }) } pub fn decrypt(encrypted_history: &EncryptedHistory, key: &secretbox::Key) -> Result { let plaintext = secretbox::open(&encrypted_history.ciphertext, &encrypted_history.nonce, key) .map_err(|_| eyre!("failed to open secretbox - invalid key?"))?; let history = rmp_serde::from_read_ref(&plaintext)?; Ok(history) } #[cfg(test)] mod test { use sodiumoxide::crypto::secretbox; use crate::history::History; use super::{decrypt, encrypt}; #[test] fn test_encrypt_decrypt() { let key1 = secretbox::gen_key(); let key2 = secretbox::gen_key(); 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()), ); let e1 = encrypt(&history, &key1).unwrap(); let e2 = encrypt(&history, &key2).unwrap(); assert_ne!(e1.ciphertext, e2.ciphertext); assert_ne!(e1.nonce, e2.nonce); // test decryption works // this should pass match decrypt(&e1, &key1) { Err(e) => assert!(false, "failed to decrypt, got {}", e), Ok(h) => assert_eq!(h, history), }; // this should err match decrypt(&e2, &key1) { Ok(_) => assert!(false, "expected an error decrypting with invalid key"), Err(_) => {} }; } }