This commit is contained in:
Conrad Ludgate 2023-05-23 17:22:04 +01:00
parent d21b691bcf
commit 5e2989cd35
No known key found for this signature in database
GPG Key ID: 197E3CACA1C980B5
15 changed files with 436 additions and 221 deletions

26
Cargo.lock generated
View File

@ -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"

View File

@ -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]

View File

@ -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<()> {

View File

@ -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");
}
}
}

View File

@ -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(

View File

@ -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?;

View File

@ -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)]

View File

@ -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

View File

@ -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))]

View File

@ -7,6 +7,7 @@ pub struct History {
pub user_id: i64,
pub hostname: String,
pub timestamp: NaiveDateTime,
pub scheme: Option<String>,
pub data: String,

View File

@ -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?;

View File

@ -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() {

View File

@ -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(())
}

View File

@ -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)

View File

@ -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()?;