diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 2c03ac1..969fbd3 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -15,6 +15,7 @@ use thiserror::Error; mod legacy; pub mod manifest; pub mod migrate; +mod steamv2; pub use manifest::*; diff --git a/src/accountmanager/migrate.rs b/src/accountmanager/migrate.rs index 55cddec..bf14ffe 100644 --- a/src/accountmanager/migrate.rs +++ b/src/accountmanager/migrate.rs @@ -11,6 +11,7 @@ use crate::encryption::EncryptionScheme; use super::{ legacy::{SdaAccount, SdaManifest}, manifest::ManifestV1, + steamv2::SteamMobileV2, EntryLoader, Manifest, }; @@ -145,7 +146,11 @@ impl MigratingManifest { errors )); } - accounts.into_iter().map(MigratingAccount::Sda).collect() + accounts + .into_iter() + .map(ExternalAccount::Sda) + .map(MigratingAccount::External) + .collect() } Self::ManifestV1(manifest) => { let (accounts, errors) = manifest @@ -228,15 +233,16 @@ fn deserialize_manifest( } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum MigratingAccount { - Sda(SdaAccount), + External(ExternalAccount), ManifestV1(SteamGuardAccount), } impl MigratingAccount { pub fn upgrade(self) -> Self { match self { - Self::Sda(sda) => Self::ManifestV1(sda.into()), + Self::External(account) => Self::ManifestV1(account.into()), Self::ManifestV1(_) => self, } } @@ -255,10 +261,12 @@ impl From for SteamGuardAccount { } } -pub fn load_and_upgrade_sda_account(path: &Path) -> anyhow::Result { +pub fn load_and_upgrade_external_account(path: &Path) -> anyhow::Result { let file = File::open(path)?; - let account: SdaAccount = serde_json::from_reader(file)?; - let mut account = MigratingAccount::Sda(account); + let mut deser = serde_json::Deserializer::from_reader(&file); + let account: ExternalAccount = serde_path_to_error::deserialize(&mut deser) + .map_err(|err| anyhow::anyhow!("Failed to deserialize account: {}", err))?; + let mut account = MigratingAccount::External(account); while !account.is_latest() { account = account.upgrade(); } @@ -266,6 +274,23 @@ pub fn load_and_upgrade_sda_account(path: &Path) -> anyhow::Result for SteamGuardAccount { + fn from(account: ExternalAccount) -> Self { + match account { + ExternalAccount::Sda(account) => account.into(), + ExternalAccount::SteamMobileV2(account) => account.into(), + } + } +} + #[cfg(test)] mod tests { use crate::{accountmanager::CURRENT_MANIFEST_VERSION, AccountManager}; @@ -360,10 +385,15 @@ mod tests { account_name: "example", steam_id: 1234, }, + Test { + mafile: "src/fixtures/maFiles/compat/steamv2/sample.maFile", + account_name: "afarihm", + steam_id: 76561199441992970, + }, ]; for case in cases { eprintln!("testing: {:?}", case); - let account = load_and_upgrade_sda_account(Path::new(case.mafile))?; + let account = load_and_upgrade_external_account(Path::new(case.mafile))?; assert_eq!(account.account_name, case.account_name); assert_eq!(account.steam_id, case.steam_id); } diff --git a/src/accountmanager/steamv2.rs b/src/accountmanager/steamv2.rs new file mode 100644 index 0000000..dea3e6f --- /dev/null +++ b/src/accountmanager/steamv2.rs @@ -0,0 +1,73 @@ +use secrecy::SecretString; +use serde::{Deserialize, Deserializer}; +use serde_json::Value; +use steamguard::{token::TwoFactorSecret, SteamGuardAccount}; +use uuid::Uuid; + +/// Defines the schema for loading steamguard accounts extracted from backups of the official Steam app (v2). +/// +/// ```json +/// { +/// "steamid": "X", +/// "shared_secret": "X", +/// "serial_number": "X", +/// "revocation_code": "X", +/// "uri": "otpauth:\/\/totp\/Steam:USERNAME?secret=X&issuer=Steam", +/// "server_time": "X", +/// "account_name": "USERNAME", +/// "token_gid": "X", +/// "identity_secret": "X", +/// "secret_1": "X", +/// "status": 1, +/// "steamguard_scheme": "2" +/// } +/// ``` +#[derive(Debug, Clone, Deserialize)] +pub struct SteamMobileV2 { + #[serde(deserialize_with = "de_parse_number")] + pub steamid: u64, + pub shared_secret: TwoFactorSecret, + pub serial_number: String, + #[serde(with = "crate::secret_string")] + pub revocation_code: SecretString, + #[serde(with = "crate::secret_string")] + pub uri: SecretString, + pub server_time: Option, + pub account_name: String, + pub token_gid: String, + #[serde(with = "crate::secret_string")] + pub identity_secret: SecretString, + #[serde(with = "crate::secret_string")] + pub secret_1: SecretString, + pub status: Option, + pub steamguard_scheme: Option, +} + +impl From for SteamGuardAccount { + fn from(account: SteamMobileV2) -> Self { + Self { + shared_secret: account.shared_secret, + identity_secret: account.identity_secret, + revocation_code: account.revocation_code, + uri: account.uri, + account_name: account.account_name, + token_gid: account.token_gid, + serial_number: account.serial_number, + steam_id: account.steamid, + // device_id is unknown, so we just make one up + device_id: format!("android:{}", Uuid::new_v4()), + secret_1: account.secret_1, + tokens: None, + } + } +} + +fn de_parse_number<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + Ok(match Value::deserialize(deserializer)? { + Value::String(s) => s.parse().map_err(serde::de::Error::custom)?, + Value::Number(num) => num + .as_u64() + .ok_or(serde::de::Error::custom("Invalid number"))?, + _ => return Err(serde::de::Error::custom("wrong type")), + }) +} diff --git a/src/commands/import.rs b/src/commands/import.rs index 4d5850a..93c9055 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -11,9 +11,6 @@ use super::*; about = "Import an account with steamguard already set up. It must not be encrypted. If you haven't used steamguard-cli before, you probably don't need to use this command." )] pub struct ImportCommand { - #[clap(long, help = "Whether or not the provided maFiles are from SDA.")] - pub sda: bool, - #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] pub files: Vec, } @@ -25,26 +22,33 @@ where fn execute(&self, _transport: T, manager: &mut AccountManager) -> anyhow::Result<()> { for file_path in self.files.iter() { debug!("loading entry: {:?}", file_path); - if self.sda { - let path = Path::new(&file_path); - let account = crate::accountmanager::migrate::load_and_upgrade_sda_account(path)?; - manager.add_account(account); - info!("Imported account: {}", &file_path); - } else { - match manager.import_account(file_path) { - Ok(_) => { - info!("Imported account: {}", &file_path); - } - Err(ManifestAccountImportError::AlreadyExists { .. }) => { - warn!("Account already exists: {} -- Ignoring", &file_path); - } - Err(ManifestAccountImportError::DeserializationFailed(err)) => { - warn!("Failed to import account: {} {}", &file_path, err); - warn!("If this file came from SDA, try using --sda"); - } - Err(err) => { - bail!("Failed to import account: {} {}", &file_path, err); - } + match manager.import_account(file_path) { + Ok(_) => { + info!("Imported account: {}", &file_path); + } + Err(ManifestAccountImportError::AlreadyExists { .. }) => { + warn!("Account already exists: {} -- Ignoring", &file_path); + } + Err(ManifestAccountImportError::DeserializationFailed(orig_err)) => { + debug!("Falling back to external account import",); + + let path = Path::new(&file_path); + let account = + match crate::accountmanager::migrate::load_and_upgrade_external_account( + path, + ) { + Ok(account) => account, + Err(err) => { + error!("Failed to import account: {} {}", &file_path, err); + error!("The original error was: {}", orig_err); + continue; + } + }; + manager.add_account(account); + info!("Imported account: {}", &file_path); + } + Err(err) => { + bail!("Failed to import account: {} {}", &file_path, err); } } } diff --git a/src/fixtures/maFiles/compat/steamv2/sample.maFile b/src/fixtures/maFiles/compat/steamv2/sample.maFile new file mode 100644 index 0000000..e83fff1 --- /dev/null +++ b/src/fixtures/maFiles/compat/steamv2/sample.maFile @@ -0,0 +1,14 @@ +{ + "steamid": "76561199441992970", + "shared_secret": "kSJa7hfbr8IvReG9/1Ax13BhTJA=", + "serial_number": "5182004572898897156", + "revocation_code": "R52260", + "uri": "otpauth://totp/Steam:afarihm?secret=SERFV3QX3OX4EL2F4G676UBR25YGCTEQ&issuer=Steam", + "server_time": "123", + "account_name": "afarihm", + "token_gid": "2d5a1b6cdbbfa9cc", + "identity_secret": "f62XbJcml4r1j3NcFm0GGTtmcXw=", + "secret_1": "BEelQHBr74ahsgiJbGArNV62/Bs=", + "status": 1, + "steamguard_scheme": "2" +}