prompt for passkey if not provided

This commit is contained in:
Carson McManus 2021-08-16 21:13:58 -04:00
parent 1b1f12f423
commit 531e69ea88
4 changed files with 81 additions and 28 deletions

1
Cargo.lock generated
View file

@ -1797,6 +1797,7 @@ dependencies = [
"tempdir", "tempdir",
"termion", "termion",
"text_io", "text_io",
"thiserror",
"uuid", "uuid",
] ]

View file

@ -37,6 +37,7 @@ dirs = "3.0.2"
ring = "0.16.20" ring = "0.16.20"
aes = "0.7.4" aes = "0.7.4"
block-modes = "0.8.1" block-modes = "0.8.1"
thiserror = "1.0.26"
[dev-dependencies] [dev-dependencies]
tempdir = "0.3" tempdir = "0.3"

View file

@ -10,6 +10,7 @@ use std::io::{BufReader, Read, Write};
use std::path::Path; use std::path::Path;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use steamguard::SteamGuardAccount; use steamguard::SteamGuardAccount;
use thiserror::Error;
type Aes256Cbc = Cbc<Aes256, NoPadding>; type Aes256Cbc = Cbc<Aes256, NoPadding>;
@ -91,7 +92,10 @@ impl Manifest {
return Ok(manifest); return Ok(manifest);
} }
pub fn load_accounts(&mut self, passkey: Option<&str>) -> anyhow::Result<()> { pub fn load_accounts(
&mut self,
passkey: &Option<String>,
) -> anyhow::Result<(), ManifestAccountLoadError> {
for entry in &mut self.entries { for entry in &mut self.entries {
let path = Path::new(&self.folder).join(&entry.filename); let path = Path::new(&self.folder).join(&entry.filename);
debug!("loading account: {:?}", path); debug!("loading account: {:?}", path);
@ -112,14 +116,17 @@ impl Manifest {
let mut buffer = vec![0xffu8; 16 * size]; let mut buffer = vec![0xffu8; 16 * size];
buffer[..ciphertext.len()].copy_from_slice(&ciphertext); buffer[..ciphertext.len()].copy_from_slice(&ciphertext);
let decrypted = cipher.decrypt(&mut buffer)?; let decrypted = cipher.decrypt(&mut buffer)?;
let mut padded = &decrypted[..ciphertext.len()]; // This padding doesn't make any sense. // This padding doesn't make any sense.
let unpadded = Pkcs7::unpad(&mut padded).unwrap(); 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(); let s = std::str::from_utf8(&unpadded).unwrap();
account = serde_json::from_str(&s)?; account = serde_json::from_str(&s)?;
} }
(None, Some(_)) => { (None, Some(_)) => {
bail!("maFiles are encrypted, but no passkey was provided."); return Err(ManifestAccountLoadError::MissingPasskey);
} }
(_, None) => { (_, None) => {
account = serde_json::from_reader(reader)?; account = serde_json::from_reader(reader)?;
@ -166,7 +173,7 @@ impl Manifest {
self.entries.remove(index); self.entries.remove(index);
} }
pub fn save(&self, passkey: Option<&str>) -> anyhow::Result<()> { pub fn save(&self, passkey: &Option<String>) -> anyhow::Result<()> {
ensure!( ensure!(
self.entries.len() == self.accounts.len(), self.entries.len() == self.accounts.len(),
"Manifest entries don't match accounts." "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<block_modes::BlockModeError> for ManifestAccountLoadError {
fn from(error: block_modes::BlockModeError) -> Self {
return Self::Unknown(anyhow::Error::from(error));
}
}
impl From<base64::DecodeError> for ManifestAccountLoadError {
fn from(error: base64::DecodeError) -> Self {
return Self::Unknown(anyhow::Error::from(error));
}
}
impl From<block_modes::InvalidKeyIvLength> for ManifestAccountLoadError {
fn from(error: block_modes::InvalidKeyIvLength) -> Self {
return Self::Unknown(anyhow::Error::from(error));
}
}
impl From<std::io::Error> for ManifestAccountLoadError {
fn from(error: std::io::Error) -> Self {
return Self::Unknown(anyhow::Error::from(error));
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -262,7 +304,7 @@ mod tests {
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
let manifest_path = tmp_dir.path().join("manifest.json"); let manifest_path = tmp_dir.path().join("manifest.json");
let manifest = Manifest::new(manifest_path.as_path()); let manifest = Manifest::new(manifest_path.as_path());
assert!(matches!(manifest.save(None), Ok(_))); assert!(matches!(manifest.save(&None), Ok(_)));
} }
#[test] #[test]
@ -275,12 +317,12 @@ mod tests {
account.revocation_code = "R12345".into(); account.revocation_code = "R12345".into();
account.shared_secret = "secret".into(); account.shared_secret = "secret".into();
manifest.add_account(account); 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(); let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap();
assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries.len(), 1);
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); 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!( assert_eq!(
loaded_manifest.entries.len(), loaded_manifest.entries.len(),
loaded_manifest.accounts.len() loaded_manifest.accounts.len()
@ -301,7 +343,7 @@ mod tests {
#[test] #[test]
fn test_should_save_and_load_manifest_encrypted() { fn test_should_save_and_load_manifest_encrypted() {
let passkey: Option<&str> = Some("password"); let passkey: Option<String> = Some("password".into());
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
let manifest_path = tmp_dir.path().join("manifest.json"); let manifest_path = tmp_dir.path().join("manifest.json");
let mut manifest = Manifest::new(manifest_path.as_path()); let mut manifest = Manifest::new(manifest_path.as_path());
@ -311,12 +353,12 @@ mod tests {
account.shared_secret = "secret".into(); account.shared_secret = "secret".into();
manifest.add_account(account); manifest.add_account(account);
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate()); 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(); let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap();
assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries.len(), 1);
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); 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!( assert_eq!(
loaded_manifest.entries.len(), loaded_manifest.entries.len(),
loaded_manifest.accounts.len() loaded_manifest.accounts.len()
@ -345,7 +387,7 @@ mod tests {
account.revocation_code = "R12345".into(); account.revocation_code = "R12345".into();
account.shared_secret = "secret".into(); account.shared_secret = "secret".into();
manifest.add_account(account); manifest.add_account(account);
assert!(matches!(manifest.save(None), Ok(_))); assert!(matches!(manifest.save(&None), Ok(_)));
std::fs::remove_file(&manifest_path).unwrap(); std::fs::remove_file(&manifest_path).unwrap();
let mut loaded_manifest = Manifest::new(manifest_path.as_path()); let mut loaded_manifest = Manifest::new(manifest_path.as_path());
@ -386,7 +428,7 @@ mod tests {
assert!(matches!(result, Ok(_))); assert!(matches!(result, Ok(_)));
let mut manifest = result.unwrap(); let mut manifest = result.unwrap();
assert!(matches!(manifest.entries.last().unwrap().encryption, None)); assert!(matches!(manifest.entries.last().unwrap().encryption, None));
assert!(matches!(manifest.load_accounts(None), Ok(_))); assert!(matches!(manifest.load_accounts(&None), Ok(_)));
assert_eq!( assert_eq!(
manifest.entries.last().unwrap().account_name, manifest.entries.last().unwrap().account_name,
manifest manifest
@ -410,7 +452,7 @@ mod tests {
manifest.entries.last().unwrap().encryption, manifest.entries.last().unwrap().encryption,
Some(_) Some(_)
)); ));
let result = manifest.load_accounts(Some("password")); let result = manifest.load_accounts(&Some("password".into()));
assert!( assert!(
matches!(result, Ok(_)), matches!(result, Ok(_)),
"error when loading accounts: {:?}", "error when loading accounts: {:?}",

View file

@ -182,11 +182,24 @@ fn main() {
} }
} }
let passkey = matches.value_of("passkey"); let mut passkey: Option<String> = matches.value_of("passkey").map(|s| s.into());
manifest loop {
.load_accounts(passkey) match manifest.load_accounts(&passkey) {
.expect("Failed to load accounts in manifest"); 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") { if matches.is_present("setup") {
println!("Log in to the account that you want to link to steamguard-cli"); println!("Log in to the account that you want to link to steamguard-cli");
@ -227,7 +240,7 @@ fn main() {
} }
} }
manifest.add_account(account); manifest.add_account(account);
match manifest.save(passkey) { match manifest.save(&passkey) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", 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."); println!("Authenticator finalized.");
match manifest.save(None) { match manifest.save(&None) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
println!( println!(
@ -296,13 +309,13 @@ fn main() {
} }
} }
manifest.save(passkey).expect("Failed to save manifest."); manifest.save(&passkey).expect("Failed to save manifest.");
return; return;
} else if matches.is_present("encrypt") { } else if matches.is_present("encrypt") {
for entry in &mut manifest.entries { for entry in &mut manifest.entries {
entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); entry.encryption = Some(accountmanager::EntryEncryptionParams::generate());
} }
manifest.save(passkey).expect("Failed to save manifest."); manifest.save(&passkey).expect("Failed to save manifest.");
return; return;
} }
@ -379,9 +392,7 @@ fn main() {
} }
} }
manifest manifest.save(&passkey).expect("Failed to save manifest");
.save(passkey)
.expect("Failed to save manifest");
} else if let Some(_) = matches.subcommand_matches("remove") { } else if let Some(_) = matches.subcommand_matches("remove") {
println!( println!(
"This will remove the mobile authenticator from {} accounts: {}", "This will remove the mobile authenticator from {} accounts: {}",
@ -429,9 +440,7 @@ fn main() {
manifest.remove_account(account_name); manifest.remove_account(account_name);
} }
manifest manifest.save(&passkey).expect("Failed to save manifest.");
.save(passkey)
.expect("Failed to save manifest.");
} else { } else {
let server_time = steamapi::get_server_time(); let server_time = steamapi::get_server_time();
for account in selected_accounts { for account in selected_accounts {