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 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<u8> = 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<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 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());
|
||||
|
|
32
src/main.rs
32
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 {
|
||||
|
|
Loading…
Reference in a new issue