import: add support for importing steam mobile app v2 format, remove --sda argument (#307)

- add account schema for steam app v2
- make it possible to import steam v2 accounts
- import: remove the `--sda` flag, the format should be detected
automatically
- add unit tests
- make it actually work

closes #305
This commit is contained in:
Carson McManus 2023-08-23 13:59:14 -04:00 committed by GitHub
parent 40d81f360b
commit 4348529ffd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 152 additions and 30 deletions

View file

@ -15,6 +15,7 @@ use thiserror::Error;
mod legacy; mod legacy;
pub mod manifest; pub mod manifest;
pub mod migrate; pub mod migrate;
mod steamv2;
pub use manifest::*; pub use manifest::*;

View file

@ -11,6 +11,7 @@ use crate::encryption::EncryptionScheme;
use super::{ use super::{
legacy::{SdaAccount, SdaManifest}, legacy::{SdaAccount, SdaManifest},
manifest::ManifestV1, manifest::ManifestV1,
steamv2::SteamMobileV2,
EntryLoader, Manifest, EntryLoader, Manifest,
}; };
@ -145,7 +146,11 @@ impl MigratingManifest {
errors errors
)); ));
} }
accounts.into_iter().map(MigratingAccount::Sda).collect() accounts
.into_iter()
.map(ExternalAccount::Sda)
.map(MigratingAccount::External)
.collect()
} }
Self::ManifestV1(manifest) => { Self::ManifestV1(manifest) => {
let (accounts, errors) = manifest let (accounts, errors) = manifest
@ -228,15 +233,16 @@ fn deserialize_manifest(
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum MigratingAccount { enum MigratingAccount {
Sda(SdaAccount), External(ExternalAccount),
ManifestV1(SteamGuardAccount), ManifestV1(SteamGuardAccount),
} }
impl MigratingAccount { impl MigratingAccount {
pub fn upgrade(self) -> Self { pub fn upgrade(self) -> Self {
match self { match self {
Self::Sda(sda) => Self::ManifestV1(sda.into()), Self::External(account) => Self::ManifestV1(account.into()),
Self::ManifestV1(_) => self, Self::ManifestV1(_) => self,
} }
} }
@ -255,10 +261,12 @@ impl From<MigratingAccount> for SteamGuardAccount {
} }
} }
pub fn load_and_upgrade_sda_account(path: &Path) -> anyhow::Result<SteamGuardAccount> { pub fn load_and_upgrade_external_account(path: &Path) -> anyhow::Result<SteamGuardAccount> {
let file = File::open(path)?; let file = File::open(path)?;
let account: SdaAccount = serde_json::from_reader(file)?; let mut deser = serde_json::Deserializer::from_reader(&file);
let mut account = MigratingAccount::Sda(account); 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() { while !account.is_latest() {
account = account.upgrade(); account = account.upgrade();
} }
@ -266,6 +274,23 @@ pub fn load_and_upgrade_sda_account(path: &Path) -> anyhow::Result<SteamGuardAcc
Ok(account.into()) Ok(account.into())
} }
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum ExternalAccount {
Sda(SdaAccount),
SteamMobileV2(SteamMobileV2),
}
impl From<ExternalAccount> for SteamGuardAccount {
fn from(account: ExternalAccount) -> Self {
match account {
ExternalAccount::Sda(account) => account.into(),
ExternalAccount::SteamMobileV2(account) => account.into(),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{accountmanager::CURRENT_MANIFEST_VERSION, AccountManager}; use crate::{accountmanager::CURRENT_MANIFEST_VERSION, AccountManager};
@ -360,10 +385,15 @@ mod tests {
account_name: "example", account_name: "example",
steam_id: 1234, steam_id: 1234,
}, },
Test {
mafile: "src/fixtures/maFiles/compat/steamv2/sample.maFile",
account_name: "afarihm",
steam_id: 76561199441992970,
},
]; ];
for case in cases { for case in cases {
eprintln!("testing: {:?}", case); 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.account_name, case.account_name);
assert_eq!(account.steam_id, case.steam_id); assert_eq!(account.steam_id, case.steam_id);
} }

View file

@ -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<serde_json::Value>,
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<serde_json::Value>,
pub steamguard_scheme: Option<serde_json::Value>,
}
impl From<SteamMobileV2> 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<u64, D::Error> {
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")),
})
}

View file

@ -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." 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 { 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\"")] #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")]
pub files: Vec<String>, pub files: Vec<String>,
} }
@ -25,26 +22,33 @@ where
fn execute(&self, _transport: T, manager: &mut AccountManager) -> anyhow::Result<()> { fn execute(&self, _transport: T, manager: &mut AccountManager) -> anyhow::Result<()> {
for file_path in self.files.iter() { for file_path in self.files.iter() {
debug!("loading entry: {:?}", file_path); debug!("loading entry: {:?}", file_path);
if self.sda { match manager.import_account(file_path) {
let path = Path::new(&file_path); Ok(_) => {
let account = crate::accountmanager::migrate::load_and_upgrade_sda_account(path)?; info!("Imported account: {}", &file_path);
manager.add_account(account); }
info!("Imported account: {}", &file_path); Err(ManifestAccountImportError::AlreadyExists { .. }) => {
} else { warn!("Account already exists: {} -- Ignoring", &file_path);
match manager.import_account(file_path) { }
Ok(_) => { Err(ManifestAccountImportError::DeserializationFailed(orig_err)) => {
info!("Imported account: {}", &file_path); debug!("Falling back to external account import",);
}
Err(ManifestAccountImportError::AlreadyExists { .. }) => { let path = Path::new(&file_path);
warn!("Account already exists: {} -- Ignoring", &file_path); let account =
} match crate::accountmanager::migrate::load_and_upgrade_external_account(
Err(ManifestAccountImportError::DeserializationFailed(err)) => { path,
warn!("Failed to import account: {} {}", &file_path, err); ) {
warn!("If this file came from SDA, try using --sda"); Ok(account) => account,
} Err(err) => {
Err(err) => { error!("Failed to import account: {} {}", &file_path, err);
bail!("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);
} }
} }
} }

View file

@ -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"
}