mirror of
https://github.com/atuinsh/atuin.git
synced 2024-12-03 21:59:58 +01:00
e43e5ce74a
Also: - Ensures that a key is generated as soon as a user registers! - Ensures that "atuin key" will generate a key if needed, and doesn't double base64 encode data And a few other little fixes :) Resolves #85 Resolves #86
147 lines
4.0 KiB
Rust
147 lines
4.0 KiB
Rust
// 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<u8>,
|
|
pub nonce: secretbox::Nonce,
|
|
}
|
|
|
|
pub fn new_key(settings: &Settings) -> Result<secretbox::Key> {
|
|
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<secretbox::Key> {
|
|
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<String> {
|
|
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<String> {
|
|
let buf = rmp_serde::to_vec(&key)?;
|
|
let buf = base64::encode(buf);
|
|
|
|
Ok(buf)
|
|
}
|
|
|
|
pub fn decode_key(key: String) -> Result<secretbox::Key> {
|
|
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<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::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(_) => {}
|
|
};
|
|
}
|
|
}
|