diff --git a/Cargo.lock b/Cargo.lock index c9c29b9..432943f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1797,6 +1797,7 @@ dependencies = [ "tempdir", "termion", "text_io", + "thiserror", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index aaba523..2e1daea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ dirs = "3.0.2" ring = "0.16.20" aes = "0.7.4" block-modes = "0.8.1" +thiserror = "1.0.26" [dev-dependencies] tempdir = "0.3" diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 5a2354c..2d7bacc 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -10,6 +10,7 @@ use std::io::{BufReader, Read, Write}; use std::path::Path; use std::sync::{Arc, Mutex}; use steamguard::SteamGuardAccount; +use thiserror::Error; type Aes256Cbc = Cbc; @@ -91,7 +92,10 @@ impl Manifest { return Ok(manifest); } - pub fn load_accounts(&mut self, passkey: Option<&str>) -> anyhow::Result<()> { + pub fn load_accounts( + &mut self, + passkey: &Option, + ) -> anyhow::Result<(), ManifestAccountLoadError> { for entry in &mut self.entries { let path = Path::new(&self.folder).join(&entry.filename); debug!("loading account: {:?}", path); @@ -112,14 +116,17 @@ impl Manifest { let mut buffer = vec![0xffu8; 16 * size]; buffer[..ciphertext.len()].copy_from_slice(&ciphertext); let decrypted = cipher.decrypt(&mut buffer)?; - let mut padded = &decrypted[..ciphertext.len()]; // This padding doesn't make any sense. - let unpadded = Pkcs7::unpad(&mut padded).unwrap(); + // This padding doesn't make any sense. + let mut padded = &decrypted[..ciphertext.len()]; + // Also, UnpadError does not implement Error for some fucking reason, so we have to do this. + let unpadded = Pkcs7::unpad(&mut padded) + .map_err(|_| ManifestAccountLoadError::DecryptionFailed)?; let s = std::str::from_utf8(&unpadded).unwrap(); account = serde_json::from_str(&s)?; } (None, Some(_)) => { - bail!("maFiles are encrypted, but no passkey was provided."); + return Err(ManifestAccountLoadError::MissingPasskey); } (_, None) => { account = serde_json::from_reader(reader)?; @@ -166,7 +173,7 @@ impl Manifest { self.entries.remove(index); } - pub fn save(&self, passkey: Option<&str>) -> anyhow::Result<()> { + pub fn save(&self, passkey: &Option) -> anyhow::Result<()> { ensure!( self.entries.len() == self.accounts.len(), "Manifest entries don't match accounts." @@ -252,6 +259,41 @@ impl EntryEncryptionParams { } } +#[derive(Debug, Error)] +pub enum ManifestAccountLoadError { + #[error("Manifest accounts are encrypted, but no passkey was provided.")] + MissingPasskey, + #[error("Failed to decrypt account.")] + #[from(block_modes::block_padding::UnpadError)] + DecryptionFailed, + #[error("Failed to deserialize the account.")] + DeserializationFailed(#[from] serde_json::Error), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +/// For some reason, these errors do not get converted to `ManifestAccountLoadError`s, even though they get converted into `anyhow::Error` just fine. I am too lazy to figure out why right now. +impl From for ManifestAccountLoadError { + fn from(error: block_modes::BlockModeError) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for ManifestAccountLoadError { + fn from(error: base64::DecodeError) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for ManifestAccountLoadError { + fn from(error: block_modes::InvalidKeyIvLength) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for ManifestAccountLoadError { + fn from(error: std::io::Error) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} + #[cfg(test)] mod tests { use super::*; @@ -262,7 +304,7 @@ mod tests { let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let manifest_path = tmp_dir.path().join("manifest.json"); let manifest = Manifest::new(manifest_path.as_path()); - assert!(matches!(manifest.save(None), Ok(_))); + assert!(matches!(manifest.save(&None), Ok(_))); } #[test] @@ -275,12 +317,12 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(None), Ok(_))); + assert!(matches!(manifest.save(&None), Ok(_))); let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); - assert!(matches!(loaded_manifest.load_accounts(None), Ok(_))); + assert!(matches!(loaded_manifest.load_accounts(&None), Ok(_))); assert_eq!( loaded_manifest.entries.len(), loaded_manifest.accounts.len() @@ -301,7 +343,7 @@ mod tests { #[test] fn test_should_save_and_load_manifest_encrypted() { - let passkey: Option<&str> = Some("password"); + let passkey: Option = Some("password".into()); let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let manifest_path = tmp_dir.path().join("manifest.json"); let mut manifest = Manifest::new(manifest_path.as_path()); @@ -311,12 +353,12 @@ mod tests { account.shared_secret = "secret".into(); manifest.add_account(account); manifest.entries[0].encryption = Some(EntryEncryptionParams::generate()); - assert!(matches!(manifest.save(passkey), Ok(_))); + assert!(matches!(manifest.save(&passkey), Ok(_))); let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); - assert!(matches!(loaded_manifest.load_accounts(passkey), Ok(_))); + assert!(matches!(loaded_manifest.load_accounts(&passkey), Ok(_))); assert_eq!( loaded_manifest.entries.len(), loaded_manifest.accounts.len() @@ -345,7 +387,7 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(None), Ok(_))); + assert!(matches!(manifest.save(&None), Ok(_))); std::fs::remove_file(&manifest_path).unwrap(); let mut loaded_manifest = Manifest::new(manifest_path.as_path()); @@ -386,7 +428,7 @@ mod tests { assert!(matches!(result, Ok(_))); let mut manifest = result.unwrap(); assert!(matches!(manifest.entries.last().unwrap().encryption, None)); - assert!(matches!(manifest.load_accounts(None), Ok(_))); + assert!(matches!(manifest.load_accounts(&None), Ok(_))); assert_eq!( manifest.entries.last().unwrap().account_name, manifest @@ -410,7 +452,7 @@ mod tests { manifest.entries.last().unwrap().encryption, Some(_) )); - let result = manifest.load_accounts(Some("password")); + let result = manifest.load_accounts(&Some("password".into())); assert!( matches!(result, Ok(_)), "error when loading accounts: {:?}", diff --git a/src/main.rs b/src/main.rs index 427809a..1dbeae0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,11 +182,24 @@ fn main() { } } - let passkey = matches.value_of("passkey"); + let mut passkey: Option = matches.value_of("passkey").map(|s| s.into()); - manifest - .load_accounts(passkey) - .expect("Failed to load accounts in manifest"); + loop { + match manifest.load_accounts(&passkey) { + Ok(_) => break, + Err( + accountmanager::ManifestAccountLoadError::MissingPasskey + | accountmanager::ManifestAccountLoadError::DecryptionFailed, + ) => { + error!("Incorrect passkey"); + passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); + } + Err(e) => { + error!("Could not load accounts: {}", e); + return; + } + } + } if matches.is_present("setup") { println!("Log in to the account that you want to link to steamguard-cli"); @@ -227,7 +240,7 @@ fn main() { } } manifest.add_account(account); - match manifest.save(passkey) { + match manifest.save(&passkey) { Ok(_) => {} Err(err) => { error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); @@ -272,7 +285,7 @@ fn main() { } println!("Authenticator finalized."); - match manifest.save(None) { + match manifest.save(&None) { Ok(_) => {} Err(err) => { println!( @@ -296,13 +309,13 @@ fn main() { } } - manifest.save(passkey).expect("Failed to save manifest."); + manifest.save(&passkey).expect("Failed to save manifest."); return; } else if matches.is_present("encrypt") { for entry in &mut manifest.entries { entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); } - manifest.save(passkey).expect("Failed to save manifest."); + manifest.save(&passkey).expect("Failed to save manifest."); return; } @@ -379,9 +392,7 @@ fn main() { } } - manifest - .save(passkey) - .expect("Failed to save manifest"); + manifest.save(&passkey).expect("Failed to save manifest"); } else if let Some(_) = matches.subcommand_matches("remove") { println!( "This will remove the mobile authenticator from {} accounts: {}", @@ -429,9 +440,7 @@ fn main() { manifest.remove_account(account_name); } - manifest - .save(passkey) - .expect("Failed to save manifest."); + manifest.save(&passkey).expect("Failed to save manifest."); } else { let server_time = steamapi::get_server_time(); for account in selected_accounts {