summaryrefslogtreecommitdiffstats
path: root/src/local/encryption.rs
blob: 3c1699e350b5d50563ea97bd1ff043de9d15bb63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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(_) => {}
        };
    }
}