import: add support for importing winauth exports (#331)

closes #295
This commit is contained in:
Carson McManus 2023-10-08 11:10:46 -04:00 committed by GitHub
parent 6d4915a39c
commit 94a3210f09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 110 additions and 18 deletions

View file

@ -16,6 +16,7 @@ mod legacy;
pub mod manifest; pub mod manifest;
pub mod migrate; pub mod migrate;
mod steamv2; mod steamv2;
mod winauth;
pub use manifest::*; pub use manifest::*;

View file

@ -1,6 +1,6 @@
use std::{fs::File, io::Read, path::Path}; use std::{fs::File, io::Read, path::Path};
use log::debug; use log::*;
use secrecy::SecretString; use secrecy::SecretString;
use serde::{de::Error, Deserialize}; use serde::{de::Error, Deserialize};
use steamguard::SteamGuardAccount; use steamguard::SteamGuardAccount;
@ -12,6 +12,7 @@ use super::{
legacy::{SdaAccount, SdaManifest}, legacy::{SdaAccount, SdaManifest},
manifest::ManifestV1, manifest::ManifestV1,
steamv2::SteamMobileV2, steamv2::SteamMobileV2,
winauth::parse_winauth_exports,
EntryLoader, Manifest, EntryLoader, Manifest,
}; };
@ -261,23 +262,48 @@ impl From<MigratingAccount> for SteamGuardAccount {
} }
} }
pub fn load_and_upgrade_external_account(path: &Path) -> anyhow::Result<SteamGuardAccount> { pub fn load_and_upgrade_external_accounts(path: &Path) -> anyhow::Result<Vec<SteamGuardAccount>> {
let file = File::open(path)?; let mut file = File::open(path)?;
let mut deser = serde_json::Deserializer::from_reader(&file); let mut buf = vec![];
let account: ExternalAccount = serde_path_to_error::deserialize(&mut deser) file.read_to_end(&mut buf)?;
.map_err(|err| anyhow::anyhow!("Failed to deserialize account: {}", err))?; let mut deser = serde_json::Deserializer::from_slice(&buf);
let mut account = MigratingAccount::External(account); let accounts = match serde_path_to_error::deserialize(&mut deser) {
while !account.is_latest() { Ok(account) => {
account = account.upgrade(); vec![MigratingAccount::External(account)]
} }
Err(json_err) => {
// the file is not JSON, so it's probably a winauth export
match parse_winauth_exports(buf) {
Ok(accounts) => accounts
.into_iter()
.map(MigratingAccount::External)
.collect(),
Err(winauth_err) => {
bail!(
"Failed to parse as JSON: {}\nFailed to parse as Winauth export: {}",
json_err,
winauth_err
)
}
}
}
};
Ok(account.into()) Ok(accounts
.into_iter()
.map(|mut account| {
while !account.is_latest() {
account = account.upgrade();
}
account.into()
})
.collect())
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
enum ExternalAccount { pub(crate) enum ExternalAccount {
Sda(SdaAccount), Sda(SdaAccount),
SteamMobileV2(SteamMobileV2), SteamMobileV2(SteamMobileV2),
} }
@ -390,10 +416,16 @@ mod tests {
account_name: "afarihm", account_name: "afarihm",
steam_id: 76561199441992970, steam_id: 76561199441992970,
}, },
Test {
mafile: "src/fixtures/maFiles/compat/winauth/exports.txt",
account_name: "example",
steam_id: 1234,
},
]; ];
for case in cases { for case in cases {
eprintln!("testing: {:?}", case); eprintln!("testing: {:?}", case);
let account = load_and_upgrade_external_account(Path::new(case.mafile))?; let accounts = load_and_upgrade_external_accounts(Path::new(case.mafile))?;
let account = accounts[0].clone();
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,50 @@
//! Accounts exported from Winauth are in the following format:
//!
//! One account per line, with each account represented as a URL.
//!
//! ```ignore
//! otpauth://totp/Steam:<steamaccountname>?secret=<ABCDEFG1234_secret_dunno_what_for>&digits=5&issuer=Steam&deviceid=<URL_Escaped_device_name>&data=<url_encoded_data_json>
//! ```
//!
//! The `data` field is a URL encoded JSON object with the following fields:
//!
//! ```json
//! {"steamid":"<steam_id>","status":1,"shared_secret":"<shared_secret>","serial_number":"<serial_number>","revocation_code":"<revocation_code>","uri":"<uri>","server_time":"<server_time>","account_name":"<steam_login_name>","token_gid":"<token_gid>","identity_secret":"<identity_secret>","secret_1":"<secret_1>","steamguard_scheme":"2"}
//! ```
use anyhow::Context;
use log::*;
use reqwest::Url;
use super::migrate::ExternalAccount;
pub(crate) fn parse_winauth_exports(buf: Vec<u8>) -> anyhow::Result<Vec<ExternalAccount>> {
let buf = String::from_utf8(buf)?;
let mut accounts = Vec::new();
for line in buf.split('\n') {
if line.is_empty() {
continue;
}
let url = Url::parse(line).context("parsing as winauth export URL")?;
let mut query = url.query_pairs();
let issuer = query
.find(|(key, _)| key == "issuer")
.context("missing issuer field")?
.1;
if issuer != "Steam" {
debug!("skipping non-Steam account: {}", issuer);
continue;
}
let data = query
.find(|(key, _)| key == "data")
.context("missing data field")?
.1;
trace!("data: {}", data);
let mut deser = serde_json::Deserializer::from_str(&data);
let account = serde_path_to_error::deserialize(&mut deser)?;
accounts.push(account);
}
Ok(accounts)
}

View file

@ -25,6 +25,7 @@ where
manager: &mut AccountManager, manager: &mut AccountManager,
_args: &GlobalArgs, _args: &GlobalArgs,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut accounts_added = 0;
for file_path in self.files.iter() { for file_path in self.files.iter() {
debug!("loading entry: {:?}", file_path); debug!("loading entry: {:?}", file_path);
match manager.import_account(file_path) { match manager.import_account(file_path) {
@ -38,25 +39,31 @@ where
debug!("Falling back to external account import",); debug!("Falling back to external account import",);
let path = Path::new(&file_path); let path = Path::new(&file_path);
let account = let accounts =
match crate::accountmanager::migrate::load_and_upgrade_external_account( match crate::accountmanager::migrate::load_and_upgrade_external_accounts(
path, path,
) { ) {
Ok(account) => account, Ok(accounts) => accounts,
Err(err) => { Err(err) => {
error!("Failed to import account: {} {}", &file_path, err); error!("Failed to import account: {} {}", &file_path, err);
error!("The original error was: {}", orig_err); error!("The original error was: {}", orig_err);
continue; continue;
} }
}; };
manager.add_account(account); for account in accounts {
info!("Imported account: {}", &file_path); manager.add_account(account);
info!("Imported account: {}", &file_path);
accounts_added += 1;
}
} }
Err(err) => { Err(err) => {
bail!("Failed to import account: {} {}", &file_path, err); bail!("Failed to import account: {} {}", &file_path, err);
} }
} }
} }
if accounts_added > 0 {
info!("Imported {} accounts", accounts_added);
}
manager.save()?; manager.save()?;
Ok(()) Ok(())

