refactor: account manager no longer needs to have all accounts loaded in order to function
This commit is contained in:
parent
eb3c417ff2
commit
00ea8dca71
2 changed files with 278 additions and 151 deletions
|
@ -2,6 +2,7 @@ pub use crate::encryption::EntryEncryptionParams;
|
||||||
use crate::encryption::EntryEncryptor;
|
use crate::encryption::EntryEncryptor;
|
||||||
use log::*;
|
use log::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufReader, Read, Write};
|
use std::io::{BufReader, Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -28,9 +29,11 @@ pub struct Manifest {
|
||||||
pub auto_confirm_trades: bool,
|
pub auto_confirm_trades: bool,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
|
accounts: HashMap<String, Arc<Mutex<SteamGuardAccount>>>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
folder: String, // I wanted to use a Path here, but it was too hard to make it work...
|
folder: String, // I wanted to use a Path here, but it was too hard to make it work...
|
||||||
|
#[serde(skip)]
|
||||||
|
passkey: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
@ -56,8 +59,9 @@ impl Default for Manifest {
|
||||||
auto_confirm_market_transactions: false,
|
auto_confirm_market_transactions: false,
|
||||||
auto_confirm_trades: false,
|
auto_confirm_trades: false,
|
||||||
|
|
||||||
accounts: vec![],
|
accounts: HashMap::new(),
|
||||||
folder: "".into(),
|
folder: "".into(),
|
||||||
|
passkey: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,23 +84,44 @@ impl Manifest {
|
||||||
return Ok(manifest);
|
return Ok(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_accounts(
|
/// Tells the manifest to keep track of the encryption passkey, and use it for encryption when loading or saving accounts.
|
||||||
|
pub fn submit_passkey(&mut self, passkey: Option<String>) {
|
||||||
|
if passkey.is_some() {
|
||||||
|
debug!("passkey was submitted to manifest");
|
||||||
|
} else {
|
||||||
|
debug!("passkey was revoked from manifest");
|
||||||
|
}
|
||||||
|
self.passkey = passkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_accounts(&mut self) -> anyhow::Result<(), ManifestAccountLoadError> {
|
||||||
|
let account_names: Vec<String> = self
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.account_name.clone())
|
||||||
|
.collect();
|
||||||
|
for account_name in account_names {
|
||||||
|
self.load_account(&account_name)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_account(
|
||||||
&mut self,
|
&mut self,
|
||||||
passkey: &Option<String>,
|
account_name: &String,
|
||||||
) -> anyhow::Result<(), ManifestAccountLoadError> {
|
) -> anyhow::Result<(), ManifestAccountLoadError> {
|
||||||
for entry in &mut self.entries {
|
let mut entry = self.get_entry_mut(account_name)?.clone();
|
||||||
let path = Path::new(&self.folder).join(&entry.filename);
|
let path = Path::new(&self.folder).join(&entry.filename);
|
||||||
debug!("loading account: {:?}", path);
|
debug!("loading account: {:?}", path);
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let mut reader = BufReader::new(file);
|
let mut reader = BufReader::new(file);
|
||||||
let account: SteamGuardAccount;
|
let account: SteamGuardAccount;
|
||||||
match (passkey, entry.encryption.as_ref()) {
|
match (&self.passkey, entry.encryption.as_ref()) {
|
||||||
(Some(passkey), Some(params)) => {
|
(Some(passkey), Some(params)) => {
|
||||||
let mut ciphertext: Vec<u8> = vec![];
|
let mut ciphertext: Vec<u8> = vec![];
|
||||||
reader.read_to_end(&mut ciphertext)?;
|
reader.read_to_end(&mut ciphertext)?;
|
||||||
let plaintext = crate::encryption::LegacySdaCompatible::decrypt(
|
let plaintext =
|
||||||
passkey, params, ciphertext,
|
crate::encryption::LegacySdaCompatible::decrypt(&passkey, params, ciphertext)?;
|
||||||
)?;
|
|
||||||
if plaintext[0] != '{' as u8 && plaintext[plaintext.len() - 1] != '}' as u8 {
|
if plaintext[0] != '{' as u8 && plaintext[plaintext.len() - 1] != '}' as u8 {
|
||||||
return Err(ManifestAccountLoadError::IncorrectPasskey);
|
return Err(ManifestAccountLoadError::IncorrectPasskey);
|
||||||
}
|
}
|
||||||
|
@ -111,8 +136,9 @@ impl Manifest {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
entry.account_name = account.account_name.clone();
|
entry.account_name = account.account_name.clone();
|
||||||
self.accounts.push(Arc::new(Mutex::new(account)));
|
self.accounts
|
||||||
}
|
.insert(entry.account_name.clone(), Arc::new(Mutex::new(account)));
|
||||||
|
*self.get_entry_mut(account_name)? = entry;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +160,8 @@ impl Manifest {
|
||||||
account_name: account.account_name.clone(),
|
account_name: account.account_name.clone(),
|
||||||
encryption: None,
|
encryption: None,
|
||||||
});
|
});
|
||||||
self.accounts.push(Arc::new(Mutex::new(account)));
|
self.accounts
|
||||||
|
.insert(account.account_name.clone(), Arc::new(Mutex::new(account)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_account(&mut self, import_path: String) -> anyhow::Result<()> {
|
pub fn import_account(&mut self, import_path: String) -> anyhow::Result<()> {
|
||||||
|
@ -156,33 +183,36 @@ impl Manifest {
|
||||||
|
|
||||||
pub fn remove_account(&mut self, account_name: String) {
|
pub fn remove_account(&mut self, account_name: String) {
|
||||||
let index = self
|
let index = self
|
||||||
.accounts
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|a| a.lock().unwrap().account_name == account_name)
|
.position(|a| a.account_name == account_name)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.accounts.remove(index);
|
self.accounts.remove(&account_name);
|
||||||
self.entries.remove(index);
|
self.entries.remove(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self, passkey: &Option<String>) -> anyhow::Result<()> {
|
/// Saves the manifest and all loaded accounts.
|
||||||
ensure!(
|
pub fn save(&self) -> anyhow::Result<()> {
|
||||||
self.entries.len() == self.accounts.len(),
|
|
||||||
"Manifest entries don't match accounts."
|
|
||||||
);
|
|
||||||
info!("Saving manifest and accounts...");
|
info!("Saving manifest and accounts...");
|
||||||
for (entry, account) in self.entries.iter().zip(&self.accounts) {
|
for account in self
|
||||||
|
.accounts
|
||||||
|
.values()
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.clone().lock().unwrap().clone())
|
||||||
|
{
|
||||||
|
let entry = self.get_entry(&account.account_name)?.clone();
|
||||||
debug!("saving {}", entry.filename);
|
debug!("saving {}", entry.filename);
|
||||||
let serialized = serde_json::to_vec(account.as_ref())?;
|
let serialized = serde_json::to_vec(&account)?;
|
||||||
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>;
|
let final_buffer: Vec<u8>;
|
||||||
match (passkey, entry.encryption.as_ref()) {
|
match (&self.passkey, entry.encryption.as_ref()) {
|
||||||
(Some(passkey), Some(params)) => {
|
(Some(passkey), Some(params)) => {
|
||||||
final_buffer = crate::encryption::LegacySdaCompatible::encrypt(
|
final_buffer = crate::encryption::LegacySdaCompatible::encrypt(
|
||||||
passkey, params, serialized,
|
&passkey, params, serialized,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
(None, Some(_)) => {
|
(None, Some(_)) => {
|
||||||
|
@ -206,10 +236,68 @@ impl Manifest {
|
||||||
file.sync_data()?;
|
file.sync_data()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return all loaded accounts. Order is not guarenteed.
|
||||||
|
pub fn get_all_loaded(&self) -> Vec<Arc<Mutex<SteamGuardAccount>>> {
|
||||||
|
return self.accounts.values().cloned().into_iter().collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_entry(
|
||||||
|
&self,
|
||||||
|
account_name: &String,
|
||||||
|
) -> anyhow::Result<&ManifestEntry, ManifestAccountLoadError> {
|
||||||
|
self.entries
|
||||||
|
.iter()
|
||||||
|
.find(|e| &e.account_name == account_name)
|
||||||
|
.ok_or(ManifestAccountLoadError::MissingManifestEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_entry_mut(
|
||||||
|
&mut self,
|
||||||
|
account_name: &String,
|
||||||
|
) -> anyhow::Result<&mut ManifestEntry, ManifestAccountLoadError> {
|
||||||
|
self.entries
|
||||||
|
.iter_mut()
|
||||||
|
.find(|e| &e.account_name == account_name)
|
||||||
|
.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,
|
||||||
|
account_name: &String,
|
||||||
|
) -> anyhow::Result<Arc<Mutex<SteamGuardAccount>>> {
|
||||||
|
let account = self
|
||||||
|
.accounts
|
||||||
|
.get(account_name)
|
||||||
|
.map(|a| a.clone())
|
||||||
|
.ok_or(anyhow!("Account not loaded"));
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or load the spcified account.
|
||||||
|
pub fn get_or_load_account(
|
||||||
|
&mut self,
|
||||||
|
account_name: &String,
|
||||||
|
) -> anyhow::Result<Arc<Mutex<SteamGuardAccount>>, ManifestAccountLoadError> {
|
||||||
|
let account = self.get_account(account_name);
|
||||||
|
if account.is_ok() {
|
||||||
|
return Ok(account.unwrap());
|
||||||
|
}
|
||||||
|
self.load_account(&account_name)?;
|
||||||
|
return Ok(self.get_account(account_name)?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ManifestAccountLoadError {
|
pub enum ManifestAccountLoadError {
|
||||||
|
#[error("Could not find an entry in the manifest for this account. Check your spelling.")]
|
||||||
|
MissingManifestEntry,
|
||||||
#[error("Manifest accounts are encrypted, but no passkey was provided.")]
|
#[error("Manifest accounts are encrypted, but no passkey was provided.")]
|
||||||
MissingPasskey,
|
MissingPasskey,
|
||||||
#[error("Incorrect passkey provided.")]
|
#[error("Incorrect passkey provided.")]
|
||||||
|
@ -253,7 +341,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(&None), Ok(_)));
|
assert!(matches!(manifest.save(), Ok(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -269,26 +357,39 @@ mod tests {
|
||||||
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
||||||
)?;
|
)?;
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
manifest.save(&None)?;
|
manifest.save()?;
|
||||||
|
|
||||||
let mut loaded_manifest = Manifest::load(manifest_path.as_path())?;
|
let mut loaded_manifest = Manifest::load(manifest_path.as_path())?;
|
||||||
assert_eq!(loaded_manifest.entries.len(), 1);
|
assert_eq!(loaded_manifest.entries.len(), 1);
|
||||||
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
||||||
loaded_manifest.load_accounts(&None)?;
|
loaded_manifest.load_accounts()?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.entries.len(),
|
loaded_manifest.entries.len(),
|
||||||
loaded_manifest.accounts.len()
|
loaded_manifest.accounts.len()
|
||||||
);
|
);
|
||||||
|
let account_name = "asdf1234".into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().account_name,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.account_name,
|
||||||
"asdf1234"
|
"asdf1234"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().revocation_code,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.revocation_code,
|
||||||
"R12345"
|
"R12345"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.shared_secret,
|
||||||
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
)?,
|
)?,
|
||||||
|
@ -297,9 +398,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_save_and_load_manifest_encrypted() {
|
fn test_should_save_and_load_manifest_encrypted() -> anyhow::Result<()> {
|
||||||
let passkey: Option<String> = Some("password".into());
|
let passkey = Some("password".into());
|
||||||
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
||||||
let manifest_path = tmp_dir.path().join("manifest.json");
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
||||||
let mut manifest = Manifest::new(manifest_path.as_path());
|
let mut manifest = Manifest::new(manifest_path.as_path());
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
|
@ -307,41 +408,60 @@ mod tests {
|
||||||
account.revocation_code = "R12345".into();
|
account.revocation_code = "R12345".into();
|
||||||
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
|
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
|
||||||
assert!(matches!(manifest.save(&passkey), Ok(_)));
|
manifest.submit_passkey(passkey.clone());
|
||||||
|
assert!(matches!(manifest.save(), Ok(_)));
|
||||||
|
|
||||||
let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap();
|
let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap();
|
||||||
|
loaded_manifest.submit_passkey(passkey);
|
||||||
assert_eq!(loaded_manifest.entries.len(), 1);
|
assert_eq!(loaded_manifest.entries.len(), 1);
|
||||||
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
||||||
assert!(matches!(loaded_manifest.load_accounts(&passkey), Ok(_)));
|
let _r = loaded_manifest.load_accounts();
|
||||||
|
if _r.is_err() {
|
||||||
|
eprintln!("{:?}", _r);
|
||||||
|
}
|
||||||
|
assert!(matches!(_r, Ok(_)));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.entries.len(),
|
loaded_manifest.entries.len(),
|
||||||
loaded_manifest.accounts.len()
|
loaded_manifest.accounts.len()
|
||||||
);
|
);
|
||||||
|
let account_name = "asdf1234".into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().account_name,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.account_name,
|
||||||
"asdf1234"
|
"asdf1234"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().revocation_code,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.revocation_code,
|
||||||
"R12345"
|
"R12345"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.shared_secret,
|
||||||
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_save_and_load_manifest_encrypted_longer() -> anyhow::Result<()> {
|
fn test_should_save_and_load_manifest_encrypted_longer() -> anyhow::Result<()> {
|
||||||
let passkey: Option<String> = Some("password".into());
|
let passkey = Some("password".into());
|
||||||
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
||||||
let manifest_path = tmp_dir.path().join("manifest.json");
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
||||||
let mut manifest = Manifest::new(manifest_path.as_path());
|
let mut manifest = Manifest::new(manifest_path.as_path());
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
|
@ -354,27 +474,42 @@ mod tests {
|
||||||
account.uri = "otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj".into();
|
account.uri = "otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj".into();
|
||||||
account.token_gid = "asdf1234".into();
|
account.token_gid = "asdf1234".into();
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
|
manifest.submit_passkey(passkey.clone());
|
||||||
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
|
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
|
||||||
manifest.save(&passkey)?;
|
manifest.save()?;
|
||||||
|
|
||||||
let mut loaded_manifest = Manifest::load(manifest_path.as_path())?;
|
let mut loaded_manifest = Manifest::load(manifest_path.as_path())?;
|
||||||
|
loaded_manifest.submit_passkey(passkey.clone());
|
||||||
assert_eq!(loaded_manifest.entries.len(), 1);
|
assert_eq!(loaded_manifest.entries.len(), 1);
|
||||||
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
||||||
loaded_manifest.load_accounts(&passkey)?;
|
loaded_manifest.load_accounts()?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.entries.len(),
|
loaded_manifest.entries.len(),
|
||||||
loaded_manifest.accounts.len()
|
loaded_manifest.accounts.len()
|
||||||
);
|
);
|
||||||
|
let account_name = "asdf1234".into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().account_name,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.account_name,
|
||||||
"asdf1234"
|
"asdf1234"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().revocation_code,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.revocation_code,
|
||||||
"R12345"
|
"R12345"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.shared_secret,
|
||||||
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
)
|
)
|
||||||
|
@ -385,8 +520,8 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_import() {
|
fn test_should_import() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
||||||
let manifest_path = tmp_dir.path().join("manifest.json");
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
||||||
let mut manifest = Manifest::new(manifest_path.as_path());
|
let mut manifest = Manifest::new(manifest_path.as_path());
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
|
@ -397,8 +532,8 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
assert!(matches!(manifest.save(&None), Ok(_)));
|
manifest.save()?;
|
||||||
std::fs::remove_file(&manifest_path).unwrap();
|
std::fs::remove_file(&manifest_path)?;
|
||||||
|
|
||||||
let mut loaded_manifest = Manifest::new(manifest_path.as_path());
|
let mut loaded_manifest = Manifest::new(manifest_path.as_path());
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
@ -416,104 +551,94 @@ mod tests {
|
||||||
loaded_manifest.entries.len(),
|
loaded_manifest.entries.len(),
|
||||||
loaded_manifest.accounts.len()
|
loaded_manifest.accounts.len()
|
||||||
);
|
);
|
||||||
|
let account_name = "asdf1234".into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().account_name,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.account_name,
|
||||||
"asdf1234"
|
"asdf1234"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().revocation_code,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.revocation_code,
|
||||||
"R12345"
|
"R12345"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest
|
||||||
|
.get_account(&account_name)?
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.shared_secret,
|
||||||
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sda_compatibility_1() {
|
fn test_sda_compatibility_1() -> anyhow::Result<()> {
|
||||||
let path = Path::new("src/fixtures/maFiles/compat/1-account/manifest.json");
|
let path = Path::new("src/fixtures/maFiles/compat/1-account/manifest.json");
|
||||||
assert!(path.is_file());
|
assert!(path.is_file());
|
||||||
let result = Manifest::load(path);
|
let mut manifest = Manifest::load(path)?;
|
||||||
assert!(matches!(result, Ok(_)));
|
|
||||||
let mut manifest = result.unwrap();
|
|
||||||
assert!(matches!(manifest.entries.last().unwrap().encryption, None));
|
assert!(matches!(manifest.entries.last().unwrap().encryption, None));
|
||||||
assert!(matches!(manifest.load_accounts(&None), Ok(_)));
|
manifest.load_accounts()?;
|
||||||
|
let account_name = manifest.entries.last().unwrap().account_name.clone();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
manifest.entries.last().unwrap().account_name,
|
account_name,
|
||||||
manifest
|
manifest
|
||||||
.accounts
|
.get_account(&account_name)?
|
||||||
.last()
|
|
||||||
.unwrap()
|
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.account_name
|
.account_name
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sda_compatibility_1_encrypted() {
|
fn test_sda_compatibility_1_encrypted() -> anyhow::Result<()> {
|
||||||
let path = Path::new("src/fixtures/maFiles/compat/1-account-encrypted/manifest.json");
|
let path = Path::new("src/fixtures/maFiles/compat/1-account-encrypted/manifest.json");
|
||||||
assert!(path.is_file());
|
assert!(path.is_file());
|
||||||
let result = Manifest::load(path);
|
let mut manifest = Manifest::load(path)?;
|
||||||
assert!(matches!(result, Ok(_)));
|
|
||||||
let mut manifest = result.unwrap();
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
manifest.entries.last().unwrap().encryption,
|
manifest.entries.last().unwrap().encryption,
|
||||||
Some(_)
|
Some(_)
|
||||||
));
|
));
|
||||||
let result = manifest.load_accounts(&Some("password".into()));
|
manifest.submit_passkey(Some("password".into()));
|
||||||
assert!(
|
manifest.load_accounts()?;
|
||||||
matches!(result, Ok(_)),
|
let account_name = manifest.entries.last().unwrap().account_name.clone();
|
||||||
"error when loading accounts: {:?}",
|
|
||||||
result.unwrap_err()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
manifest.entries.last().unwrap().account_name,
|
account_name,
|
||||||
manifest
|
manifest
|
||||||
.accounts
|
.get_account(&account_name)?
|
||||||
.last()
|
|
||||||
.unwrap()
|
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.account_name
|
.account_name
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sda_compatibility_no_webcookie() {
|
fn test_sda_compatibility_no_webcookie() -> anyhow::Result<()> {
|
||||||
let path = Path::new("src/fixtures/maFiles/compat/no-webcookie/manifest.json");
|
let path = Path::new("src/fixtures/maFiles/compat/no-webcookie/manifest.json");
|
||||||
assert!(path.is_file());
|
assert!(path.is_file());
|
||||||
let result = Manifest::load(path);
|
let mut manifest = Manifest::load(path)?;
|
||||||
assert!(matches!(result, Ok(_)));
|
|
||||||
let mut manifest = result.unwrap();
|
|
||||||
assert!(matches!(manifest.entries.last().unwrap().encryption, None));
|
assert!(matches!(manifest.entries.last().unwrap().encryption, None));
|
||||||
assert!(matches!(manifest.load_accounts(&None), Ok(_)));
|
assert!(matches!(manifest.load_accounts(), Ok(_)));
|
||||||
|
let account_name = manifest.entries.last().unwrap().account_name.clone();
|
||||||
|
let account = manifest.get_account(&account_name)?;
|
||||||
|
assert_eq!(account_name, account.lock().unwrap().account_name);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
manifest.entries.last().unwrap().account_name,
|
account.lock().unwrap().session.as_ref().unwrap().web_cookie,
|
||||||
manifest
|
|
||||||
.accounts
|
|
||||||
.last()
|
|
||||||
.unwrap()
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.account_name
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
manifest
|
|
||||||
.accounts
|
|
||||||
.last()
|
|
||||||
.unwrap()
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.session
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.web_cookie,
|
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
54
src/main.rs
54
src/main.rs
|
@ -190,24 +190,26 @@ fn run() -> anyhow::Result<()> {
|
||||||
std::fs::create_dir_all(mafiles_dir)?;
|
std::fs::create_dir_all(mafiles_dir)?;
|
||||||
|
|
||||||
manifest = accountmanager::Manifest::new(path.as_path());
|
manifest = accountmanager::Manifest::new(path.as_path());
|
||||||
manifest.save(&None)?;
|
manifest.save()?;
|
||||||
} else {
|
} else {
|
||||||
manifest = accountmanager::Manifest::load(path.as_path())?;
|
manifest = accountmanager::Manifest::load(path.as_path())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut passkey: Option<String> = matches.value_of("passkey").map(|s| s.into());
|
let mut passkey: Option<String> = matches.value_of("passkey").map(|s| s.into());
|
||||||
|
manifest.submit_passkey(passkey);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match manifest.load_accounts(&passkey) {
|
match manifest.load_accounts() {
|
||||||
Ok(_) => break,
|
Ok(_) => break,
|
||||||
Err(
|
Err(
|
||||||
accountmanager::ManifestAccountLoadError::MissingPasskey
|
accountmanager::ManifestAccountLoadError::MissingPasskey
|
||||||
| accountmanager::ManifestAccountLoadError::IncorrectPasskey,
|
| accountmanager::ManifestAccountLoadError::IncorrectPasskey,
|
||||||
) => {
|
) => {
|
||||||
if passkey.is_some() {
|
if manifest.has_passkey() {
|
||||||
error!("Incorrect passkey");
|
error!("Incorrect passkey");
|
||||||
}
|
}
|
||||||
passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok();
|
passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok();
|
||||||
|
manifest.submit_passkey(passkey);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Could not load accounts: {}", e);
|
error!("Could not load accounts: {}", e);
|
||||||
|
@ -220,6 +222,7 @@ fn run() -> anyhow::Result<()> {
|
||||||
println!("Log in to the account that you want to link to steamguard-cli");
|
println!("Log in to the account that you want to link to steamguard-cli");
|
||||||
print!("Username: ");
|
print!("Username: ");
|
||||||
let username = tui::prompt();
|
let username = tui::prompt();
|
||||||
|
let account_name = username.clone();
|
||||||
if manifest.account_exists(&username) {
|
if manifest.account_exists(&username) {
|
||||||
bail!(
|
bail!(
|
||||||
"Account {} already exists in manifest, remove it first",
|
"Account {} already exists in manifest, remove it first",
|
||||||
|
@ -264,26 +267,20 @@ fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
match manifest.save(&passkey) {
|
match manifest.save() {
|
||||||
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);
|
||||||
println!(
|
println!(
|
||||||
"Just in case, here is the account info. Save it somewhere just in case!\n{:?}",
|
"Just in case, here is the account info. Save it somewhere just in case!\n{:?}",
|
||||||
manifest.accounts.last().unwrap().lock().unwrap()
|
manifest.get_account(&account_name).unwrap().lock().unwrap()
|
||||||
);
|
);
|
||||||
return Err(err.into());
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut account = manifest
|
let account_arc = manifest.get_account(&account_name).unwrap();
|
||||||
.accounts
|
let mut account = account_arc.lock().unwrap();
|
||||||
.last()
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.lock()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code);
|
println!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code);
|
||||||
tui::pause();
|
tui::pause();
|
||||||
|
@ -311,7 +308,7 @@ fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Authenticator finalized.");
|
println!("Authenticator finalized.");
|
||||||
match manifest.save(&None) {
|
match manifest.save() {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!(
|
println!(
|
||||||
|
@ -340,10 +337,10 @@ fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest.save(&passkey)?;
|
manifest.save()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if matches.is_present("encrypt") {
|
} else if matches.is_present("encrypt") {
|
||||||
if passkey.is_none() {
|
if !manifest.has_passkey() {
|
||||||
loop {
|
loop {
|
||||||
passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok();
|
passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok();
|
||||||
let passkey_confirm =
|
let passkey_confirm =
|
||||||
|
@ -353,34 +350,39 @@ fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
error!("Passkeys do not match, try again.");
|
error!("Passkeys do not match, try again.");
|
||||||
}
|
}
|
||||||
|
manifest.submit_passkey(passkey);
|
||||||
}
|
}
|
||||||
for entry in &mut manifest.entries {
|
for entry in &mut manifest.entries {
|
||||||
entry.encryption = Some(accountmanager::EntryEncryptionParams::generate());
|
entry.encryption = Some(accountmanager::EntryEncryptionParams::generate());
|
||||||
}
|
}
|
||||||
manifest.save(&passkey)?;
|
manifest.save()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if matches.is_present("decrypt") {
|
} else if matches.is_present("decrypt") {
|
||||||
for entry in &mut manifest.entries {
|
for entry in &mut manifest.entries {
|
||||||
entry.encryption = None;
|
entry.encryption = None;
|
||||||
}
|
}
|
||||||
manifest.save(&passkey)?;
|
manifest.submit_passkey(None);
|
||||||
|
manifest.save()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
||||||
if matches.is_present("all") {
|
if matches.is_present("all") {
|
||||||
|
manifest
|
||||||
|
.load_accounts()
|
||||||
|
.expect("Failed to load all requested accounts, aborting");
|
||||||
// manifest.accounts.iter().map(|a| selected_accounts.push(a.b));
|
// manifest.accounts.iter().map(|a| selected_accounts.push(a.b));
|
||||||
for account in &manifest.accounts {
|
for entry in &manifest.entries {
|
||||||
selected_accounts.push(account.clone());
|
selected_accounts.push(manifest.get_account(&entry.account_name).unwrap().clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for account in &manifest.accounts {
|
for entry in &manifest.entries {
|
||||||
if !matches.is_present("username") {
|
if !matches.is_present("username") {
|
||||||
selected_accounts.push(account.clone());
|
selected_accounts.push(manifest.get_account(&entry.account_name).unwrap().clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if matches.value_of("username").unwrap() == account.lock().unwrap().account_name {
|
if matches.value_of("username").unwrap() == entry.account_name {
|
||||||
selected_accounts.push(account.clone());
|
selected_accounts.push(manifest.get_account(&entry.account_name).unwrap().clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -471,7 +473,7 @@ fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest.save(&passkey)?;
|
manifest.save()?;
|
||||||
} 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: {}",
|
||||||
|
@ -528,7 +530,7 @@ fn run() -> anyhow::Result<()> {
|
||||||
manifest.remove_account(account_name);
|
manifest.remove_account(account_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest.save(&passkey)?;
|
manifest.save()?;
|
||||||
} else {
|
} else {
|
||||||
let server_time = steamapi::get_server_time();
|
let server_time = steamapi::get_server_time();
|
||||||
debug!("Time used to generate codes: {}", server_time);
|
debug!("Time used to generate codes: {}", server_time);
|
||||||
|
|
Loading…
Reference in a new issue