mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-22 08:13:57 +01:00
use keyring for encryption key
This commit is contained in:
parent
6118da2ee2
commit
10dd02dd8f
683
Cargo.lock
generated
683
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@ sync = [
|
||||
"base64",
|
||||
"generic-array",
|
||||
"xsalsa20poly1305",
|
||||
"keyring",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@ -63,6 +64,7 @@ tokio = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
xsalsa20poly1305 = { version = "0.9.0", optional = true }
|
||||
generic-array = { version = "0.14", optional = true, features = ["serde"] }
|
||||
keyring = { version = "2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
@ -1,5 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use chrono::Utc;
|
||||
use eyre::{bail, Result};
|
||||
@ -13,22 +12,13 @@ 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::{history::History, sync::hash_str};
|
||||
|
||||
static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
// TODO: remove all references to the encryption key from this
|
||||
// It should be handled *elsewhere*
|
||||
|
||||
pub struct Client<'a> {
|
||||
sync_addr: &'a str,
|
||||
key: Key,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
@ -110,13 +100,12 @@ 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(sync_addr: &'a str, session_token: &'a str) -> Result<Self> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, format!("Token {session_token}").parse()?);
|
||||
|
||||
Ok(Client {
|
||||
sync_addr,
|
||||
key: decode_key(key)?,
|
||||
client: reqwest::Client::builder()
|
||||
.user_agent(APP_USER_AGENT)
|
||||
.default_headers(headers)
|
||||
@ -154,13 +143,12 @@ impl<'a> Client<'a> {
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub async fn get_history(
|
||||
pub async fn get_encrypted_history(
|
||||
&self,
|
||||
sync_ts: chrono::DateTime<Utc>,
|
||||
history_ts: chrono::DateTime<Utc>,
|
||||
host: Option<String>,
|
||||
deleted: HashSet<String>,
|
||||
) -> Result<Vec<History>> {
|
||||
) -> Result<Vec<String>> {
|
||||
let host = match host {
|
||||
None => hash_str(&format!("{}:{}", whoami::hostname(), whoami::username())),
|
||||
Some(h) => h,
|
||||
@ -177,23 +165,7 @@ 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("");
|
||||
}
|
||||
|
||||
h
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(history)
|
||||
Ok(history.history)
|
||||
}
|
||||
|
||||
pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> {
|
||||
|
@ -8,11 +8,10 @@
|
||||
// clients must share the secret in order to be able to sync, as it is needed
|
||||
// to decrypt
|
||||
|
||||
use std::{io::prelude::*, path::PathBuf};
|
||||
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::{
|
||||
@ -31,49 +30,57 @@ pub struct EncryptedHistory {
|
||||
pub nonce: Nonce<XSalsa20Poly1305>,
|
||||
}
|
||||
|
||||
pub fn new_key(settings: &Settings) -> Result<Key> {
|
||||
pub fn new_key(username: &str, settings: &Settings) -> Result<Key> {
|
||||
let entry = keyring::Entry::new("atuin", username).ok();
|
||||
new_key_inner(entry, settings)
|
||||
}
|
||||
|
||||
pub fn save_key(username: &str, settings: &Settings, key: &Key) -> Result<()> {
|
||||
let entry = keyring::Entry::new("atuin", username).ok();
|
||||
save_key_inner(entry, settings, key)
|
||||
}
|
||||
|
||||
fn new_key_inner(entry: Option<keyring::Entry>, settings: &Settings) -> Result<Key> {
|
||||
let key = XSalsa20Poly1305::generate_key(&mut OsRng);
|
||||
save_key_inner(entry, settings, &key)?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
fn save_key_inner(entry: Option<keyring::Entry>, settings: &Settings, key: &Key) -> Result<()> {
|
||||
let path = settings.key_path.as_str();
|
||||
|
||||
let key = XSalsa20Poly1305::generate_key(&mut OsRng);
|
||||
let encoded = encode_key(&key)?;
|
||||
let encoded = encode_key(key)?;
|
||||
|
||||
let mut file = fs::File::create(path)?;
|
||||
file.write_all(encoded.as_bytes())?;
|
||||
// prefer keyring
|
||||
if let Some(entry) = entry {
|
||||
entry.set_password(&encoded)?;
|
||||
}
|
||||
|
||||
Ok(key)
|
||||
// write to the file system too for now
|
||||
fs_err::write(path, encoded.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Loads the secret key, will create + save if it doesn't exist
|
||||
pub fn load_key(settings: &Settings) -> Result<Key> {
|
||||
pub fn load_key(username: &str, 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)?
|
||||
let entry = keyring::Entry::new("atuin", username).ok();
|
||||
// prefer the keyring
|
||||
let key = match entry.as_ref().map(|e| e.get_password()) {
|
||||
Some(Ok(key)) => decode_key(key)?,
|
||||
_ if Path::new(path).exists() => decode_key(fs_err::read_to_string(path)?)?,
|
||||
_ => {
|
||||
let key = XSalsa20Poly1305::generate_key(&mut OsRng);
|
||||
save_key_inner(entry, settings, &key)?;
|
||||
key
|
||||
}
|
||||
};
|
||||
|
||||
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 mut file = fs::File::create(path)?;
|
||||
file.write_all(encoded.as_bytes())?;
|
||||
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -3,14 +3,14 @@ use std::convert::TryInto;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use eyre::Result;
|
||||
use eyre::{Context, Result};
|
||||
|
||||
use atuin_common::api::AddHistoryRequest;
|
||||
|
||||
use crate::{
|
||||
api_client,
|
||||
database::Database,
|
||||
encryption::{encrypt, load_encoded_key, load_key},
|
||||
encryption::{decrypt, encrypt, load_key},
|
||||
settings::Settings,
|
||||
};
|
||||
|
||||
@ -33,17 +33,21 @@ pub fn hash_str(string: &str) -> String {
|
||||
// Check if remote has things we don't, and if so, download them.
|
||||
// Returns (num downloaded, total local)
|
||||
async fn sync_download(
|
||||
settings: &Settings,
|
||||
force: bool,
|
||||
client: &api_client::Client<'_>,
|
||||
db: &mut (impl Database + Send),
|
||||
) -> Result<(i64, i64)> {
|
||||
) -> Result<()> {
|
||||
debug!("starting sync download");
|
||||
|
||||
let remote_status = client.status().await?;
|
||||
let remote_count = remote_status.count;
|
||||
|
||||
let key = load_key(&remote_status.username, settings)?; // encryption key
|
||||
|
||||
// useful to ensure we don't even save something that hasn't yet been synced + deleted
|
||||
let remote_deleted = HashSet::from_iter(remote_status.deleted.clone());
|
||||
let remote_deleted =
|
||||
HashSet::<&str>::from_iter(remote_status.deleted.iter().map(|s| s.as_str()));
|
||||
|
||||
let initial_local = db.history_count().await?;
|
||||
let mut local_count = initial_local;
|
||||
@ -60,23 +64,30 @@ async fn sync_download(
|
||||
|
||||
while remote_count > local_count {
|
||||
let page = client
|
||||
.get_history(
|
||||
last_sync,
|
||||
last_timestamp,
|
||||
host.clone(),
|
||||
remote_deleted.clone(),
|
||||
)
|
||||
.get_encrypted_history(last_sync, last_timestamp, host.clone())
|
||||
.await?;
|
||||
|
||||
db.save_bulk(&page).await?;
|
||||
let mut history = Vec::with_capacity(page.len());
|
||||
for entry in page {
|
||||
let entry = serde_json::from_str(&entry).context("invalid base64")?;
|
||||
let mut entry =
|
||||
decrypt(entry, &key).context("failed to decrypt history! check your key")?;
|
||||
if remote_deleted.contains(&entry.id.as_str()) {
|
||||
entry.deleted_at = Some(chrono::Utc::now());
|
||||
entry.command = String::from("");
|
||||
}
|
||||
history.push(entry);
|
||||
}
|
||||
|
||||
db.save_bulk(&history).await?;
|
||||
|
||||
local_count = db.history_count().await?;
|
||||
|
||||
if page.len() < remote_status.page_size.try_into().unwrap() {
|
||||
if history.len() < remote_status.page_size.try_into().unwrap() {
|
||||
break;
|
||||
}
|
||||
|
||||
let page_last = page
|
||||
let page_last = history
|
||||
.last()
|
||||
.expect("could not get last element of page")
|
||||
.timestamp;
|
||||
@ -104,8 +115,8 @@ async fn sync_download(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((local_count - initial_local, local_count))
|
||||
debug!("sync downloaded {}", local_count - initial_local);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check if we have things remote doesn't, and if so, upload them
|
||||
@ -127,7 +138,7 @@ async fn sync_upload(
|
||||
|
||||
debug!("remote has {}, we have {}", remote_count, local_count);
|
||||
|
||||
let key = load_key(settings)?; // encryption key
|
||||
let key = load_key(&remote_status.username, settings)?; // encryption key
|
||||
|
||||
// first just try the most recent set
|
||||
|
||||
@ -178,17 +189,10 @@ 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_address, &settings.session_token)?;
|
||||
|
||||
sync_upload(settings, force, &client, db).await?;
|
||||
|
||||
let download = sync_download(force, &client, db).await?;
|
||||
|
||||
debug!("sync downloaded {}", download.0);
|
||||
sync_download(settings, force, &client, db).await?;
|
||||
|
||||
Settings::save_sync_time()?;
|
||||
|
||||
|
@ -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.sync_address, &settings.session_token)?;
|
||||
|
||||
client.delete().await?;
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
use std::{io, path::PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use eyre::{bail, Context, Result};
|
||||
use eyre::{bail, Result};
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
use atuin_client::{
|
||||
api_client,
|
||||
encryption::{decode_key, encode_key, new_key, Key},
|
||||
encryption::{decode_key, load_key, save_key, Key},
|
||||
settings::Settings,
|
||||
};
|
||||
use atuin_common::api::LoginRequest;
|
||||
@ -47,27 +47,24 @@ impl Cmd {
|
||||
let key = or_user_input(&self.key, "encryption key [blank to use existing key file]");
|
||||
let password = self.password.clone().unwrap_or_else(read_user_password);
|
||||
|
||||
let key_path = settings.key_path.as_str();
|
||||
if key.is_empty() {
|
||||
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() {
|
||||
bail!("the key in existing key file was invalid");
|
||||
}
|
||||
} else {
|
||||
println!("No key file exists, creating a new");
|
||||
let _key = new_key(settings)?;
|
||||
}
|
||||
load_key(&username, 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()))?,
|
||||
// from_slice panics if the wrong length
|
||||
Ok(mnemonic) if mnemonic.entropy().len() == 32 => {
|
||||
*Key::from_slice(mnemonic.entropy())
|
||||
}
|
||||
// if we parsed ok, it was a mnemonic, but it wasn't the full mnemonic
|
||||
Ok(_) => {
|
||||
bail!("key was not the correct length")
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(err) = err.downcast_ref::<bip39::ErrorKind>() {
|
||||
match err {
|
||||
// assume they copied in the base64 key
|
||||
bip39::ErrorKind::InvalidWord => key,
|
||||
bip39::ErrorKind::InvalidWord => decode_key(key.clone())?,
|
||||
bip39::ErrorKind::InvalidChecksum => {
|
||||
bail!("key mnemonic was not valid")
|
||||
}
|
||||
@ -79,17 +76,12 @@ impl Cmd {
|
||||
}
|
||||
} else {
|
||||
// unknown error. assume they copied the base64 key
|
||||
key
|
||||
decode_key(key.clone())?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if decode_key(key.clone()).is_err() {
|
||||
bail!("the specified key was invalid");
|
||||
}
|
||||
|
||||
let mut file = File::create(key_path).await?;
|
||||
file.write_all(key.as_bytes()).await?;
|
||||
save_key(&username, settings, &key)?;
|
||||
}
|
||||
|
||||
let session = api_client::login(
|
||||
|
@ -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::new_key(&username, settings)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -46,7 +46,10 @@ impl Cmd {
|
||||
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")?;
|
||||
// jank... we need the username to get the key, I guess we can store the username in a file at some point :D
|
||||
// for now this should still load the key from the fs. This is the only place it's needed, alternatively we can
|
||||
// require the user provide the username to this command. idk
|
||||
let key = load_key("", &settings).wrap_err("could not load encryption key")?;
|
||||
|
||||
if base64 {
|
||||
let encode = encode_key(&key).wrap_err("could not encode encryption key")?;
|
||||
|
@ -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.sync_address, &settings.session_token)?;
|
||||
|
||||
let status = client.status().await?;
|
||||
let last_sync = Settings::last_sync()?;
|
||||
|
Loading…
Reference in New Issue
Block a user