diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 59f17dd..5a2354c 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -1,8 +1,9 @@ use aes::Aes256; -use block_modes::block_padding::NoPadding; +use block_modes::block_padding::{NoPadding, Padding, Pkcs7}; use block_modes::{BlockMode, Cbc}; use log::*; use ring::pbkdf2; +use ring::rand::SecureRandom; use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{BufReader, Read, Write}; @@ -103,15 +104,19 @@ impl Manifest { let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + // This sucks. let mut ciphertext: Vec = vec![]; reader.read_to_end(&mut ciphertext)?; ciphertext = base64::decode(ciphertext)?; let size: usize = ciphertext.len() / 16 + 1; let mut buffer = vec![0xffu8; 16 * size]; buffer[..ciphertext.len()].copy_from_slice(&ciphertext); - let decrypted = cipher.clone().decrypt(&mut buffer[..ciphertext.len()])?; + 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(); - account = serde_json::from_slice(decrypted)?; + 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."); @@ -161,21 +166,47 @@ impl Manifest { self.entries.remove(index); } - pub fn save(&self) -> anyhow::Result<()> { + pub fn save(&self, passkey: Option<&str>) -> anyhow::Result<()> { ensure!( self.entries.len() == self.accounts.len(), "Manifest entries don't match accounts." ); for (entry, account) in self.entries.iter().zip(&self.accounts) { debug!("saving {}", entry.filename); - let serialized = serde_json::to_string(account.as_ref())?; + let serialized = serde_json::to_vec(account.as_ref())?; ensure!( serialized.len() > 2, "Something extra weird happened and the account was serialized into nothing." ); + + let final_buffer: Vec; + match (passkey, entry.encryption.as_ref()) { + (Some(passkey), Some(params)) => { + let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; + let iv = base64::decode(¶ms.iv)?; + let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + + let plaintext = serialized; + let size: usize = plaintext.len() / 16 + 1; + let mut buffer = vec![0xffu8; 16 * size]; + assert!(plaintext.len() < 16 * size); + buffer[..plaintext.len()].copy_from_slice(&plaintext.as_slice()); + let mut padded = Pkcs7::pad(&mut buffer, plaintext.len(), 16 * size).unwrap(); + let ciphertext = cipher.encrypt(&mut padded, 16 * size)?; + + final_buffer = base64::encode(&ciphertext).as_bytes().to_vec(); + } + (None, Some(_)) => { + bail!("maFiles are encrypted, but no passkey was provided."); + } + (_, None) => { + final_buffer = serialized; + } + }; + let path = Path::new(&self.folder).join(&entry.filename); let mut file = File::create(path)?; - file.write_all(serialized.as_bytes())?; + file.write_all(final_buffer.as_slice())?; file.sync_data()?; } debug!("saving manifest"); @@ -188,7 +219,7 @@ impl Manifest { } } -const PBKDF2_ITERATIONS: u32 = 50000; +const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive. Is this needed to remain compatible with SDA? const SALT_LENGTH: usize = 8; const KEY_SIZE_BYTES: usize = 32; const IV_LENGTH: usize = 16; @@ -207,6 +238,20 @@ fn get_encryption_key(passkey: &String, salt: &String) -> anyhow::Result<[u8; KE return Ok(full_key); } +impl EntryEncryptionParams { + pub fn generate() -> EntryEncryptionParams { + let rng = ring::rand::SystemRandom::new(); + let mut salt = [0u8; SALT_LENGTH]; + let mut iv = [0u8; IV_LENGTH]; + rng.fill(&mut salt).expect("Unable to generate salt."); + rng.fill(&mut iv).expect("Unable to generate IV."); + EntryEncryptionParams { + salt: base64::encode(salt), + iv: base64::encode(iv), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -217,7 +262,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(), Ok(_))); + assert!(matches!(manifest.save(None), Ok(_))); } #[test] @@ -230,7 +275,7 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(), 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); @@ -254,6 +299,42 @@ mod tests { ); } + #[test] + fn test_should_save_and_load_manifest_encrypted() { + let passkey: Option<&str> = Some("password"); + 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()); + let mut account = SteamGuardAccount::new(); + account.account_name = "asdf1234".into(); + account.revocation_code = "R12345".into(); + account.shared_secret = "secret".into(); + manifest.add_account(account); + manifest.entries[0].encryption = Some(EntryEncryptionParams::generate()); + 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_eq!( + loaded_manifest.entries.len(), + loaded_manifest.accounts.len() + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().account_name, + "asdf1234" + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().revocation_code, + "R12345" + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().shared_secret, + "secret" + ); + } + #[test] fn test_should_import() { let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); @@ -264,7 +345,7 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(), Ok(_))); + assert!(matches!(manifest.save(None), Ok(_))); std::fs::remove_file(&manifest_path).unwrap(); let mut loaded_manifest = Manifest::new(manifest_path.as_path()); diff --git a/src/main.rs b/src/main.rs index 7300fec..427809a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,6 +103,14 @@ fn cli() -> App<'static, 'static> { App::new("remove") .about("Remove the authenticator from an account.") ) + .subcommand( + App::new("encrypt") + .about("Encrypt maFiles.") + ) + .subcommand( + App::new("decrypt") + .about("Decrypt maFiles.") + ) .subcommand( App::new("debug") .arg( @@ -174,8 +182,10 @@ fn main() { } } + let passkey = matches.value_of("passkey"); + manifest - .load_accounts(matches.value_of("passkey")) + .load_accounts(passkey) .expect("Failed to load accounts in manifest"); if matches.is_present("setup") { @@ -217,7 +227,7 @@ fn main() { } } manifest.add_account(account); - match manifest.save() { + 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); @@ -262,7 +272,7 @@ fn main() { } println!("Authenticator finalized."); - match manifest.save() { + match manifest.save(None) { Ok(_) => {} Err(err) => { println!( @@ -286,7 +296,13 @@ fn main() { } } - manifest.save().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."); return; } @@ -363,7 +379,9 @@ fn main() { } } - manifest.save().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: {}", @@ -411,7 +429,9 @@ fn main() { manifest.remove_account(account_name); } - manifest.save().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 {