diff --git a/Cargo.lock b/Cargo.lock index 5ebfd2f1d3..e773988f61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,7 @@ dependencies = [ "semver", "serde", "serde_json", + "tiny-bip39", "tokio", "tracing-subscriber", "unicode-segmentation", @@ -1418,6 +1419,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1681,6 +1691,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.36.5" @@ -2126,6 +2142,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -2185,6 +2213,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "tiny-bip39" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" +dependencies = [ + "anyhow", + "hmac", + "once_cell", + "pbkdf2", + "rand", + "rustc-hash", + "sha2", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2418,6 +2465,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -2746,3 +2799,24 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index 7cb0ad9872..227997ef84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ whoami = "1.1.2" rpassword = "7.0" semver = "1.0.14" runtime-format = "0.1.2" +tiny-bip39 = "1" # from tui bitflags = "1.3" diff --git a/README.md b/README.md index ccc2aa49c2..7d6e83de1b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- + diff --git a/atuin-client/src/database.rs b/atuin-client/src/database.rs index 5b817fa82a..d0e696c45b 100644 --- a/atuin-client/src/database.rs +++ b/atuin-client/src/database.rs @@ -448,8 +448,7 @@ impl Database for Sqlite { } else if let Some(term) = query_part.strip_prefix('\'') { format!("{glob}{term}{glob}") } else if is_inverse { - let term = query_part; - format!("{glob}{term}{glob}") + format!("{glob}{query_part}{glob}") } else { query_part.split("").join(glob) }; diff --git a/atuin-client/src/encryption.rs b/atuin-client/src/encryption.rs index 46cf1b2e42..bb017b749d 100644 --- a/atuin-client/src/encryption.rs +++ b/atuin-client/src/encryption.rs @@ -66,6 +66,7 @@ pub fn load_encoded_key(settings: &Settings) -> Result { } } +pub type Key = secretbox::Key; pub fn encode_key(key: secretbox::Key) -> Result { let buf = rmp_serde::to_vec(&key).wrap_err("could not encode key to message pack")?; let buf = base64::encode(buf); diff --git a/src/command/client/history.rs b/src/command/client/history.rs index 13d6616434..34b851d926 100644 --- a/src/command/client/history.rs +++ b/src/command/client/history.rs @@ -257,7 +257,7 @@ impl Cmd { } (Some(session), Some(cwd)) => { let query = format!( - "select * from history where cwd = '{cwd}' and session = '{session}';" + "select * from history where cwd = '{cwd}' and session = '{session}';", ); db.query_history(&query).await? } diff --git a/src/command/client/sync.rs b/src/command/client/sync.rs index b97a224068..f71bcc996f 100644 --- a/src/command/client/sync.rs +++ b/src/command/client/sync.rs @@ -27,7 +27,11 @@ pub enum Cmd { Register(register::Cmd), /// Print the encryption key for transfer to another machine - Key, + Key { + /// Switch to base64 output of the key + #[arg(long)] + base64: bool, + }, } impl Cmd { @@ -37,11 +41,18 @@ impl Cmd { Self::Login(l) => l.run(&settings).await, Self::Logout => logout::run(), Self::Register(r) => r.run(&settings).await, - Self::Key => { + Self::Key { base64 } => { use atuin_client::encryption::{encode_key, load_key}; let key = load_key(&settings).wrap_err("could not load encryption key")?; - let encode = encode_key(key).wrap_err("could not encode encryption key")?; - println!("{encode}"); + + if base64 { + let encode = encode_key(key).wrap_err("could not encode encryption key")?; + println!("{encode}"); + } else { + let mnemonic = bip39::Mnemonic::from_entropy(&key.0, bip39::Language::English) + .map_err(|_| eyre::eyre!("invalid key"))?; + println!("{mnemonic}"); + } Ok(()) } } diff --git a/src/command/client/sync/login.rs b/src/command/client/sync/login.rs index 038e822bc5..bd3a80297a 100644 --- a/src/command/client/sync/login.rs +++ b/src/command/client/sync/login.rs @@ -1,10 +1,14 @@ use std::io; use clap::Parser; -use eyre::Result; +use eyre::{bail, ContextCompat, Result}; use tokio::{fs::File, io::AsyncWriteExt}; -use atuin_client::{api_client, settings::Settings}; +use atuin_client::{ + api_client, + encryption::{encode_key, Key}, + settings::Settings, +}; use atuin_common::api::LoginRequest; use rpassword::prompt_password; @@ -54,6 +58,31 @@ impl Cmd { let key_path = settings.key_path.as_str(); let mut file = File::create(key_path).await?; + + // 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()).context("key was not the correct length")?, + )?, + Err(err) => { + if let Some(err) = err.downcast_ref::() { + match err { + // assume they copied in the base64 key + bip39::ErrorKind::InvalidWord => key, + bip39::ErrorKind::InvalidChecksum => bail!("key mnemonic was not valid"), + bip39::ErrorKind::InvalidKeysize(_) + | bip39::ErrorKind::InvalidWordLength(_) + | bip39::ErrorKind::InvalidEntropyLength(_, _) => { + bail!("key was not the correct length") + } + } + } else { + // unknown error. assume they copied the base64 key + key + } + } + }; + file.write_all(key.as_bytes()).await?; println!("Logged in!"); @@ -75,3 +104,24 @@ fn read_user_input(name: &'static str) -> String { eprint!("Please enter {name}: "); get_input().expect("Failed to read from input") } + +#[cfg(test)] +mod tests { + use atuin_client::encryption::Key; + + #[test] + fn mnemonic_round_trip() { + let key = Key { + 0: [ + 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3, 8, 4, 6, 2, 6, 4, 3, 3, 8, 3, + 2, 7, 9, 5, + ], + }; + let phrase = bip39::Mnemonic::from_entropy(&key.0, bip39::Language::English) + .unwrap() + .into_phrase(); + let mnemonic = bip39::Mnemonic::from_phrase(&phrase, bip39::Language::English).unwrap(); + assert_eq!(mnemonic.entropy(), &key.0); + assert_eq!(phrase, "adapt amused able anxiety mother adapt beef gaze amount else seat alcohol cage lottery avoid scare alcohol cactus school avoid coral adjust catch pink"); + } +}