Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
|
96a30c6150 | ||
|
dd153a617c | ||
|
28c7a797cf | ||
|
c2a72fee6c | ||
|
602acc6641 | ||
|
b4564b7d5e | ||
|
d30ba017c9 | ||
|
37ae7c76a6 | ||
|
ef72bd898c | ||
|
52044c95bb | ||
|
b24a7415f6 | ||
|
8d54a1254a | ||
|
ab5cb00ee3 | ||
|
c7fefc1452 | ||
|
a64f7d4134 |
12 changed files with 1058 additions and 933 deletions
1839
Cargo.lock
generated
1839
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
Cargo.toml
27
Cargo.toml
|
@ -4,9 +4,9 @@ members = ["steamguard"]
|
|||
|
||||
[package]
|
||||
name = "steamguard-cli"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
authors = ["dyc3 (Carson McManus) <carson.mcmanus1@gmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "A command line utility to generate Steam 2FA codes and respond to confirmations."
|
||||
keywords = ["steam", "2fa", "steamguard", "authentication", "cli"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
@ -29,10 +29,10 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "^1.0"
|
||||
base64 = "0.21.2"
|
||||
base64 = "0.22.1"
|
||||
text_io = "0.1.8"
|
||||
rpassword = "7.2.0"
|
||||
reqwest = { version = "0.11", default-features = false, features = [
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"blocking",
|
||||
"json",
|
||||
"cookies",
|
||||
|
@ -43,21 +43,20 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1.0"
|
||||
rsa = "0.9.2"
|
||||
rand = "0.8.5"
|
||||
standback = "0.2.17" # required to fix a compilation error on a transient dependency
|
||||
clap = { version = "3.1.18", features = ["derive", "cargo", "env"] }
|
||||
clap_complete = "3.2.1"
|
||||
clap = { version = "4.5.4", features = ["derive", "cargo", "env"] }
|
||||
clap_complete = "4.5.2"
|
||||
log = "0.4.19"
|
||||
stderrlog = "0.6"
|
||||
cookie = "0.14"
|
||||
cookie = "0.18"
|
||||
regex = "1"
|
||||
lazy_static = "1.4.0"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
steamguard = { version = "^0.13.0", path = "./steamguard" }
|
||||
dirs = "3.0.2"
|
||||
uuid = { version = "1.8", features = ["v4"] }
|
||||
steamguard = { version = "^0.14.0", path = "./steamguard" }
|
||||
dirs = "5.0.1"
|
||||
aes = { version = "0.8.3", features = ["zeroize"] }
|
||||
thiserror = "1.0.26"
|
||||
thiserror = "1.0.61"
|
||||
crossterm = { version = "0.23.2", features = ["event-stream"] }
|
||||
qrcode = { version = "0.12.0", optional = true }
|
||||
qrcode = { version = "0.14.0", optional = true }
|
||||
gethostname = "0.4.3"
|
||||
secrecy = { version = "0.8", features = ["serde"] }
|
||||
zeroize = { version = "^1.6.0", features = ["std", "zeroize_derive"] }
|
||||
|
@ -73,6 +72,8 @@ argon2 = { version = "0.5.0", features = ["std", "zeroize"] }
|
|||
pbkdf2 = { version = "0.12.1", features = ["parallel"] }
|
||||
sha1 = "0.10.5"
|
||||
rayon = "1.7.0"
|
||||
rqrr = "0.7.1"
|
||||
image = "0.25"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
3
PKGBUILD
3
PKGBUILD
|
@ -3,7 +3,7 @@
|
|||
|
||||
_pkgname=steamguard-cli
|
||||
pkgname=${_pkgname}-git
|
||||
pkgver=0.8.1.r1.fe0d6e9a
|
||||
pkgver=0.14.0.r1.602acc66
|
||||
pkgrel=1
|
||||
pkgdesc="A command line utility to generate Steam 2FA codes and respond to confirmations."
|
||||
arch=('i686' 'x86_64' 'armv6h' 'armv7h')
|
||||
|
@ -12,6 +12,7 @@ license=('GPL3')
|
|||
makedepends=('rust' 'cargo' 'git')
|
||||
source=("git+https://github.com/dyc3/steamguard-cli.git")
|
||||
sha256sums=('SKIP')
|
||||
options=(!lto)
|
||||
|
||||
pkgver() {
|
||||
cd "${srcdir}/${_pkgname}"
|
||||
|
|
|
@ -339,8 +339,9 @@ impl AccountManager {
|
|||
debug!("Adding missing account names");
|
||||
for i in 0..self.manifest.entries.len() {
|
||||
let account = self.load_account_by_entry(&self.manifest.entries[i].clone())?;
|
||||
self.manifest.entries[i].account_name =
|
||||
account.lock().unwrap().account_name.clone();
|
||||
self.manifest.entries[i]
|
||||
.account_name
|
||||
.clone_from(&account.lock().unwrap().account_name);
|
||||
}
|
||||
upgraded = true;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use clap::{clap_derive::ArgEnum, Parser};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use clap_complete::Shell;
|
||||
use secrecy::SecretString;
|
||||
use std::str::FromStr;
|
||||
|
@ -127,7 +127,7 @@ pub(crate) struct GlobalArgs {
|
|||
help = "Specify your encryption passkey."
|
||||
)]
|
||||
pub passkey: Option<SecretString>,
|
||||
#[clap(short, long, arg_enum, default_value_t=Verbosity::Info, help = "Set the log level. Be warned, trace is capable of printing sensitive data.")]
|
||||
#[clap(short, long, value_enum, default_value_t=Verbosity::Info, help = "Set the log level. Be warned, trace is capable of printing sensitive data.")]
|
||||
pub verbosity: Verbosity,
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
|
@ -160,7 +160,7 @@ pub(crate) struct GlobalArgs {
|
|||
pub danger_accept_invalid_certs: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub(crate) enum Subcommands {
|
||||
Debug(DebugCommand),
|
||||
Completion(CompletionsCommand),
|
||||
|
@ -177,7 +177,7 @@ pub(crate) enum Subcommands {
|
|||
QrLogin(QrLoginCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ArgEnum)]
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub(crate) enum Verbosity {
|
||||
Error = 0,
|
||||
Warn = 1,
|
||||
|
@ -223,3 +223,14 @@ impl From<Args> for CodeCommand {
|
|||
args.code
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
use clap::CommandFactory;
|
||||
Args::command().debug_assert()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,12 @@ use super::*;
|
|||
#[derive(Debug, Clone, Parser)]
|
||||
#[clap(about = "Generate shell completions")]
|
||||
pub struct CompletionsCommand {
|
||||
#[clap(short, long, arg_enum, help = "The shell to generate completions for.")]
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
value_enum,
|
||||
help = "The shell to generate completions for."
|
||||
)]
|
||||
pub shell: Shell,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use log::*;
|
||||
use rqrr::PreparedImage;
|
||||
use steamguard::{QrApprover, QrApproverError};
|
||||
|
||||
use crate::AccountManager;
|
||||
|
@ -10,11 +14,8 @@ use super::*;
|
|||
#[derive(Debug, Clone, Parser)]
|
||||
#[clap(about = "Log in to Steam on another device using the QR code that it's displaying.")]
|
||||
pub struct QrLoginCommand {
|
||||
#[clap(
|
||||
long,
|
||||
help = "The URL that would normally open in the Steam app. This is the URL that the QR code is displaying. It should start with \"https://s.team/...\""
|
||||
)]
|
||||
pub url: String,
|
||||
#[clap(flatten)]
|
||||
login_url_source: LoginUrlSource,
|
||||
}
|
||||
|
||||
impl<T> AccountCommand<T> for QrLoginCommand
|
||||
|
@ -41,6 +42,8 @@ where
|
|||
crate::do_login(transport.clone(), &mut account, args.password.clone())?;
|
||||
}
|
||||
|
||||
let url = self.login_url_source.url()?;
|
||||
debug!("Using login URL to approve: {}", url);
|
||||
loop {
|
||||
let Some(tokens) = account.tokens.as_ref() else {
|
||||
error!(
|
||||
|
@ -51,7 +54,7 @@ where
|
|||
};
|
||||
|
||||
let mut approver = QrApprover::new(transport.clone(), tokens);
|
||||
match approver.approve(&account, &self.url) {
|
||||
match approver.approve(&account, url.to_owned()) {
|
||||
Ok(_) => {
|
||||
info!("Login approved.");
|
||||
break;
|
||||
|
@ -70,3 +73,56 @@ where
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
pub struct LoginUrlSource {
|
||||
/// The URL that would normally open in the Steam app. This is the URL that the QR code is displaying. It should start with \"https://s.team/...\"
|
||||
#[clap(long)]
|
||||
url: Option<String>,
|
||||
/// Path to an image file containing the QR code. The QR code will be scanned from this image.
|
||||
#[clap(long)]
|
||||
image: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl LoginUrlSource {
|
||||
fn url(&self) -> anyhow::Result<String> {
|
||||
match self {
|
||||
Self { url: Some(url), .. } => Ok(url.clone()),
|
||||
Self {
|
||||
image: Some(path), ..
|
||||
} => read_qr_image(path),
|
||||
_ => Err(anyhow!(
|
||||
"You must provide either a URL with --url or an image file with --image."
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_qr_image(path: &Path) -> anyhow::Result<String> {
|
||||
use image::io::Reader as ImageReader;
|
||||
let image = ImageReader::open(path)?.decode()?.to_luma8();
|
||||
let mut img = PreparedImage::prepare(image);
|
||||
let grids = img.detect_grids();
|
||||
for grid in grids {
|
||||
let (_meta, text) = grid.decode()?;
|
||||
// a rough validation that the QR code is a Steam login code
|
||||
if text.contains("s.team") {
|
||||
return Ok(text);
|
||||
}
|
||||
}
|
||||
Err(anyhow!("No Steam login url found in the QR code"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_read_qr_image() {
|
||||
let path = Path::new("src/fixtures/qr-codes/login-qr.png");
|
||||
let url = read_qr_image(path).unwrap();
|
||||
assert_eq!(url, "https://s.team/q/1/2372462679780599330");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -193,6 +193,7 @@ impl SetupCommand {
|
|||
"authenticator state: {} -- did not actually finalize",
|
||||
status.state()
|
||||
);
|
||||
debug!("full status: {:#?}", status);
|
||||
manager.remove_account(&account_name);
|
||||
manager.save()?;
|
||||
bail!("Authenticator finalization was unsuccessful. You may have entered the wrong confirm code in the previous step. Try again.");
|
||||
|
|
BIN
src/fixtures/qr-codes/login-qr.png
Normal file
BIN
src/fixtures/qr-codes/login-qr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "steamguard"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
authors = ["Carson McManus <carson.mcmanus1@gmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "Library for generating 2fa codes for Steam and responding to mobile confirmations."
|
||||
keywords = ["steam", "2fa", "steamguard", "authentication"]
|
||||
repository = "https://github.com/dyc3/steamguard-cli/tree/master/steamguard"
|
||||
|
@ -11,8 +11,8 @@ license = "MIT OR Apache-2.0"
|
|||
[dependencies]
|
||||
anyhow = "^1.0"
|
||||
sha1 = "^0.10"
|
||||
base64 = "^0.21"
|
||||
reqwest = { version = "0.11", default-features = false, features = [
|
||||
base64 = "^0.22.1"
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"blocking",
|
||||
"json",
|
||||
"cookies",
|
||||
|
@ -24,13 +24,11 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1.0"
|
||||
rsa = "0.9.2"
|
||||
rand = "0.8.4"
|
||||
standback = "0.2.17" # required to fix a compilation error on a transient dependency
|
||||
cookie = "0.14"
|
||||
cookie = "0.18"
|
||||
regex = "1"
|
||||
lazy_static = "1.4.0"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
uuid = { version = "1.8", features = ["v4"] }
|
||||
log = "0.4.19"
|
||||
scraper = "0.12.0"
|
||||
maplit = "1.0.2"
|
||||
thiserror = "1.0.26"
|
||||
secrecy = { version = "0.8", features = ["serde"] }
|
||||
|
|
|
@ -137,10 +137,7 @@ where
|
|||
let mut req = CTwoFactor_Status_Request::new();
|
||||
req.set_steamid(account.steam_id);
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.query_status(req, self.tokens.access_token())
|
||||
.unwrap();
|
||||
let resp = self.client.query_status(req, self.tokens.access_token())?;
|
||||
|
||||
Ok(resp.into_response_data())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct OAuthData {
|
||||
pub oauth_token: String,
|
||||
pub steamid: String,
|
||||
|
|
Loading…
Add table
Reference in a new issue