use std::{io, path::PathBuf}; use clap::Parser; use eyre::{Context, Result, bail}; use tokio::{fs::File, io::AsyncWriteExt}; use crate::atuin_client::{ auth::{self, AuthResponse}, encryption::{decode_key, load_key}, record::sqlite_store::SqliteStore, record::store::Store, record::sync::{self, SyncError}, settings::{Settings, SyncAuth}, }; use rpassword::prompt_password; #[derive(Parser, Debug)] pub(crate) struct Cmd { #[clap(long, short)] pub(crate) username: Option, #[clap(long, short)] pub(crate) password: Option, /// The encryption key for your account #[clap(long, short)] pub(crate) key: Option, /// The two-factor authentication code for your account, if any #[clap(long, short)] pub(crate) totp_code: Option, #[clap(long, hide = true)] pub(crate) from_registration: bool, } 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(crate) async fn run(&self, settings: &Settings, store: &SqliteStore) -> Result<()> { match settings.resolve_sync_auth().await { SyncAuth::Legacy { .. } => { println!("You are logged in to your sync server."); println!("Run 'atuin logout' to log out."); return Ok(()); } SyncAuth::NotLoggedIn { .. } => {} } self.run_legacy_login(settings, store).await?; verify_key_against_remote(settings).await } /// Legacy login: always prompt for username/password interactively /// (or accept them via flags). async fn run_legacy_login(&self, settings: &Settings, store: &SqliteStore) -> Result<()> { let username = or_user_input(self.username.clone(), "username"); let password = self.password.clone().unwrap_or_else(read_user_password); self.prompt_and_store_key(settings, store).await?; let client = auth::auth_client(settings).await; let response = client.login(&username, &password).await?; match response { AuthResponse::Success { session, .. } => { Settings::meta_store().await?.save_session(&session).await?; } AuthResponse::TwoFactorRequired => { // Legacy server doesn't support 2FA, so this shouldn't happen. bail!("unexpected two-factor requirement from legacy server"); } } println!("Logged in!"); Ok(()) } async fn prompt_and_store_key(&self, settings: &Settings, store: &SqliteStore) -> Result<()> { let key_path = settings.key_path.as_str(); let key_path = PathBuf::from(key_path); println!("IMPORTANT"); println!( "If you are already logged in on another machine, you must ensure that the key you use here is the same as the key you used there." ); println!("You can find your key by running 'atuin key' on the other machine."); println!("Do not share this key with anyone."); println!("\nRead more here: https://docs.atuin.sh/guide/sync/#login \n"); let key = or_user_input( self.key.clone(), "encryption key [blank to use existing key file]", ); if key.is_empty() { if key_path.exists() { let bytes = fs_err::read_to_string(&key_path).context(format!( "Existing key file at '{}' could not be read", key_path.to_string_lossy() ))?; if decode_key(bytes).is_err() { bail!(format!( "The key in existing key file at '{}' is invalid", key_path.to_string_lossy() )); } } else { panic!( "No key provided and no existing key file found. Please use 'atuin key' on your other machine, or recover your key from a backup" ) } } else if !key_path.exists() { if decode_key(key.clone()).is_err() { bail!("The specified key is invalid"); } let mut file = File::create(&key_path).await?; file.write_all(key.as_bytes()).await?; } else { // we now know that the user has logged in specifying a key, AND that the key path // exists // 1. check if the saved key and the provided key match. if so, nothing to do. // 2. if not, re-encrypt the local history and overwrite the key let current_key: [u8; 32] = load_key(settings)?.into(); let encoded = key.clone(); // gonna want to save it in a bit let new_key: [u8; 32] = decode_key(key) .context("Could not decode provided key; is not valid base64-encoded key")? .into(); if new_key != current_key { println!("\nRe-encrypting local store with new key"); store.re_encrypt(¤t_key, &new_key).await?; println!("Writing new key"); let mut file = File::create(&key_path).await?; file.write_all(encoded.as_bytes()).await?; } } Ok(()) } } async fn verify_key_against_remote(settings: &Settings) -> Result<()> { let key: [u8; 32] = load_key(settings) .context("could not load encryption key for verification")? .into(); let client = sync::build_client(settings).await?; let remote_index = match client.record_status().await { Ok(idx) => idx, Err(e) => { tracing::warn!("could not fetch remote status to verify key: {e}"); return Ok(()); } }; match sync::check_encryption_key(&client, &remote_index, &key).await { Ok(()) => Ok(()), Err(SyncError::WrongKey) => { // Roll back the saved session so the user is not left in a // half-authenticated state with a key that can't read the data. if let Ok(meta) = Settings::meta_store().await { let _ = meta.delete_session().await; } crate::print_error::print_error( "Wrong encryption key", "The encryption key on this machine does not match the data on the server. \ You have been logged out.\n\n\ To fix this, find your existing key by running `atuin key` on a machine that \ already syncs successfully, then run `atuin login` again here with that key.", ); std::process::exit(1); } Err(e) => { // Non-key error (e.g. transient network issue). Don't fail the // login — the user is authenticated and can sync later when the // network recovers. tracing::warn!("could not verify encryption key against remote: {e}"); Ok(()) } } } pub(super) fn or_user_input(value: Option, name: &'static str) -> String { value.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") }