aboutsummaryrefslogtreecommitdiffstats
path: root/src/local/encryption.rs
diff options
context:
space:
mode:
authorEllie Huxtable <e@elm.sh>2021-04-13 19:14:07 +0100
committerGitHub <noreply@github.com>2021-04-13 19:14:07 +0100
commit5751463942cc91f1f1ffaf6e2ac633d7a0085f25 (patch)
treef7b5b9a4702c4c3ef29aa60d36612f61ffeae052 /src/local/encryption.rs
parentUpdate config (diff)
downloadatuin-5751463942cc91f1f1ffaf6e2ac633d7a0085f25.zip
Add history sync, resolves #13 (#31)
* Add encryption * Add login and register command * Add count endpoint * Write initial sync push * Add single sync command Confirmed working for one client only * Automatically sync on a configurable frequency * Add key command, key arg to login * Only load session if it exists * Use sync and history timestamps for download * Bind other key code Seems like some systems have this code for up arrow? I'm not sure why, and it's not an easy one to google. * Simplify upload * Try and fix download sync loop * Change sync order to avoid uploading what we just downloaded * Multiline import fix * Fix time parsing * Fix importing history with no time * Add hostname to sync * Use hostname to filter sync * Fixes * Add binding * Stuff from yesterday * Set cursor modes * Make clippy happy * Bump version
Diffstat (limited to 'src/local/encryption.rs')
-rw-r--r--src/local/encryption.rs108
1 files changed, 108 insertions, 0 deletions
diff --git a/src/local/encryption.rs b/src/local/encryption.rs
new file mode 100644
index 00000000..3c1699e3
--- /dev/null
+++ b/src/local/encryption.rs
@@ -0,0 +1,108 @@
+// 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::local::history::History;
+use crate::settings::Settings;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct EncryptedHistory {
+ pub ciphertext: Vec<u8>,
+ pub nonce: secretbox::Nonce,
+}
+
+// Loads the secret key, will create + save if it doesn't exist
+pub fn load_key(settings: &Settings) -> Result<secretbox::Key> {
+ let path = settings.local.key_path.as_str();
+
+ if PathBuf::from(path).exists() {
+ let bytes = std::fs::read(path)?;
+ let key: secretbox::Key = rmp_serde::from_read_ref(&bytes)?;
+ Ok(key)
+ } else {
+ let key = secretbox::gen_key();
+ let buf = rmp_serde::to_vec(&key)?;
+
+ let mut file = File::create(path)?;
+ file.write_all(&buf)?;
+
+ Ok(key)
+ }
+}
+
+pub fn encrypt(history: &History, key: &secretbox::Key) -> Result<EncryptedHistory> {
+ // 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<History> {
+ 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::local::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(_) => {}
+ };
+ }
+}