Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
Loading