2023-06-22 22:20:15 +02:00
|
|
|
use crate::accountmanager::legacy::SdaManifest;
|
2023-07-03 16:23:56 +02:00
|
|
|
pub use crate::encryption::EncryptionScheme;
|
2021-08-19 22:54:18 +02:00
|
|
|
use crate::encryption::EntryEncryptor;
|
2021-08-01 14:43:18 +02:00
|
|
|
use log::*;
|
2023-07-03 18:25:43 +02:00
|
|
|
use rayon::prelude::*;
|
2023-06-26 02:23:26 +02:00
|
|
|
use secrecy::{ExposeSecret, SecretString};
|
2022-02-18 20:55:10 +01:00
|
|
|
use std::collections::HashMap;
|
2021-08-15 17:52:54 +02:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::{BufReader, Read, Write};
|
2021-03-26 00:47:44 +01:00
|
|
|
use std::path::Path;
|
2021-08-01 18:34:13 +02:00
|
|
|
use std::sync::{Arc, Mutex};
|
2023-06-23 19:36:23 +02:00
|
|
|
use steamguard::SteamGuardAccount;
|
2021-08-17 03:13:58 +02:00
|
|
|
use thiserror::Error;
|
2021-03-25 22:45:41 +01:00
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
mod legacy;
|
|
|
|
pub mod manifest;
|
|
|
|
pub mod migrate;
|
2023-08-23 19:59:14 +02:00
|
|
|
mod steamv2;
|
2023-10-08 17:10:46 +02:00
|
|
|
mod winauth;
|
2021-03-25 22:45:41 +01:00
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
pub use manifest::*;
|
2021-08-15 02:47:29 +02:00
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
#[derive(Debug, Default)]
|
|
|
|
pub struct AccountManager {
|
|
|
|
manifest: Manifest,
|
|
|
|
accounts: HashMap<String, Arc<Mutex<SteamGuardAccount>>>,
|
|
|
|
folder: String,
|
2023-06-26 02:23:26 +02:00
|
|
|
passkey: Option<SecretString>,
|
2021-08-13 00:06:18 +02:00
|
|
|
}
|
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
impl AccountManager {
|
2021-08-13 00:06:18 +02:00
|
|
|
/// `path` should be the path to manifest.json
|
|
|
|
pub fn new(path: &Path) -> Self {
|
2023-06-22 22:20:15 +02:00
|
|
|
Self {
|
2021-08-13 00:06:18 +02:00
|
|
|
folder: String::from(path.parent().unwrap().to_str().unwrap()),
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
pub fn from_manifest(manifest: Manifest, folder: String) -> Self {
|
|
|
|
Self {
|
|
|
|
manifest,
|
|
|
|
folder,
|
|
|
|
..Default::default()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn register_accounts(&mut self, accounts: Vec<SteamGuardAccount>) {
|
|
|
|
for account in accounts {
|
|
|
|
self.register_loaded_account(Arc::new(Mutex::new(account)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn load(path: &Path) -> anyhow::Result<Self, ManifestLoadError> {
|
2021-08-08 18:54:46 +02:00
|
|
|
debug!("loading manifest: {:?}", &path);
|
|
|
|
let file = File::open(path)?;
|
2023-06-22 22:20:15 +02:00
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
let mut buffer = String::new();
|
|
|
|
reader.read_to_string(&mut buffer)?;
|
2023-06-24 18:06:12 +02:00
|
|
|
let mut deser = serde_json::Deserializer::from_str(&buffer);
|
|
|
|
let manifest: Manifest = match serde_path_to_error::deserialize(&mut deser) {
|
2023-06-22 22:20:15 +02:00
|
|
|
Ok(m) => m,
|
|
|
|
Err(orig_err) => match serde_json::from_str::<SdaManifest>(&buffer) {
|
|
|
|
Ok(_) => return Err(ManifestLoadError::MigrationNeeded)?,
|
|
|
|
Err(_) => return Err(orig_err)?,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
if manifest.version != CURRENT_MANIFEST_VERSION {
|
|
|
|
return Err(ManifestLoadError::MigrationNeeded)?;
|
|
|
|
}
|
|
|
|
let accountmanager = Self {
|
|
|
|
manifest,
|
|
|
|
folder: String::from(path.parent().unwrap().to_str().unwrap()),
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
Ok(accountmanager)
|
2021-08-08 18:54:46 +02:00
|
|
|
}
|
2021-03-26 19:05:54 +01:00
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
/// Tells the manager to keep track of the encryption passkey, and use it for encryption when loading or saving accounts.
|
2023-06-26 02:23:26 +02:00
|
|
|
pub fn submit_passkey(&mut self, passkey: Option<SecretString>) {
|
2023-03-18 15:08:57 +01:00
|
|
|
if let Some(p) = passkey.as_ref() {
|
2023-06-26 02:23:26 +02:00
|
|
|
if p.expose_secret().is_empty() {
|
2023-03-18 15:08:57 +01:00
|
|
|
panic!("Encryption passkey cannot be empty");
|
|
|
|
}
|
|
|
|
}
|
2022-02-18 20:55:10 +01:00
|
|
|
if passkey.is_some() {
|
|
|
|
debug!("passkey was submitted to manifest");
|
|
|
|
} else {
|
|
|
|
debug!("passkey was revoked from manifest");
|
|
|
|
}
|
|
|
|
self.passkey = passkey;
|
|
|
|
}
|
|
|
|
|
2023-07-02 16:44:18 +02:00
|
|
|
pub fn keyring_id(&self) -> Option<&String> {
|
|
|
|
self.manifest.keyring_id.as_ref()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_keyring_id(&mut self, keyring_id: String) {
|
|
|
|
self.manifest.keyring_id = Some(keyring_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn clear_keyring_id(&mut self) {
|
|
|
|
self.manifest.keyring_id = None;
|
|
|
|
}
|
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
/// Loads all accounts, and registers them.
|
2022-02-18 20:55:10 +01:00
|
|
|
pub fn load_accounts(&mut self) -> anyhow::Result<(), ManifestAccountLoadError> {
|
2023-07-03 18:25:43 +02:00
|
|
|
let accounts = self
|
|
|
|
.manifest
|
|
|
|
.entries
|
|
|
|
.par_iter()
|
|
|
|
.map(|entry| self.load_account_by_entry(entry))
|
|
|
|
.collect::<Vec<_>>();
|
2022-02-22 15:03:40 +01:00
|
|
|
for account in accounts {
|
2023-07-03 18:25:43 +02:00
|
|
|
self.register_loaded_account(account?);
|
2022-02-18 20:55:10 +01:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-02-22 15:03:40 +01:00
|
|
|
/// Loads an account by account name.
|
|
|
|
/// Must call `register_loaded_account` after loading the account.
|
2022-02-18 20:55:10 +01:00
|
|
|
fn load_account(
|
2022-02-22 15:03:40 +01:00
|
|
|
&self,
|
2023-06-26 18:44:14 +02:00
|
|
|
account_name: impl AsRef<str>,
|
2022-02-22 15:03:40 +01:00
|
|
|
) -> anyhow::Result<Arc<Mutex<SteamGuardAccount>>, ManifestAccountLoadError> {
|
|
|
|
let entry = self.get_entry(account_name)?;
|
2023-06-22 22:20:15 +02:00
|
|
|
self.load_account_by_entry(entry)
|
2022-02-22 15:03:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Loads an account from a manifest entry.
|
|
|
|
/// Must call `register_loaded_account` after loading the account.
|
|
|
|
fn load_account_by_entry(
|
|
|
|
&self,
|
|
|
|
entry: &ManifestEntry,
|
|
|
|
) -> anyhow::Result<Arc<Mutex<SteamGuardAccount>>, ManifestAccountLoadError> {
|
2022-02-18 20:55:10 +01:00
|
|
|
let path = Path::new(&self.folder).join(&entry.filename);
|
2023-06-22 22:20:15 +02:00
|
|
|
let account = entry.load(
|
|
|
|
path.as_path(),
|
|
|
|
self.passkey.as_ref(),
|
|
|
|
entry.encryption.as_ref(),
|
|
|
|
)?;
|
2022-02-22 15:03:40 +01:00
|
|
|
let account = Arc::new(Mutex::new(account));
|
|
|
|
Ok(account)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Register an account as loaded, so it can be operated on.
|
|
|
|
fn register_loaded_account(&mut self, account: Arc<Mutex<SteamGuardAccount>>) {
|
|
|
|
let account_name = account.lock().unwrap().account_name.clone();
|
|
|
|
self.accounts.insert(account_name, account);
|
2021-08-08 18:54:46 +02:00
|
|
|
}
|
2021-08-01 17:20:57 +02:00
|
|
|
|
2021-09-06 22:51:44 +02:00
|
|
|
pub fn account_exists(&self, account_name: &String) -> bool {
|
2023-06-22 22:20:15 +02:00
|
|
|
for entry in &self.manifest.entries {
|
2021-09-06 22:51:44 +02:00
|
|
|
if &entry.account_name == account_name {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2023-06-22 22:20:15 +02:00
|
|
|
false
|
2021-09-06 22:51:44 +02:00
|
|
|
}
|
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
pub fn add_account(&mut self, account: SteamGuardAccount) {
|
|
|
|
debug!("adding account to manifest: {}", account.account_name);
|
2023-06-22 22:20:15 +02:00
|
|
|
self.manifest.entries.push(ManifestEntry {
|
2021-08-08 18:54:46 +02:00
|
|
|
filename: format!("{}.maFile", &account.account_name),
|
2023-06-22 22:20:15 +02:00
|
|
|
steam_id: account.steam_id,
|
2021-08-14 19:39:01 +02:00
|
|
|
account_name: account.account_name.clone(),
|
2021-08-15 02:47:29 +02:00
|
|
|
encryption: None,
|
2021-08-08 18:54:46 +02:00
|
|
|
});
|
2022-02-18 20:55:10 +01:00
|
|
|
self.accounts
|
|
|
|
.insert(account.account_name.clone(), Arc::new(Mutex::new(account)));
|
2021-08-08 18:54:46 +02:00
|
|
|
}
|
2021-08-01 17:20:57 +02:00
|
|
|
|
2023-07-10 15:43:02 +02:00
|
|
|
pub fn import_account(
|
|
|
|
&mut self,
|
|
|
|
import_path: &String,
|
|
|
|
) -> anyhow::Result<(), ManifestAccountImportError> {
|
2022-06-19 17:43:37 +02:00
|
|
|
let path = Path::new(import_path);
|
2023-07-10 15:43:02 +02:00
|
|
|
if !path.exists() {
|
|
|
|
return Err(ManifestAccountImportError::FileNotFound);
|
|
|
|
}
|
|
|
|
if !path.is_file() {
|
|
|
|
return Err(ManifestAccountImportError::NotAFile);
|
|
|
|
}
|
2021-08-14 01:04:03 +02:00
|
|
|
|
|
|
|
let file = File::open(path)?;
|
|
|
|
let reader = BufReader::new(file);
|
2023-06-24 18:06:12 +02:00
|
|
|
let mut deser = serde_json::Deserializer::from_reader(reader);
|
|
|
|
let account: SteamGuardAccount = serde_path_to_error::deserialize(&mut deser)?;
|
2023-07-10 15:43:02 +02:00
|
|
|
if self.account_exists(&account.account_name) {
|
|
|
|
return Err(ManifestAccountImportError::AlreadyExists {
|
|
|
|
account_name: account.account_name,
|
|
|
|
});
|
|
|
|
}
|
2021-08-14 01:04:03 +02:00
|
|
|
self.add_account(account);
|
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
Ok(())
|
2021-08-14 01:04:03 +02:00
|
|
|
}
|
|
|
|
|
2021-08-12 01:39:29 +02:00
|
|
|
pub fn remove_account(&mut self, account_name: String) {
|
|
|
|
let index = self
|
2023-06-22 22:20:15 +02:00
|
|
|
.manifest
|
2022-02-18 20:55:10 +01:00
|
|
|
.entries
|
2021-08-12 01:39:29 +02:00
|
|
|
.iter()
|
2022-02-18 20:55:10 +01:00
|
|
|
.position(|a| a.account_name == account_name)
|
2021-08-12 01:39:29 +02:00
|
|
|
.unwrap();
|
2022-02-18 20:55:10 +01:00
|
|
|
self.accounts.remove(&account_name);
|
2023-06-22 22:20:15 +02:00
|
|
|
self.manifest.entries.remove(index);
|
2021-08-12 01:39:29 +02:00
|
|
|
}
|
|
|
|
|
2022-02-18 20:55:10 +01:00
|
|
|
/// Saves the manifest and all loaded accounts.
|
|
|
|
pub fn save(&self) -> anyhow::Result<()> {
|
2021-09-01 14:52:23 +02:00
|
|
|
info!("Saving manifest and accounts...");
|
2023-07-03 18:25:43 +02:00
|
|
|
let save_results: Vec<_> = self
|
2022-02-18 20:55:10 +01:00
|
|
|
.accounts
|
|
|
|
.values()
|
2023-07-03 18:25:43 +02:00
|
|
|
.par_bridge()
|
|
|
|
.map(|account| -> anyhow::Result<()> {
|
|
|
|
let account = account.lock().unwrap();
|
|
|
|
let entry = self.get_entry(&account.account_name)?.clone();
|
|
|
|
debug!("saving {}", entry.filename);
|
|
|
|
let serialized = serde_json::to_vec(&account.clone())?;
|
|
|
|
ensure!(
|
|
|
|
serialized.len() > 2,
|
|
|
|
"Something extra weird happened and the account was serialized into nothing."
|
|
|
|
);
|
|
|
|
|
|
|
|
let final_buffer: Vec<u8> = match (&self.passkey, entry.encryption.as_ref()) {
|
|
|
|
(Some(passkey), Some(scheme)) => {
|
|
|
|
scheme.encrypt(passkey.expose_secret(), serialized)?
|
|
|
|
}
|
|
|
|
(None, Some(_)) => {
|
|
|
|
bail!("maFiles are encrypted, but no passkey was provided.");
|
|
|
|
}
|
|
|
|
(_, None) => serialized,
|
|
|
|
};
|
|
|
|
|
|
|
|
let path = Path::new(&self.folder).join(&entry.filename);
|
|
|
|
let mut file = File::create(path)?;
|
|
|
|
file.write_all(final_buffer.as_slice())?;
|
|
|
|
file.sync_data()?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
for result in save_results {
|
|
|
|
result?;
|
2021-08-08 18:54:46 +02:00
|
|
|
}
|
|
|
|
debug!("saving manifest");
|
2023-06-22 22:20:15 +02:00
|
|
|
let manifest_serialized = serde_json::to_string(&self.manifest)?;
|
2021-08-08 18:54:46 +02:00
|
|
|
let path = Path::new(&self.folder).join("manifest.json");
|
|
|
|
let mut file = File::create(path)?;
|
|
|
|
file.write_all(manifest_serialized.as_bytes())?;
|
|
|
|
file.sync_data()?;
|
|
|
|
Ok(())
|
|
|
|
}
|
2022-02-18 20:55:10 +01:00
|
|
|
|
|
|
|
/// Return all loaded accounts. Order is not guarenteed.
|
2022-12-06 16:07:25 +01:00
|
|
|
#[allow(dead_code)]
|
2022-02-18 20:55:10 +01:00
|
|
|
pub fn get_all_loaded(&self) -> Vec<Arc<Mutex<SteamGuardAccount>>> {
|
2023-06-22 22:20:15 +02:00
|
|
|
return self.accounts.values().cloned().collect();
|
2022-02-18 20:55:10 +01:00
|
|
|
}
|
|
|
|
|
2022-12-06 16:07:25 +01:00
|
|
|
#[allow(dead_code)]
|
2022-02-18 20:55:10 +01:00
|
|
|
pub fn get_entry(
|
|
|
|
&self,
|
2023-06-26 18:44:14 +02:00
|
|
|
account_name: impl AsRef<str>,
|
2022-02-18 20:55:10 +01:00
|
|
|
) -> anyhow::Result<&ManifestEntry, ManifestAccountLoadError> {
|
2023-06-22 22:20:15 +02:00
|
|
|
self.manifest
|
|
|
|
.entries
|
2022-02-18 20:55:10 +01:00
|
|
|
.iter()
|
2023-06-26 18:44:14 +02:00
|
|
|
.find(|e| e.account_name == account_name.as_ref())
|
2022-02-18 20:55:10 +01:00
|
|
|
.ok_or(ManifestAccountLoadError::MissingManifestEntry)
|
|
|
|
}
|
|
|
|
|
2022-12-06 16:07:25 +01:00
|
|
|
#[allow(dead_code)]
|
2022-02-18 20:55:10 +01:00
|
|
|
pub fn get_entry_mut(
|
|
|
|
&mut self,
|
2023-06-26 18:44:14 +02:00
|
|
|
account_name: impl AsRef<str>,
|
2022-02-18 20:55:10 +01:00
|
|
|
) -> anyhow::Result<&mut ManifestEntry, ManifestAccountLoadError> {
|
2023-06-22 22:20:15 +02:00
|
|
|
self.manifest
|
|
|
|
.entries
|
2022-02-18 20:55:10 +01:00
|
|
|
.iter_mut()
|
2023-06-26 18:44:14 +02:00
|
|
|
.find(|e| e.account_name == account_name.as_ref())
|
2022-02-18 20:55:10 +01:00
|
|
|
.ok_or(ManifestAccountLoadError::MissingManifestEntry)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn has_passkey(&self) -> bool {
|
|
|
|
self.passkey.is_some()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Gets the specified account by name.
|
|
|
|
/// Fails if the account does not exist in the manifest entries.
|
|
|
|
pub fn get_account(
|
|
|
|
&self,
|
2023-06-26 18:44:14 +02:00
|
|
|
account_name: impl AsRef<str>,
|
2022-02-18 20:55:10 +01:00
|
|
|
) -> anyhow::Result<Arc<Mutex<SteamGuardAccount>>> {
|
|
|
|
let account = self
|
|
|
|
.accounts
|
2023-06-26 18:44:14 +02:00
|
|
|
.get(account_name.as_ref())
|
2023-06-22 22:20:15 +02:00
|
|
|
.cloned()
|
2022-02-18 20:55:10 +01:00
|
|
|
.ok_or(anyhow!("Account not loaded"));
|
2023-06-22 22:20:15 +02:00
|
|
|
account
|
2022-02-18 20:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Get or load the spcified account.
|
|
|
|
pub fn get_or_load_account(
|
|
|
|
&mut self,
|
2023-06-26 18:44:14 +02:00
|
|
|
account_name: impl AsRef<str>,
|
2022-02-18 20:55:10 +01:00
|
|
|
) -> anyhow::Result<Arc<Mutex<SteamGuardAccount>>, ManifestAccountLoadError> {
|
2023-06-26 18:44:14 +02:00
|
|
|
let account = self.get_account(account_name.as_ref());
|
2023-06-22 22:20:15 +02:00
|
|
|
if let Ok(account) = account {
|
|
|
|
return Ok(account);
|
2022-02-18 20:55:10 +01:00
|
|
|
}
|
2023-06-26 18:44:14 +02:00
|
|
|
let account = self.load_account(account_name.as_ref())?;
|
2022-02-22 15:03:40 +01:00
|
|
|
self.register_loaded_account(account.clone());
|
2023-06-22 22:20:15 +02:00
|
|
|
Ok(account)
|
2022-02-22 15:03:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Determine if any manifest entries are missing `account_name`.
|
|
|
|
fn is_missing_account_name(&self) -> bool {
|
2023-06-22 22:20:15 +02:00
|
|
|
self.manifest
|
|
|
|
.entries
|
|
|
|
.iter()
|
|
|
|
.any(|e| e.account_name.is_empty())
|
2022-02-22 15:03:40 +01:00
|
|
|
}
|
|
|
|
|
2022-12-05 16:55:45 +01:00
|
|
|
fn has_any_uppercase_in_account_names(&self) -> bool {
|
2023-06-22 22:20:15 +02:00
|
|
|
self.manifest
|
|
|
|
.entries
|
2022-12-05 16:55:45 +01:00
|
|
|
.iter()
|
|
|
|
.any(|e| e.account_name != e.account_name.to_lowercase())
|
|
|
|
}
|
|
|
|
|
2022-02-22 15:19:56 +01:00
|
|
|
/// Performs auto-upgrades on the manifest. Returns true if any upgrades were performed.
|
|
|
|
pub fn auto_upgrade(&mut self) -> anyhow::Result<bool, ManifestAccountLoadError> {
|
2022-02-22 15:03:40 +01:00
|
|
|
debug!("Performing auto-upgrade...");
|
2022-02-22 15:19:56 +01:00
|
|
|
let mut upgraded = false;
|
2022-02-22 15:03:40 +01:00
|
|
|
if self.is_missing_account_name() {
|
|
|
|
debug!("Adding missing account names");
|
2023-06-22 22:20:15 +02:00
|
|
|
for i in 0..self.manifest.entries.len() {
|
|
|
|
let account = self.load_account_by_entry(&self.manifest.entries[i].clone())?;
|
|
|
|
self.manifest.entries[i].account_name =
|
|
|
|
account.lock().unwrap().account_name.clone();
|
2022-02-22 15:03:40 +01:00
|
|
|
}
|
2022-02-22 15:19:56 +01:00
|
|
|
upgraded = true;
|
2022-02-22 15:03:40 +01:00
|
|
|
}
|
|
|
|
|
2022-12-05 16:55:45 +01:00
|
|
|
if self.has_any_uppercase_in_account_names() {
|
|
|
|
debug!("Lowercasing account names");
|
2023-06-22 22:20:15 +02:00
|
|
|
for i in 0..self.manifest.entries.len() {
|
|
|
|
self.manifest.entries[i].account_name =
|
|
|
|
self.manifest.entries[i].account_name.to_lowercase();
|
2022-12-05 16:55:45 +01:00
|
|
|
}
|
|
|
|
upgraded = true;
|
|
|
|
}
|
|
|
|
|
2022-02-22 15:19:56 +01:00
|
|
|
Ok(upgraded)
|
2022-02-18 20:55:10 +01:00
|
|
|
}
|
2023-06-22 22:20:15 +02:00
|
|
|
|
|
|
|
pub fn iter(&self) -> impl Iterator<Item = &ManifestEntry> {
|
|
|
|
self.manifest.entries.iter()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ManifestEntry> {
|
|
|
|
self.manifest.entries.iter_mut()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trait EntryLoader<T> {
|
|
|
|
fn load(
|
|
|
|
&self,
|
|
|
|
path: &Path,
|
2023-06-26 02:23:26 +02:00
|
|
|
passkey: Option<&SecretString>,
|
2023-07-03 16:23:56 +02:00
|
|
|
encryption_params: Option<&EncryptionScheme>,
|
2023-06-22 22:20:15 +02:00
|
|
|
) -> anyhow::Result<T, ManifestAccountLoadError>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EntryLoader<SteamGuardAccount> for ManifestEntry {
|
|
|
|
fn load(
|
|
|
|
&self,
|
|
|
|
path: &Path,
|
2023-06-26 02:23:26 +02:00
|
|
|
passkey: Option<&SecretString>,
|
2023-07-03 16:23:56 +02:00
|
|
|
encryption_params: Option<&EncryptionScheme>,
|
2023-06-22 22:20:15 +02:00
|
|
|
) -> anyhow::Result<SteamGuardAccount, ManifestAccountLoadError> {
|
|
|
|
debug!("loading entry: {:?}", path);
|
|
|
|
let file = File::open(path)?;
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
let account: SteamGuardAccount = match (&passkey, encryption_params.as_ref()) {
|
2023-07-03 16:23:56 +02:00
|
|
|
(Some(passkey), Some(scheme)) => {
|
2023-06-22 22:20:15 +02:00
|
|
|
let mut ciphertext: Vec<u8> = vec![];
|
|
|
|
reader.read_to_end(&mut ciphertext)?;
|
2023-07-03 16:23:56 +02:00
|
|
|
let plaintext = scheme.decrypt(passkey.expose_secret(), ciphertext)?;
|
2023-06-22 22:20:15 +02:00
|
|
|
if plaintext[0] != b'{' && plaintext[plaintext.len() - 1] != b'}' {
|
|
|
|
return Err(ManifestAccountLoadError::IncorrectPasskey);
|
|
|
|
}
|
|
|
|
let s = std::str::from_utf8(&plaintext).unwrap();
|
2023-06-24 18:06:12 +02:00
|
|
|
let mut deser = serde_json::Deserializer::from_str(s);
|
|
|
|
serde_path_to_error::deserialize(&mut deser)?
|
2023-06-22 22:20:15 +02:00
|
|
|
}
|
|
|
|
(None, Some(_)) => {
|
|
|
|
return Err(ManifestAccountLoadError::MissingPasskey);
|
|
|
|
}
|
2023-06-24 18:06:12 +02:00
|
|
|
(_, None) => {
|
|
|
|
let mut deser = serde_json::Deserializer::from_reader(reader);
|
|
|
|
serde_path_to_error::deserialize(&mut deser)?
|
|
|
|
}
|
2023-06-22 22:20:15 +02:00
|
|
|
};
|
|
|
|
Ok(account)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum ManifestLoadError {
|
|
|
|
#[error("Could not find manifest.json in the specified directory.")]
|
|
|
|
Missing(#[from] std::io::Error),
|
|
|
|
#[error("Manifest needs to be migrated to the latest format.")]
|
|
|
|
MigrationNeeded,
|
2023-06-24 18:06:12 +02:00
|
|
|
#[error("Failed to deserialize the manifest. {self:?}")]
|
|
|
|
DeserializationFailed(#[from] serde_path_to_error::Error<serde_json::Error>),
|
2023-06-22 22:20:15 +02:00
|
|
|
#[error(transparent)]
|
|
|
|
Unknown(#[from] anyhow::Error),
|
2021-03-26 00:47:44 +01:00
|
|
|
}
|
2021-08-14 19:39:01 +02:00
|
|
|
|
2021-08-17 03:13:58 +02:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum ManifestAccountLoadError {
|
2022-02-18 20:55:10 +01:00
|
|
|
#[error("Could not find an entry in the manifest for this account. Check your spelling.")]
|
|
|
|
MissingManifestEntry,
|
2021-08-17 03:13:58 +02:00
|
|
|
#[error("Manifest accounts are encrypted, but no passkey was provided.")]
|
|
|
|
MissingPasskey,
|
2021-08-20 16:01:23 +02:00
|
|
|
#[error("Incorrect passkey provided.")]
|
|
|
|
IncorrectPasskey,
|
2021-08-19 22:54:18 +02:00
|
|
|
#[error("Failed to decrypt account. {self:?}")]
|
|
|
|
DecryptionFailed(#[from] crate::encryption::EntryEncryptionError),
|
2023-06-24 18:06:12 +02:00
|
|
|
#[error("Failed to deserialize the account. {self:?}")]
|
|
|
|
DeserializationFailed(#[from] serde_path_to_error::Error<serde_json::Error>),
|
2021-08-17 03:13:58 +02:00
|
|
|
#[error(transparent)]
|
|
|
|
Unknown(#[from] anyhow::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<base64::DecodeError> for ManifestAccountLoadError {
|
|
|
|
fn from(error: base64::DecodeError) -> Self {
|
2023-06-22 22:20:15 +02:00
|
|
|
Self::Unknown(anyhow::Error::from(error))
|
2021-08-17 03:13:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
impl From<std::io::Error> for ManifestAccountLoadError {
|
|
|
|
fn from(error: std::io::Error) -> Self {
|
2023-06-22 22:20:15 +02:00
|
|
|
Self::Unknown(anyhow::Error::from(error))
|
2021-08-17 03:13:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-10 15:43:02 +02:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum ManifestAccountImportError {
|
|
|
|
#[error("Could not find the specified file.")]
|
|
|
|
FileNotFound,
|
|
|
|
#[error("The specified path is not a file.")]
|
|
|
|
NotAFile,
|
|
|
|
#[error(
|
|
|
|
"The account you are trying to import, \"{account_name}\", already exists in the manifest."
|
|
|
|
)]
|
|
|
|
AlreadyExists { account_name: String },
|
|
|
|
#[error(transparent)]
|
|
|
|
IOError(#[from] std::io::Error),
|
|
|
|
#[error("Failed to deserialize the account. {self:?}")]
|
|
|
|
DeserializationFailed(#[from] serde_path_to_error::Error<serde_json::Error>),
|
|
|
|
#[error(transparent)]
|
|
|
|
Unknown(#[from] anyhow::Error),
|
|
|
|
}
|
|
|
|
|
2021-08-14 19:39:01 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2022-06-19 20:09:08 +02:00
|
|
|
use steamguard::ExposeSecret;
|
2022-06-19 20:44:18 +02:00
|
|
|
use tempdir::TempDir;
|
2021-08-14 19:39:01 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_should_save_new_manifest() {
|
|
|
|
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
|
|
|
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
2023-06-22 22:20:15 +02:00
|
|
|
let manager = AccountManager::new(manifest_path.as_path());
|
2023-09-10 23:22:23 +02:00
|
|
|
assert!(manager.save().is_ok());
|
2021-08-14 19:39:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2021-08-25 03:13:16 +02:00
|
|
|
fn test_should_save_and_load_manifest() -> anyhow::Result<()> {
|
|
|
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
2021-08-14 19:39:01 +02:00
|
|
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
2021-08-25 03:13:16 +02:00
|
|
|
println!("tempdir: {}", manifest_path.display());
|
2023-06-22 22:20:15 +02:00
|
|
|
let mut manager = AccountManager::new(manifest_path.as_path());
|
2021-08-14 19:39:01 +02:00
|
|
|
let mut account = SteamGuardAccount::new();
|
|
|
|
account.account_name = "asdf1234".into();
|
2022-06-19 20:09:08 +02:00
|
|
|
account.revocation_code = String::from("R12345").into();
|
2021-08-25 03:13:16 +02:00
|
|
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
|
|
|
)?;
|
2023-06-22 22:20:15 +02:00
|
|
|
manager.add_account(account);
|
|
|
|
manager.save()?;
|
|
|
|
|
|
|
|
let mut manager = AccountManager::load(manifest_path.as_path())?;
|
|
|
|
assert_eq!(manager.manifest.entries.len(), 1);
|
|
|
|
assert_eq!(manager.manifest.entries[0].filename, "asdf1234.maFile");
|
|
|
|
manager.load_accounts()?;
|
|
|
|
assert_eq!(manager.manifest.entries.len(), manager.accounts.len());
|
2023-06-26 18:44:14 +02:00
|
|
|
let account_name = "asdf1234";
|
|
|
|
let account = manager.get_account(account_name)?;
|
|
|
|
let account = account.lock().unwrap();
|
|
|
|
assert_eq!(account.account_name, "asdf1234");
|
|
|
|
assert_eq!(account.revocation_code.expose_secret(), "R12345");
|
2021-08-14 19:39:01 +02:00
|
|
|
assert_eq!(
|
2023-06-26 18:44:14 +02:00
|
|
|
account.shared_secret,
|
2021-08-25 03:13:16 +02:00
|
|
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
2023-06-26 18:44:14 +02:00
|
|
|
)
|
|
|
|
.unwrap(),
|
2021-08-14 19:39:01 +02:00
|
|
|
);
|
2023-06-22 22:20:15 +02:00
|
|
|
Ok(())
|
2021-08-14 19:39:01 +02:00
|
|
|
}
|
|
|
|
|
2021-08-16 05:20:49 +02:00
|
|
|
#[test]
|
2022-02-18 20:55:10 +01:00
|
|
|
fn test_should_save_and_load_manifest_encrypted() -> anyhow::Result<()> {
|
2023-06-26 02:23:26 +02:00
|
|
|
let passkey = Some(SecretString::new("password".into()));
|
2022-02-18 20:55:10 +01:00
|
|
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
2021-08-16 05:20:49 +02:00
|
|
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
2023-06-22 22:20:15 +02:00
|
|
|
let mut manager = AccountManager::new(manifest_path.as_path());
|
2021-08-16 05:20:49 +02:00
|
|
|
let mut account = SteamGuardAccount::new();
|
|
|
|
account.account_name = "asdf1234".into();
|
2022-06-19 20:09:08 +02:00
|
|
|
account.revocation_code = String::from("R12345").into();
|
2021-08-25 03:13:16 +02:00
|
|
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
2022-02-18 20:55:10 +01:00
|
|
|
)?;
|
2023-06-22 22:20:15 +02:00
|
|
|
manager.add_account(account);
|
2023-07-03 16:23:56 +02:00
|
|
|
manager.manifest.entries[0].encryption = Some(EncryptionScheme::generate());
|
2023-06-22 22:20:15 +02:00
|
|
|
manager.submit_passkey(passkey.clone());
|
2023-09-10 23:22:23 +02:00
|
|
|
assert!(manager.save().is_ok());
|
2023-06-22 22:20:15 +02:00
|
|
|
|
|
|
|
let mut loaded_manager = AccountManager::load(manifest_path.as_path()).unwrap();
|
|
|
|
loaded_manager.submit_passkey(passkey);
|
|
|
|
assert_eq!(loaded_manager.manifest.entries.len(), 1);
|
|
|
|
assert_eq!(
|
|
|
|
loaded_manager.manifest.entries[0].filename,
|
|
|
|
"asdf1234.maFile"
|
|
|
|
);
|
|
|
|
let _r = loaded_manager.load_accounts();
|
2022-02-18 20:55:10 +01:00
|
|
|
if _r.is_err() {
|
|
|
|
eprintln!("{:?}", _r);
|
|
|
|
}
|
2023-09-10 23:22:23 +02:00
|
|
|
assert!(_r.is_ok());
|
2021-08-16 05:20:49 +02:00
|
|
|
assert_eq!(
|
2023-06-22 22:20:15 +02:00
|
|
|
loaded_manager.manifest.entries.len(),
|
|
|
|
loaded_manager.accounts.len()
|
2021-08-16 05:20:49 +02:00
|
|
|
);
|
2023-06-26 18:44:14 +02:00
|
|
|
let account_name = "asdf1234";
|
|
|
|
let account = loaded_manager.get_account(account_name)?;
|
|
|
|
let account = account.lock().unwrap();
|
|
|
|
assert_eq!(account.account_name, "asdf1234");
|
|
|
|
assert_eq!(account.revocation_code.expose_secret(), "R12345");
|
2021-08-16 05:20:49 +02:00
|
|
|
assert_eq!(
|
2023-06-26 18:44:14 +02:00
|
|
|
account.shared_secret,
|
2021-08-25 03:13:16 +02:00
|
|
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
|
|
|
)
|
|
|
|
.unwrap(),
|
2021-08-16 05:20:49 +02:00
|
|
|
);
|
2023-06-26 18:44:14 +02:00
|
|
|
|
2022-02-18 20:55:10 +01:00
|
|
|
Ok(())
|
2021-08-16 05:20:49 +02:00
|
|
|
}
|
|
|
|
|
2021-08-19 15:55:52 +02:00
|
|
|
#[test]
|
|
|
|
fn test_should_save_and_load_manifest_encrypted_longer() -> anyhow::Result<()> {
|
2023-06-26 02:23:26 +02:00
|
|
|
let passkey = Some(SecretString::new("password".into()));
|
2022-02-18 20:55:10 +01:00
|
|
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
2021-08-19 15:55:52 +02:00
|
|
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
2023-06-22 22:20:15 +02:00
|
|
|
let mut manager = AccountManager::new(manifest_path.as_path());
|
2021-08-19 15:55:52 +02:00
|
|
|
let mut account = SteamGuardAccount::new();
|
|
|
|
account.account_name = "asdf1234".into();
|
2022-06-19 20:09:08 +02:00
|
|
|
account.revocation_code = String::from("R12345").into();
|
2021-08-25 03:13:16 +02:00
|
|
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
2022-06-19 20:09:08 +02:00
|
|
|
account.uri = String::from("otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj").into();
|
2021-08-19 15:55:52 +02:00
|
|
|
account.token_gid = "asdf1234".into();
|
2023-06-22 22:20:15 +02:00
|
|
|
manager.add_account(account);
|
|
|
|
manager.submit_passkey(passkey.clone());
|
2023-07-03 16:23:56 +02:00
|
|
|
manager.manifest.entries[0].encryption = Some(EncryptionScheme::generate());
|
2023-06-22 22:20:15 +02:00
|
|
|
manager.save()?;
|
|
|
|
|
|
|
|
let mut loaded_manager = AccountManager::load(manifest_path.as_path())?;
|
|
|
|
loaded_manager.submit_passkey(passkey);
|
|
|
|
assert_eq!(loaded_manager.manifest.entries.len(), 1);
|
2021-08-19 15:55:52 +02:00
|
|
|
assert_eq!(
|
2023-06-22 22:20:15 +02:00
|
|
|
loaded_manager.manifest.entries[0].filename,
|
|
|
|
"asdf1234.maFile"
|
|
|
|
);
|
|
|
|
loaded_manager.load_accounts()?;
|
|
|
|
assert_eq!(
|
|
|
|
loaded_manager.manifest.entries.len(),
|
|
|
|
loaded_manager.accounts.len()
|
2021-08-19 15:55:52 +02:00
|
|
|
);
|
2023-06-26 18:44:14 +02:00
|
|
|
let account_name = "asdf1234";
|
|
|
|
let account = loaded_manager.get_account(account_name)?;
|
|
|
|
let account = account.lock().unwrap();
|
|
|
|
assert_eq!(account.account_name, "asdf1234");
|
|
|
|
assert_eq!(account.revocation_code.expose_secret(), "R12345");
|
2021-08-19 15:55:52 +02:00
|
|
|
assert_eq!(
|
2023-06-26 18:44:14 +02:00
|
|
|
account.shared_secret,
|
2021-08-25 03:13:16 +02:00
|
|
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
|
|
|
)
|
|
|
|
.unwrap(),
|
2021-08-19 15:55:52 +02:00
|
|
|
);
|
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
Ok(())
|
2021-08-19 15:55:52 +02:00
|
|
|
}
|
|
|
|
|
2021-08-14 19:39:01 +02:00
|
|
|
#[test]
|
2022-02-18 20:55:10 +01:00
|
|
|
fn test_should_import() -> anyhow::Result<()> {
|
|
|
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
2021-08-14 19:39:01 +02:00
|
|
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
2023-06-22 22:20:15 +02:00
|
|
|
let mut manager = AccountManager::new(manifest_path.as_path());
|
2021-08-14 19:39:01 +02:00
|
|
|
let mut account = SteamGuardAccount::new();
|
|
|
|
account.account_name = "asdf1234".into();
|
2022-06-19 20:09:08 +02:00
|
|
|
account.revocation_code = String::from("R12345").into();
|
2021-08-25 03:13:16 +02:00
|
|
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
2023-06-22 22:20:15 +02:00
|
|
|
manager.add_account(account);
|
|
|
|
manager.save()?;
|
2022-02-18 20:55:10 +01:00
|
|
|
std::fs::remove_file(&manifest_path)?;
|
2021-08-14 19:39:01 +02:00
|
|
|
|
2023-06-22 22:20:15 +02:00
|
|
|
let mut loaded_manager = AccountManager::new(manifest_path.as_path());
|
2023-09-10 23:22:23 +02:00
|
|
|
assert!(loaded_manager
|
|
|
|
.import_account(
|
2022-06-19 17:43:37 +02:00
|
|
|
&tmp_dir
|
2021-08-14 19:39:01 +02:00
|
|
|
.path()
|
|
|
|
.join("asdf1234.maFile")
|
|
|
|
.into_os_string()
|
|
|
|
.into_string()
|
|
|
|
.unwrap()
|
2023-09-10 23:22:23 +02:00
|
|
|
)
|
|
|
|
.is_ok());
|
2021-08-14 19:39:01 +02:00
|
|
|
assert_eq!(
|
2023-06-22 22:20:15 +02:00
|
|
|
loaded_manager.manifest.entries.len(),
|
|
|
|
loaded_manager.accounts.len()
|
2021-08-14 19:39:01 +02:00
|
|
|
);
|
2023-06-26 18:44:14 +02:00
|
|
|
let account_name = "asdf1234";
|
|
|
|
let account = loaded_manager.get_account(account_name)?;
|
|
|
|
let account = account.lock().unwrap();
|
|
|
|
assert_eq!(account.account_name, "asdf1234");
|
|
|
|
assert_eq!(account.revocation_code.expose_secret(), "R12345");
|
2021-08-14 19:39:01 +02:00
|
|
|
assert_eq!(
|
2023-06-26 18:44:14 +02:00
|
|
|
account.shared_secret,
|
2021-08-25 03:13:16 +02:00
|
|
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
|
|
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
|
|
|
)
|
|
|
|
.unwrap(),
|
2021-08-14 19:39:01 +02:00
|
|
|
);
|
2022-02-18 20:55:10 +01:00
|
|
|
|
|
|
|
Ok(())
|
2021-08-14 19:39:01 +02:00
|
|
|
}
|
2021-08-15 02:47:29 +02:00
|
|
|
|
2023-06-26 18:44:14 +02:00
|
|
|
#[test]
|
|
|
|
fn should_load_manifest_v1() -> anyhow::Result<()> {
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Test {
|
|
|
|
manifest: &'static str,
|
|
|
|
passkey: Option<SecretString>,
|
|
|
|
}
|
|
|
|
let cases = vec![
|
|
|
|
Test {
|
|
|
|
manifest: "src/fixtures/maFiles/manifest-v1/1-account/manifest.json",
|
|
|
|
passkey: None,
|
|
|
|
},
|
2023-06-27 01:02:48 +02:00
|
|
|
Test {
|
|
|
|
manifest: "src/fixtures/maFiles/manifest-v1/1-account-encrypted/manifest.json",
|
|
|
|
passkey: Some(SecretString::new("password".into())),
|
|
|
|
},
|
2023-06-26 18:44:14 +02:00
|
|
|
Test {
|
|
|
|
manifest: "src/fixtures/maFiles/manifest-v1/2-account/manifest.json",
|
|
|
|
passkey: None,
|
|
|
|
},
|
|
|
|
Test {
|
|
|
|
manifest: "src/fixtures/maFiles/manifest-v1/missing-account-name/manifest.json",
|
|
|
|
passkey: None,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
for case in cases {
|
|
|
|
eprintln!("testing: {:?}", case);
|
|
|
|
let mut manager = AccountManager::load(Path::new(case.manifest))?;
|
|
|
|
manager.submit_passkey(case.passkey.clone());
|
|
|
|
manager.load_accounts()?;
|
|
|
|
assert_eq!(manager.manifest.version, CURRENT_MANIFEST_VERSION);
|
|
|
|
assert_eq!(manager.manifest.entries[0].account_name, "example");
|
|
|
|
assert_eq!(manager.manifest.entries[0].steam_id, 1234);
|
|
|
|
let account = manager.get_account("example").unwrap();
|
|
|
|
let account = account.lock().unwrap();
|
|
|
|
assert_eq!(account.account_name, "example");
|
|
|
|
assert_eq!(account.steam_id, 1234);
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-08-14 19:39:01 +02:00
|
|
|
}
|