use std::{io, path::PathBuf}; use clap::Parser; use eyre::{bail, Context, ContextCompat, Result}; use tokio::{fs::File, io::AsyncWriteExt}; use atuin_client::{ api_client, encryption::{decode_key, encode_key, new_key, Key}, settings::Settings, }; use atuin_common::api::LoginRequest; use rpassword::prompt_password; #[derive(Parser)] pub struct Cmd { #[clap(long, short)] pub username: Option, #[clap(long, short)] pub password: Option, /// The encryption key for your account #[clap(long, short)] pub key: Option, } fn get_input() -> Result { let mut input = String::new(); io::stdin().read_line(&mut input)?; Ok(input.trim_end_matches(&['\r', '\n'][..]).to_string()) } impl Cmd { pub async fn run(&self, settings: &Settings) -> Result<()> { let session_path = atuin_common::utils::data_dir().join("session"); if session_path.exists() { println!( "You are already logged in! Please run 'atuin logout' if you wish to login again" ); return Ok(()); } let username = or_user_input(&self.username, "username"); 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)?; } } 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()) .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 } } }; 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?; } let session = api_client::login( settings.sync_address.as_str(), LoginRequest { username, password }, ) .await?; let session_path = settings.session_path.as_str(); let mut file = File::create(session_path).await?; file.write_all(session.session.as_bytes()).await?; println!("Logged in!"); Ok(()) } } pub(super) fn or_user_input(value: &'_ Option, name: &'static str) -> String { value.clone().unwrap_or_else(|| read_user_input(name)) } pub(super) fn read_user_password() -> String { let password = prompt_password("Please enter password: "); password.expect("Failed to read from input") } 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"); } }