View file

@ -0,0 +1,2 @@
otpauth://totp/Steam:example?secret=ASDF&issuer=Steam&data=%7B%22shared%5Fsecret%22%3A%22zvIayp3JPvtvX%2FQGHqsqKBk%2F44s%3D%22%2C%22serial%5Fnumber%22%3A%22kljasfhds%22%2C%22revocation%5Fcode%22%3A%22R12345%22%2C%22uri%22%3A%22otpauth%3A%2F%2Ftotp%2FSteam%3Aexample%3Fsecret%3DASDF%26issuer%3DSteam%22%2C%22server%5Ftime%22%3A1602522478%2C%22account%5Fname%22%3A%22example%22%2C%22token%5Fgid%22%3A%22jkkjlhkhjgf%22%2C%22identity%5Fsecret%22%3A%22kjsdlwowiqe%3D%22%2C%22secret%5F1%22%3A%22sklduhfgsdlkjhf%3D%22%2C%22status%22%3A1%2C%22device%5Fid%22%3A%22android%3A99d2ad0e%2D4bad%2D4247%2Db111%2D26393aae0be3%22%2C%22fully%5Fenrolled%22%3Atrue%2C%22Session%22%3A%7B%22SessionID%22%3A%22a%3Blskdjf%22%2C%22SteamLogin%22%3A%22983498437543%22%2C%22SteamLoginSecure%22%3A%22dlkjdsl%3Bj%257C%2532984730298%22%2C%22WebCookie%22%3A%22%3Blkjsed%3Bklfjas98093%22%2C%22OAuthToken%22%3A%22asdk%3Blf%3Bdsjlkfd%22%2C%22SteamID%22%3A1234%7D%7D
otpauth://totp/Steam:example2?secret=ASDF&issuer=Steam&data=%7B%22shared%5Fsecret%22%3A%22zvIayp3JPvtvX%2FQGHqsqKBk%2F44s%3D%22%2C%22serial%5Fnumber%22%3A%22kljasfhds%22%2C%22revocation%5Fcode%22%3A%22R56789%22%2C%22uri%22%3A%22otpauth%3A%2F%2Ftotp%2FSteam%3Aexample%3Fsecret%3DASDF%26issuer%3DSteam%22%2C%22server%5Ftime%22%3A1602522478%2C%22account%5Fname%22%3A%22example2%22%2C%22token%5Fgid%22%3A%22jkkjlhkhjgf%22%2C%22identity%5Fsecret%22%3A%22kjsdlwowiqe%3D%22%2C%22secret%5F1%22%3A%22sklduhfgsdlkjhf%3D%22%2C%22status%22%3A1%2C%22device%5Fid%22%3A%22android%3A99d2ad0e%2D4bad%2D4247%2Db111%2D26393aae0be3%22%2C%22fully%5Fenrolled%22%3Atrue%2C%22Session%22%3A%7B%22SessionID%22%3A%22a%3Blskdjf%22%2C%22SteamLogin%22%3A%22983498437543%22%2C%22SteamLoginSecure%22%3A%22dlkjdsl%3Bj%257C%2532984730298%22%2C%22WebCookie%22%3A%22%3Blkjsed%3Bklfjas98093%22%2C%22OAuthToken%22%3A%22asdk%3Blf%3Bdsjlkfd%22%2C%22SteamID%22%3A5678%7D%7D