mirror of
https://github.com/atuinsh/atuin.git
synced 2025-02-21 12:52:23 +01:00
chacha
This commit is contained in:
parent
d21b691bcf
commit
5e2989cd35
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -136,6 +136,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"atuin-common",
|
||||
"base64 0.21.0",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
@ -144,6 +145,7 @@ dependencies = [
|
||||
"fs-err",
|
||||
"generic-array",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"interim",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
@ -350,6 +352,30 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20poly1305"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"cipher",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.22"
|
||||
|
@ -22,6 +22,8 @@ sync = [
|
||||
"base64",
|
||||
"generic-array",
|
||||
"xsalsa20poly1305",
|
||||
"chacha20poly1305",
|
||||
"hkdf",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@ -62,6 +64,8 @@ base64 = { workspace = true, optional = true }
|
||||
tokio = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
xsalsa20poly1305 = { version = "0.9.0", optional = true }
|
||||
chacha20poly1305 = { version = "0.10.1", optional = true }
|
||||
hkdf = { version = "0.12.3", optional = true }
|
||||
generic-array = { version = "0.14", optional = true, features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use atuin_common::api::EncryptionScheme;
|
||||
use chrono::Utc;
|
||||
use eyre::{bail, Result};
|
||||
use reqwest::{
|
||||
@ -13,13 +14,10 @@ use atuin_common::api::{
|
||||
LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse,
|
||||
};
|
||||
use semver::Version;
|
||||
use xsalsa20poly1305::Key;
|
||||
|
||||
use crate::{
|
||||
encryption::{decode_key, decrypt},
|
||||
history::History,
|
||||
sync::hash_str,
|
||||
};
|
||||
use crate::encryption::{key, xchacha20poly1305, xsalsa20poly1305legacy};
|
||||
use crate::settings::Settings;
|
||||
use crate::{history::History, sync::hash_str};
|
||||
|
||||
static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
@ -28,7 +26,7 @@ static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
pub struct Client<'a> {
|
||||
sync_addr: &'a str,
|
||||
key: Key,
|
||||
key: key::Key,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
@ -110,13 +108,16 @@ pub async fn latest_version() -> Result<Version> {
|
||||
}
|
||||
|
||||
impl<'a> Client<'a> {
|
||||
pub fn new(sync_addr: &'a str, session_token: &'a str, key: String) -> Result<Self> {
|
||||
pub fn new(settings: &'a Settings) -> Result<Self> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, format!("Token {session_token}").parse()?);
|
||||
headers.insert(
|
||||
AUTHORIZATION,
|
||||
format!("Token {}", settings.session_token).parse()?,
|
||||
);
|
||||
|
||||
Ok(Client {
|
||||
sync_addr,
|
||||
key: decode_key(key)?,
|
||||
sync_addr: &settings.sync_address,
|
||||
key: key::load(settings)?,
|
||||
client: reqwest::Client::builder()
|
||||
.user_agent(APP_USER_AGENT)
|
||||
.default_headers(headers)
|
||||
@ -177,23 +178,28 @@ impl<'a> Client<'a> {
|
||||
let resp = self.client.get(url).send().await?;
|
||||
|
||||
let history = resp.json::<SyncHistoryResponse>().await?;
|
||||
let history = history
|
||||
.history
|
||||
.iter()
|
||||
// TODO: handle deletion earlier in this chain
|
||||
.map(|h| serde_json::from_str(h).expect("invalid base64"))
|
||||
.map(|h| decrypt(h, &self.key).expect("failed to decrypt history! check your key"))
|
||||
.map(|mut h| {
|
||||
if deleted.contains(&h.id) {
|
||||
h.deleted_at = Some(chrono::Utc::now());
|
||||
h.command = String::from("");
|
||||
|
||||
let mut output = Vec::with_capacity(history.history.len());
|
||||
for entry in history.more_history {
|
||||
let mut history = match entry.scheme {
|
||||
Some(EncryptionScheme::XSalsa20Poly1305Legacy) | None => {
|
||||
xsalsa20poly1305legacy::decrypt(entry.data, &self.key)?
|
||||
}
|
||||
Some(EncryptionScheme::XChaCha20Poly1305) => {
|
||||
xchacha20poly1305::decrypt(entry.data, &self.key, &entry.id)?
|
||||
}
|
||||
Some(EncryptionScheme::Unknown(x)) => {
|
||||
bail!("cannot decrypt '{x}' encryption scheme")
|
||||
}
|
||||
};
|
||||
if deleted.contains(&entry.id) {
|
||||
history.deleted_at = Some(Utc::now());
|
||||
history.command.clear();
|
||||
}
|
||||
output.push(history);
|
||||
}
|
||||
|
||||
h
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(history)
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> {
|
||||
|
@ -8,130 +8,273 @@
|
||||
// clients must share the secret in order to be able to sync, as it is needed
|
||||
// to decrypt
|
||||
|
||||
use std::{io::prelude::*, path::PathBuf};
|
||||
pub mod key {
|
||||
use std::path::Path;
|
||||
|
||||
use base64::prelude::{Engine, BASE64_STANDARD};
|
||||
use eyre::{eyre, Context, Result};
|
||||
use fs_err as fs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use xsalsa20poly1305::Key;
|
||||
use xsalsa20poly1305::{
|
||||
aead::{Nonce, OsRng},
|
||||
AeadInPlace, KeyInit, XSalsa20Poly1305,
|
||||
};
|
||||
use base64::prelude::{Engine, BASE64_STANDARD};
|
||||
use eyre::{Context, Result};
|
||||
pub use xsalsa20poly1305::Key;
|
||||
use xsalsa20poly1305::{aead::OsRng, KeyInit, XSalsa20Poly1305};
|
||||
|
||||
use crate::{
|
||||
history::{History, HistoryWithoutDelete},
|
||||
settings::Settings,
|
||||
};
|
||||
use crate::settings::Settings;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EncryptedHistory {
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub nonce: Nonce<XSalsa20Poly1305>,
|
||||
}
|
||||
pub fn new(settings: &Settings) -> Result<Key> {
|
||||
let path = settings.key_path.as_str();
|
||||
|
||||
pub fn new_key(settings: &Settings) -> Result<Key> {
|
||||
let path = settings.key_path.as_str();
|
||||
|
||||
let key = XSalsa20Poly1305::generate_key(&mut OsRng);
|
||||
let encoded = encode_key(&key)?;
|
||||
|
||||
let mut file = fs::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<Key> {
|
||||
let path = settings.key_path.as_str();
|
||||
|
||||
let key = if PathBuf::from(path).exists() {
|
||||
let key = fs_err::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 = fs::read_to_string(path)?;
|
||||
Ok(key)
|
||||
} else {
|
||||
let key = XSalsa20Poly1305::generate_key(&mut OsRng);
|
||||
let encoded = encode_key(&key)?;
|
||||
let encoded = encode(&key)?;
|
||||
|
||||
let mut file = fs::File::create(path)?;
|
||||
file.write_all(encoded.as_bytes())?;
|
||||
fs_err::write(path, encoded.as_bytes())?;
|
||||
|
||||
Ok(encoded)
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
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 buf = BASE64_STANDARD.encode(buf);
|
||||
// Loads the secret key, will create + save if it doesn't exist
|
||||
pub fn load(settings: &Settings) -> Result<Key> {
|
||||
let path = settings.key_path.as_str();
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
let key = if Path::new(path).exists() {
|
||||
let key = fs_err::read_to_string(path)?;
|
||||
decode(key)?
|
||||
} else {
|
||||
new(settings)?
|
||||
};
|
||||
|
||||
pub fn decode_key(key: String) -> Result<Key> {
|
||||
let buf = BASE64_STANDARD
|
||||
.decode(key.trim_end())
|
||||
.wrap_err("encryption key is not a valid base64 encoding")?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
let mbuf: Result<[u8; 32]> =
|
||||
rmp_serde::from_slice(&buf).wrap_err("encryption key is not a valid message pack encoding");
|
||||
pub fn encode(key: &Key) -> Result<String> {
|
||||
let buf =
|
||||
rmp_serde::to_vec(key.as_slice()).wrap_err("could not encode key to message pack")?;
|
||||
let buf = BASE64_STANDARD.encode(buf);
|
||||
|
||||
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(buf)
|
||||
}
|
||||
|
||||
Ok(*Key::from_slice(buf))
|
||||
pub fn decode(key: String) -> Result<Key> {
|
||||
let buf = BASE64_STANDARD
|
||||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(history: &History, key: &Key) -> Result<EncryptedHistory> {
|
||||
// serialize with msgpack
|
||||
let mut buf = rmp_serde::to_vec(history)?;
|
||||
// DO NOT MODIFY. We can't change old encryption schemes, only add new ones.
|
||||
pub mod xsalsa20poly1305legacy {
|
||||
use chrono::Utc;
|
||||
use eyre::{eyre, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xsalsa20poly1305::{
|
||||
aead::{Nonce, OsRng},
|
||||
AeadInPlace, Key, KeyInit, XSalsa20Poly1305,
|
||||
};
|
||||
|
||||
let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng);
|
||||
XSalsa20Poly1305::new(key)
|
||||
.encrypt_in_place(&nonce, &[], &mut buf)
|
||||
.map_err(|_| eyre!("could not encrypt"))?;
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct EncryptedHistory {
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub nonce: Nonce<XSalsa20Poly1305>,
|
||||
}
|
||||
|
||||
Ok(EncryptedHistory {
|
||||
ciphertext: buf,
|
||||
nonce,
|
||||
})
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct HistoryWithoutDelete {
|
||||
pub id: String,
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
pub duration: i64,
|
||||
pub exit: i64,
|
||||
pub command: String,
|
||||
pub cwd: String,
|
||||
pub session: String,
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
use crate::history::History;
|
||||
|
||||
pub fn encrypt(history: &History, key: &Key) -> Result<String> {
|
||||
// serialize with msgpack
|
||||
let mut buf = rmp_serde::to_vec(history)?;
|
||||
|
||||
let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng);
|
||||
XSalsa20Poly1305::new(key)
|
||||
.encrypt_in_place(&nonce, &[], &mut buf)
|
||||
.map_err(|_| eyre!("could not encrypt"))?;
|
||||
|
||||
let record = serde_json::to_string(&EncryptedHistory {
|
||||
ciphertext: buf,
|
||||
nonce,
|
||||
})?;
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
pub fn decrypt(encrypted_history: String, key: &Key) -> Result<History> {
|
||||
let mut decoded: EncryptedHistory = serde_json::from_str(&encrypted_history)?;
|
||||
|
||||
XSalsa20Poly1305::new(key)
|
||||
.decrypt_in_place(&decoded.nonce, &[], &mut decoded.ciphertext)
|
||||
.map_err(|_| eyre!("could not decrypt"))?;
|
||||
let plaintext = decoded.ciphertext;
|
||||
|
||||
let history = rmp_serde::from_slice(&plaintext);
|
||||
|
||||
// ugly hack because we broke things
|
||||
let Ok(history) = history else {
|
||||
// fallback to without deleted_at
|
||||
let history: HistoryWithoutDelete = rmp_serde::from_slice(&plaintext)?;
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use xsalsa20poly1305::{aead::OsRng, KeyInit, XSalsa20Poly1305};
|
||||
|
||||
use crate::history::History;
|
||||
|
||||
use super::{decrypt, 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 e1 = encrypt(&history, &key1).unwrap();
|
||||
let e2 = encrypt(&history, &key2).unwrap();
|
||||
|
||||
assert_ne!(e1, e2);
|
||||
|
||||
// test decryption works
|
||||
// this should pass
|
||||
match decrypt(e1, &key1) {
|
||||
Err(e) => panic!("failed to decrypt, got {}", e),
|
||||
Ok(h) => assert_eq!(h, history),
|
||||
};
|
||||
|
||||
// this should err
|
||||
let _ = decrypt(e2, &key1).expect_err("expected an error decrypting with invalid key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt(mut encrypted_history: EncryptedHistory, key: &Key) -> Result<History> {
|
||||
XSalsa20Poly1305::new(key)
|
||||
.decrypt_in_place(
|
||||
&encrypted_history.nonce,
|
||||
&[],
|
||||
&mut encrypted_history.ciphertext,
|
||||
)
|
||||
.map_err(|_| eyre!("could not encrypt"))?;
|
||||
let plaintext = encrypted_history.ciphertext;
|
||||
// DO NOT MODIFY. We can't change old encryption schemes, only add new ones.
|
||||
pub mod xchacha20poly1305 {
|
||||
use super::key::Key;
|
||||
use chacha20poly1305::{
|
||||
aead::{Nonce, OsRng},
|
||||
AeadCore, AeadInPlace, KeyInit, XChaCha20Poly1305,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use eyre::{eyre, Result};
|
||||
use hkdf::Hkdf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
let history = rmp_serde::from_slice(&plaintext);
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct HistoryPlaintext {
|
||||
pub duration: i64,
|
||||
pub exit: i64,
|
||||
pub command: String,
|
||||
pub cwd: String,
|
||||
pub session: String,
|
||||
pub hostname: String,
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct HistoryAdditionalData {
|
||||
pub id: String,
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct EncryptedHistory {
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub nonce: Nonce<XChaCha20Poly1305>,
|
||||
}
|
||||
|
||||
let Ok(history) = history else {
|
||||
let history: HistoryWithoutDelete = rmp_serde::from_slice(&plaintext)?;
|
||||
use crate::history::History;
|
||||
|
||||
return Ok(History {
|
||||
id: history.id,
|
||||
fn content_key(key: &Key, id: &str) -> Result<chacha20poly1305::Key> {
|
||||
let mut content_key = chacha20poly1305::Key::default();
|
||||
Hkdf::<Sha256>::new(Some(b"history"), key)
|
||||
.expand(id.as_bytes(), &mut content_key)
|
||||
.map_err(|_| eyre!("could not derive encryption key"))?;
|
||||
Ok(content_key)
|
||||
}
|
||||
|
||||
pub fn encrypt(history: &History, key: &Key) -> Result<String> {
|
||||
// a unique encryption key for this entry
|
||||
let content_key = content_key(key, &history.id)?;
|
||||
|
||||
let mut plaintext = rmp_serde::to_vec(&HistoryPlaintext {
|
||||
duration: history.duration,
|
||||
exit: history.exit,
|
||||
command: history.command.to_owned(),
|
||||
cwd: history.cwd.to_owned(),
|
||||
session: history.session.to_owned(),
|
||||
hostname: history.hostname.to_owned(),
|
||||
timestamp: history.timestamp,
|
||||
})?;
|
||||
|
||||
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||
XChaCha20Poly1305::new(&content_key)
|
||||
.encrypt_in_place(&nonce, history.id.as_bytes(), &mut plaintext)
|
||||
.map_err(|_| eyre!("could not encrypt"))?;
|
||||
|
||||
let record = serde_json::to_string(&EncryptedHistory {
|
||||
ciphertext: plaintext,
|
||||
nonce,
|
||||
})?;
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
pub fn decrypt(encrypted_history: String, key: &Key, id: &str) -> Result<History> {
|
||||
let content_key = content_key(key, id)?;
|
||||
|
||||
let mut decoded: EncryptedHistory = serde_json::from_str(&encrypted_history)?;
|
||||
|
||||
XChaCha20Poly1305::new(&content_key)
|
||||
.decrypt_in_place(&decoded.nonce, id.as_bytes(), &mut decoded.ciphertext)
|
||||
.map_err(|_| eyre!("could not decrypt"))?;
|
||||
let plaintext = decoded.ciphertext;
|
||||
|
||||
let history: HistoryPlaintext = rmp_serde::from_slice(&plaintext)?;
|
||||
|
||||
Ok(History {
|
||||
id: id.to_owned(),
|
||||
cwd: history.cwd,
|
||||
exit: history.exit,
|
||||
command: history.command,
|
||||
@ -140,50 +283,48 @@ pub fn decrypt(mut encrypted_history: EncryptedHistory, key: &Key) -> Result<His
|
||||
hostname: history.hostname,
|
||||
timestamp: history.timestamp,
|
||||
deleted_at: None,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
Ok(history)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use xsalsa20poly1305::{aead::OsRng, KeyInit, XSalsa20Poly1305};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use xsalsa20poly1305::{aead::OsRng, KeyInit, XSalsa20Poly1305};
|
||||
use crate::history::History;
|
||||
|
||||
use crate::history::History;
|
||||
use super::{decrypt, encrypt};
|
||||
|
||||
use super::{decrypt, encrypt};
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let key1 = XSalsa20Poly1305::generate_key(&mut OsRng);
|
||||
let key2 = XSalsa20Poly1305::generate_key(&mut OsRng);
|
||||
|
||||
#[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::new(
|
||||
chrono::Utc::now(),
|
||||
"ls".to_string(),
|
||||
"/home/ellie".to_string(),
|
||||
0,
|
||||
1,
|
||||
Some("beep boop".to_string()),
|
||||
Some("booop".to_string()),
|
||||
None,
|
||||
);
|
||||
let e1 = encrypt(&history, &key1).unwrap();
|
||||
let e2 = encrypt(&history, &key2).unwrap();
|
||||
|
||||
let e1 = encrypt(&history, &key1).unwrap();
|
||||
let e2 = encrypt(&history, &key2).unwrap();
|
||||
assert_ne!(e1, e2);
|
||||
|
||||
assert_ne!(e1.ciphertext, e2.ciphertext);
|
||||
assert_ne!(e1.nonce, e2.nonce);
|
||||
// test decryption works
|
||||
// this should pass
|
||||
match decrypt(e1, &key1, &history.id) {
|
||||
Err(e) => panic!("failed to decrypt, got {}", e),
|
||||
Ok(h) => assert_eq!(h, history),
|
||||
};
|
||||
|
||||
// test decryption works
|
||||
// this should pass
|
||||
match decrypt(e1, &key1) {
|
||||
Err(e) => panic!("failed to decrypt, got {}", e),
|
||||
Ok(h) => assert_eq!(h, history),
|
||||
};
|
||||
|
||||
// this should err
|
||||
let _ = decrypt(e2, &key1).expect_err("expected an error decrypting with invalid key");
|
||||
// this should err
|
||||
let _ = decrypt(e2, &key1, &history.id)
|
||||
.expect_err("expected an error decrypting with invalid key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,21 +19,6 @@ pub struct History {
|
||||
pub deleted_at: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
// Forgive me, for I have sinned
|
||||
// I need to replace rmp with something that is more backwards-compatible.
|
||||
// Protobuf, or maybe just json
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, sqlx::FromRow)]
|
||||
pub struct HistoryWithoutDelete {
|
||||
pub id: String,
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
pub duration: i64,
|
||||
pub exit: i64,
|
||||
pub command: String,
|
||||
pub cwd: String,
|
||||
pub session: String,
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
impl History {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
|
@ -5,12 +5,12 @@ use std::iter::FromIterator;
|
||||
use chrono::prelude::*;
|
||||
use eyre::Result;
|
||||
|
||||
use atuin_common::api::AddHistoryRequest;
|
||||
use atuin_common::api::{AddHistoryRequest, EncryptionScheme};
|
||||
|
||||
use crate::{
|
||||
api_client,
|
||||
database::Database,
|
||||
encryption::{encrypt, load_encoded_key, load_key},
|
||||
encryption::{key, xsalsa20poly1305legacy},
|
||||
settings::Settings,
|
||||
};
|
||||
|
||||
@ -127,7 +127,7 @@ async fn sync_upload(
|
||||
|
||||
debug!("remote has {}, we have {}", remote_count, local_count);
|
||||
|
||||
let key = load_key(settings)?; // encryption key
|
||||
let key = key::load(settings)?; // encryption key
|
||||
|
||||
// first just try the most recent set
|
||||
|
||||
@ -142,7 +142,7 @@ async fn sync_upload(
|
||||
}
|
||||
|
||||
for i in last {
|
||||
let data = encrypt(&i, &key)?;
|
||||
let data = xsalsa20poly1305legacy::encrypt(&i, &key)?;
|
||||
let data = serde_json::to_string(&data)?;
|
||||
|
||||
let add_hist = AddHistoryRequest {
|
||||
@ -150,6 +150,7 @@ async fn sync_upload(
|
||||
timestamp: i.timestamp,
|
||||
data,
|
||||
hostname: hash_str(&i.hostname),
|
||||
scheme: Some(EncryptionScheme::XSalsa20Poly1305Legacy),
|
||||
};
|
||||
|
||||
buffer.push(add_hist);
|
||||
@ -178,11 +179,7 @@ async fn sync_upload(
|
||||
}
|
||||
|
||||
pub async fn sync(settings: &Settings, force: bool, db: &mut (impl Database + Send)) -> Result<()> {
|
||||
let client = api_client::Client::new(
|
||||
&settings.sync_address,
|
||||
&settings.session_token,
|
||||
load_encoded_key(settings)?,
|
||||
)?;
|
||||
let client = api_client::Client::new(settings)?;
|
||||
|
||||
sync_upload(settings, force, &client, db).await?;
|
||||
|
||||
|
@ -39,6 +39,56 @@ pub struct AddHistoryRequest {
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
pub data: String,
|
||||
pub hostname: String,
|
||||
// the encryption scheme used
|
||||
pub scheme: Option<EncryptionScheme>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EncryptionScheme {
|
||||
/// Encryption scheme using xsalsa20poly1305 (tweetnacl crypto_box) using the legacy system
|
||||
/// with no additional data.
|
||||
XSalsa20Poly1305Legacy,
|
||||
|
||||
/// Encryption scheme using xchacha20poly1305. Entry host+id+timestamp are saved in the additional data.
|
||||
/// The key is derived from the original using the ID as info and "shell-history"+padded zeros as the salt
|
||||
XChaCha20Poly1305,
|
||||
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl EncryptionScheme {
|
||||
pub fn to_str(&self) -> &str {
|
||||
match self {
|
||||
EncryptionScheme::XSalsa20Poly1305Legacy => "XSalsa20Poly1305Legacy",
|
||||
EncryptionScheme::XChaCha20Poly1305 => "XChaCha20Poly1305",
|
||||
EncryptionScheme::Unknown(x) => x,
|
||||
}
|
||||
}
|
||||
pub fn from_string(s: String) -> Self {
|
||||
match &*s {
|
||||
"XSalsa20Poly1305Legacy" => EncryptionScheme::XSalsa20Poly1305Legacy,
|
||||
"XChaCha20Poly1305" => EncryptionScheme::XChaCha20Poly1305,
|
||||
_ => EncryptionScheme::Unknown(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for EncryptionScheme {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.to_str().serialize(serializer)
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for EncryptionScheme {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(Self::from_string(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@ -56,6 +106,7 @@ pub struct SyncHistoryRequest {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SyncHistoryResponse {
|
||||
pub history: Vec<String>,
|
||||
pub more_history: Vec<AddHistoryRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -274,7 +274,7 @@ impl Database for Postgres {
|
||||
page_size: i64,
|
||||
) -> Result<Vec<History>> {
|
||||
let res = sqlx::query_as::<_, History>(
|
||||
"select id, client_id, user_id, hostname, timestamp, data, created_at from history
|
||||
"select id, client_id, user_id, hostname, timestamp, data, created_at, scheme from history
|
||||
where user_id = $1
|
||||
and hostname != $2
|
||||
and created_at >= $3
|
||||
|
@ -5,6 +5,7 @@ use axum::{
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use http::StatusCode;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
@ -60,7 +61,7 @@ pub async fn list<DB: Database>(
|
||||
100
|
||||
};
|
||||
|
||||
let history = db
|
||||
let entries = db
|
||||
.list_history(
|
||||
&user,
|
||||
req.sync_ts.naive_utc(),
|
||||
@ -78,16 +79,26 @@ pub async fn list<DB: Database>(
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = history {
|
||||
error!("failed to load history: {}", e);
|
||||
return Err(ErrorResponse::reply("failed to load history")
|
||||
.with_status(StatusCode::INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
let entries = match entries {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
error!("failed to load history: {}", e);
|
||||
return Err(ErrorResponse::reply("failed to load history")
|
||||
.with_status(StatusCode::INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
};
|
||||
|
||||
let history: Vec<String> = history
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|i| i.data.to_string())
|
||||
let history: Vec<String> = entries.iter().map(|i| i.data.clone()).collect();
|
||||
|
||||
let more_history: Vec<AddHistoryRequest> = entries
|
||||
.into_iter()
|
||||
.map(|i| AddHistoryRequest {
|
||||
id: i.client_id,
|
||||
timestamp: Utc.from_utc_datetime(&i.timestamp),
|
||||
data: i.data,
|
||||
hostname: i.hostname,
|
||||
scheme: i.scheme.map(EncryptionScheme::from_string),
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!(
|
||||
@ -96,7 +107,10 @@ pub async fn list<DB: Database>(
|
||||
user.id
|
||||
);
|
||||
|
||||
Ok(Json(SyncHistoryResponse { history }))
|
||||
Ok(Json(SyncHistoryResponse {
|
||||
history,
|
||||
more_history,
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(user.id = user.id))]
|
||||
|
@ -7,6 +7,7 @@ pub struct History {
|
||||
pub user_id: i64,
|
||||
pub hostname: String,
|
||||
pub timestamp: NaiveDateTime,
|
||||
pub scheme: Option<String>,
|
||||
|
||||
pub data: String,
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use atuin_client::{api_client, encryption::load_encoded_key, settings::Settings};
|
||||
use atuin_client::{api_client, settings::Settings};
|
||||
use eyre::{bail, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@ -9,11 +9,7 @@ pub async fn run(settings: &Settings) -> Result<()> {
|
||||
bail!("You are not logged in");
|
||||
}
|
||||
|
||||
let client = api_client::Client::new(
|
||||
&settings.sync_address,
|
||||
&settings.session_token,
|
||||
load_encoded_key(settings)?,
|
||||
)?;
|
||||
let client = api_client::Client::new(settings)?;
|
||||
|
||||
client.delete().await?;
|
||||
|
||||
|
@ -6,7 +6,7 @@ use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
use atuin_client::{
|
||||
api_client,
|
||||
encryption::{decode_key, encode_key, new_key, Key},
|
||||
encryption::key::{decode, encode, new, Key},
|
||||
settings::Settings,
|
||||
};
|
||||
use atuin_common::api::LoginRequest;
|
||||
@ -52,17 +52,17 @@ impl Cmd {
|
||||
if PathBuf::from(key_path).exists() {
|
||||
let bytes = fs_err::read_to_string(key_path)
|
||||
.context("existing key file couldn't be read")?;
|
||||
if decode_key(bytes).is_err() {
|
||||
if decode(bytes).is_err() {
|
||||
bail!("the key in existing key file was invalid");
|
||||
}
|
||||
} else {
|
||||
println!("No key file exists, creating a new");
|
||||
let _key = new_key(settings)?;
|
||||
let _key = new(settings)?;
|
||||
}
|
||||
} else {
|
||||
// try parse the key as a mnemonic...
|
||||
let key = match bip39::Mnemonic::from_phrase(&key, bip39::Language::English) {
|
||||
Ok(mnemonic) => encode_key(Key::from_slice(mnemonic.entropy()))?,
|
||||
Ok(mnemonic) => encode(Key::from_slice(mnemonic.entropy()))?,
|
||||
Err(err) => {
|
||||
if let Some(err) = err.downcast_ref::<bip39::ErrorKind>() {
|
||||
match err {
|
||||
@ -84,7 +84,7 @@ impl Cmd {
|
||||
}
|
||||
};
|
||||
|
||||
if decode_key(key.clone()).is_err() {
|
||||
if decode(key.clone()).is_err() {
|
||||
bail!("the specified key was invalid");
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ fn read_user_input(name: &'static str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use atuin_client::encryption::Key;
|
||||
use atuin_client::encryption::key::Key;
|
||||
|
||||
#[test]
|
||||
fn mnemonic_round_trip() {
|
||||
|
@ -43,7 +43,7 @@ pub async fn run(
|
||||
file.write_all(session.session.as_bytes()).await?;
|
||||
|
||||
// Create a new key, and save it to disk
|
||||
let _key = atuin_client::encryption::new_key(settings)?;
|
||||
let _key = atuin_client::encryption::key::new(settings)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -45,11 +45,11 @@ impl Cmd {
|
||||
Self::Register(r) => r.run(&settings).await,
|
||||
Self::Status => status::run(&settings, db).await,
|
||||
Self::Key { base64 } => {
|
||||
use atuin_client::encryption::{encode_key, load_key};
|
||||
let key = load_key(&settings).wrap_err("could not load encryption key")?;
|
||||
use atuin_client::encryption::key;
|
||||
let key = key::load(&settings).wrap_err("could not load encryption key")?;
|
||||
|
||||
if base64 {
|
||||
let encode = encode_key(&key).wrap_err("could not encode encryption key")?;
|
||||
let encode = key::encode(&key).wrap_err("could not encode encryption key")?;
|
||||
println!("{encode}");
|
||||
} else {
|
||||
let mnemonic = bip39::Mnemonic::from_entropy(&key, bip39::Language::English)
|
||||
|
@ -1,15 +1,9 @@
|
||||
use atuin_client::{
|
||||
api_client, database::Database, encryption::load_encoded_key, settings::Settings,
|
||||
};
|
||||
use atuin_client::{api_client, database::Database, settings::Settings};
|
||||
use colored::Colorize;
|
||||
use eyre::Result;
|
||||
|
||||
pub async fn run(settings: &Settings, db: &impl Database) -> Result<()> {
|
||||
let client = api_client::Client::new(
|
||||
&settings.sync_address,
|
||||
&settings.session_token,
|
||||
load_encoded_key(settings)?,
|
||||
)?;
|
||||
let client = api_client::Client::new(settings)?;
|
||||
|
||||
let status = client.status().await?;
|
||||
let last_sync = Settings::last_sync()?;
|
||||
|
Loading…
Reference in New Issue
Block a user