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 migrate;
mod steamv2;
mod winauth;
pub use manifest::*;

View file

@ -1,6 +1,6 @@
use std::{fs::File, io::Read, path::Path};
use log::debug;
use log::*;
use secrecy::SecretString;
use serde::{de::Error, Deserialize};
use steamguard::SteamGuardAccount;
@ -12,6 +12,7 @@ use super::{
legacy::{SdaAccount, SdaManifest},
manifest::ManifestV1,
steamv2::SteamMobileV2,
winauth::parse_winauth_exports,
EntryLoader, Manifest,
};
@ -261,23 +262,48 @@ impl From<MigratingAccount> for SteamGuardAccount {
}
}
pub fn load_and_upgrade_external_account(path: &Path) -> anyhow::Result<SteamGuardAccount> {
let file = File::open(path)?;
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();
}
pub fn load_and_upgrade_external_accounts(path: &Path) -> anyhow::Result<Vec<SteamGuardAccount>> {
let mut file = File::open(path)?;
let mut buf = vec![];
file.read_to_end(&mut buf)?;
let mut deser = serde_json::Deserializer::from_slice(&buf);
let accounts = match serde_path_to_error::deserialize(&mut deser) {
Ok(account) => {
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)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum ExternalAccount {
pub(crate) enum ExternalAccount {
Sda(SdaAccount),
SteamMobileV2(SteamMobileV2),
}
@ -390,10 +416,16 @@ mod tests {
account_name: "afarihm",
steam_id: 76561199441992970,
},
Test {
mafile: "src/fixtures/maFiles/compat/winauth/exports.txt",
account_name: "example",
steam_id: 1234,
},
];
for case in cases {
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.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,
_args: &GlobalArgs,
) -> anyhow::Result<()> {
let mut accounts_added = 0;
for file_path in self.files.iter() {
debug!("loading entry: {:?}", file_path);
match manager.import_account(file_path) {
@ -38,25 +39,31 @@ where
debug!("Falling back to external account import",);
let path = Path::new(&file_path);
let account =
match crate::accountmanager::migrate::load_and_upgrade_external_account(
let accounts =
match crate::accountmanager::migrate::load_and_upgrade_external_accounts(
path,
) {
Ok(account) => account,
Ok(accounts) => accounts,
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);
for account in accounts {
manager.add_account(account);
info!("Imported account: {}", &file_path);
accounts_added += 1;
}
}
Err(err) => {
bail!("Failed to import account: {} {}", &file_path, err);
}
}
}
if accounts_added > 0 {
info!("Imported {} accounts", accounts_added);
}
manager.save()?;
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