implement saving encrypted maFiles

This commit is contained in:
Carson McManus 2021-08-15 23:20:49 -04:00
parent 02d8cade2a
commit 1b1f12f423
2 changed files with 117 additions and 16 deletions

View file

@ -1,8 +1,9 @@
use aes::Aes256; use aes::Aes256;
use block_modes::block_padding::NoPadding; use block_modes::block_padding::{NoPadding, Padding, Pkcs7};
use block_modes::{BlockMode, Cbc}; use block_modes::{BlockMode, Cbc};
use log::*; use log::*;
use ring::pbkdf2; use ring::pbkdf2;
use ring::rand::SecureRandom;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs::File; use std::fs::File;
use std::io::{BufReader, Read, Write}; use std::io::{BufReader, Read, Write};
@ -103,15 +104,19 @@ impl Manifest {
let iv = base64::decode(&params.iv)?; let iv = base64::decode(&params.iv)?;
let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?;
// This sucks.
let mut ciphertext: Vec<u8> = vec![]; let mut ciphertext: Vec<u8> = vec![];
reader.read_to_end(&mut ciphertext)?; reader.read_to_end(&mut ciphertext)?;
ciphertext = base64::decode(ciphertext)?; ciphertext = base64::decode(ciphertext)?;
let size: usize = ciphertext.len() / 16 + 1; let size: usize = ciphertext.len() / 16 + 1;
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.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(_)) => { (None, Some(_)) => {
bail!("maFiles are encrypted, but no passkey was provided."); bail!("maFiles are encrypted, but no passkey was provided.");
@ -161,21 +166,47 @@ impl Manifest {
self.entries.remove(index); self.entries.remove(index);
} }
pub fn save(&self) -> anyhow::Result<()> { pub fn save(&self, passkey: Option<&str>) -> 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."
); );
for (entry, account) in self.entries.iter().zip(&self.accounts) { for (entry, account) in self.entries.iter().zip(&self.accounts) {
debug!("saving {}", entry.filename); debug!("saving {}", entry.filename);
let serialized = serde_json::to_string(account.as_ref())?; let serialized = serde_json::to_vec(account.as_ref())?;
ensure!( ensure!(
serialized.len() > 2, serialized.len() > 2,
"Something extra weird happened and the account was serialized into nothing." "Something extra weird happened and the account was serialized into nothing."
); );
let final_buffer: Vec<u8>;
match (passkey, entry.encryption.as_ref()) {
(Some(passkey), Some(params)) => {
let key = get_encryption_key(&passkey.into(), &params.salt)?;
let iv = base64::decode(&params.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 path = Path::new(&self.folder).join(&entry.filename);
let mut file = File::create(path)?; let mut file = File::create(path)?;
file.write_all(serialized.as_bytes())?; file.write_all(final_buffer.as_slice())?;
file.sync_data()?; file.sync_data()?;
} }
debug!("saving manifest"); 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 SALT_LENGTH: usize = 8;
const KEY_SIZE_BYTES: usize = 32; const KEY_SIZE_BYTES: usize = 32;
const IV_LENGTH: usize = 16; 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); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -217,7 +262,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(), Ok(_))); assert!(matches!(manifest.save(None), Ok(_)));
} }
#[test] #[test]
@ -230,7 +275,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(), 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);
@ -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] #[test]
fn test_should_import() { fn test_should_import() {
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
@ -264,7 +345,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(), 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());

View file

@ -103,6 +103,14 @@ fn cli() -> App<'static, 'static> {
App::new("remove") App::new("remove")
.about("Remove the authenticator from an account.") .about("Remove the authenticator from an account.")
) )
.subcommand(
App::new("encrypt")
.about("Encrypt maFiles.")
)
.subcommand(
App::new("decrypt")
.about("Decrypt maFiles.")
)
.subcommand( .subcommand(
App::new("debug") App::new("debug")
.arg( .arg(
@ -174,8 +182,10 @@ fn main() {
} }
} }
let passkey = matches.value_of("passkey");
manifest manifest
.load_accounts(matches.value_of("passkey")) .load_accounts(passkey)
.expect("Failed to load accounts in manifest"); .expect("Failed to load accounts in manifest");
if matches.is_present("setup") { if matches.is_present("setup") {
@ -217,7 +227,7 @@ fn main() {
} }
} }
manifest.add_account(account); manifest.add_account(account);
match manifest.save() { 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);
@ -262,7 +272,7 @@ fn main() {
} }
println!("Authenticator finalized."); println!("Authenticator finalized.");
match manifest.save() { match manifest.save(None) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
println!( 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; 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") { } 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: {}",
@ -411,7 +429,9 @@ fn main() {
manifest.remove_account(account_name); manifest.remove_account(account_name);
} }
manifest.save().expect("Failed to save manifest."); 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 {