Skip to content

Commit

Permalink
import: add support for importing steam mobile app v2 format, remove …
Browse files Browse the repository at this point in the history
…`--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
  • Loading branch information
dyc3 authored Aug 23, 2023
1 parent 40d81f3 commit 4348529
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/accountmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use thiserror::Error;
mod legacy;
pub mod manifest;
pub mod migrate;
mod steamv2;

pub use manifest::*;

Expand Down
44 changes: 37 additions & 7 deletions src/accountmanager/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::encryption::EncryptionScheme;
use super::{
legacy::{SdaAccount, SdaManifest},
manifest::ManifestV1,
steamv2::SteamMobileV2,
EntryLoader, Manifest,
};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
}
Expand All @@ -255,17 +261,36 @@ 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 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();
}

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)]
mod tests {
use crate::{accountmanager::CURRENT_MANIFEST_VERSION, AccountManager};
Expand Down Expand Up @@ -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);
}
Expand Down
73 changes: 73 additions & 0 deletions src/accountmanager/steamv2.rs
Original file line number Diff line number Diff line change
@@ -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")),
})
}
50 changes: 27 additions & 23 deletions src/commands/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}
Expand All @@ -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);
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/fixtures/maFiles/compat/steamv2/sample.maFile
Original file line number Diff line number Diff line change
@@ -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"
}

0 comments on commit 4348529

Please sign in to comment.