From 5611bc59f584a48ffa3a4adfb8837f470cfe8c22 Mon Sep 17 00:00:00 2001 From: Conrad Ludgate Date: Mon, 6 Feb 2023 19:22:58 +0000 Subject: [PATCH] display mnemonic key (#694) --- Cargo.lock | 74 ++++++++++++++++++++++++++++++++ Cargo.toml | 1 + atuin-client/src/encryption.rs | 1 + src/command/client/sync.rs | 19 ++++++-- src/command/client/sync/login.rs | 54 ++++++++++++++++++++++- 5 files changed, 143 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88b2ac2fe91..eb9081dd641 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,7 @@ dependencies = [ "serde", "serde_json", "termion", + "tiny-bip39", "tokio", "tracing-subscriber", "tui", @@ -1385,6 +1386,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" @@ -1657,6 +1667,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" @@ -2081,6 +2097,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" @@ -2152,6 +2180,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" @@ -2398,6 +2445,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" @@ -2726,3 +2779,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 21ea881d237..574dc55e0e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ whoami = "1.1.2" rpassword = "7.0" semver = "1.0.14" runtime-format = "0.1.2" +tiny-bip39 = "1" [dependencies.tracing-subscriber] version = "0.3" diff --git a/atuin-client/src/encryption.rs b/atuin-client/src/encryption.rs index 46cf1b2e42b..bb017b749d5 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/sync.rs b/src/command/client/sync.rs index b97a2240680..f71bcc996f7 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 038e822bc54..bd3a80297a3 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"); + } +}