implement saving encrypted maFiles
This commit is contained in:
parent
02d8cade2a
commit
1b1f12f423
2 changed files with 117 additions and 16 deletions
|
@ -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(¶ms.iv)?;
|
let iv = base64::decode(¶ms.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(), ¶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 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());
|
||||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue