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;
|
mod legacy;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub mod migrate;
|
pub mod migrate;
|
||||||
|
mod steamv2;
|
||||||
|
|
||||||
pub use manifest::*;
|
pub use manifest::*;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
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."
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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