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:
parent
40d81f360b
commit
4348529ffd
5 changed files with 152 additions and 30 deletions
|
@ -15,6 +15,7 @@ use thiserror::Error;
|
|||
mod legacy;
|
||||
pub mod manifest;
|
||||
pub mod migrate;
|
||||
mod steamv2;
|
||||
|
||||
pub use manifest::*;
|
||||
|
||||
|
|
|
@ -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<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();
|
||||
}
|
||||
|
@ -266,6 +274,23 @@ pub fn load_and_upgrade_sda_account(path: &Path) -> anyhow::Result<SteamGuardAcc
|
|||
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};
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
73
src/accountmanager/steamv2.rs
Normal file
73
src/accountmanager/steamv2.rs
Normal 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")),
|
||||
})
|
||||
}
|
|
@ -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>,
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
src/fixtures/maFiles/compat/steamv2/sample.maFile
Normal file
14
src/fixtures/maFiles/compat/steamv2/sample.maFile
Normal 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"
|
||||
}
|
Loading…
Reference in a new issue