parent
6d4915a39c
commit
94a3210f09
5 changed files with 110 additions and 18 deletions
|
@ -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::*;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
50
src/accountmanager/winauth.rs
Normal file
50
src/accountmanager/winauth.rs
Normal 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)
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
|
2
src/fixtures/maFiles/compat/winauth/exports.txt
Normal file
2
src/fixtures/maFiles/compat/winauth/exports.txt
Normal 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
|
Loading…
Reference in a new issue