From 5599e28c9fc0b6cc78bf2fb924568695bebeca1e Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 5 Dec 2022 08:47:33 -0500 Subject: [PATCH] add the ability to generate a QR code for 2fa secrets --- Cargo.lock | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 +++++ README.md | 13 ++++++++++++- src/cli.rs | 13 +++++++++++++ src/main.rs | 43 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index dac7071..19f8a53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,12 @@ version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + [[package]] name = "byteorder" version = "1.4.3" @@ -159,6 +165,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.19" @@ -229,6 +241,12 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "const-oid" version = "0.6.2" @@ -748,6 +766,20 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "indexmap" version = "1.8.2" @@ -956,6 +988,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1236,6 +1279,16 @@ dependencies = [ "psl-types", ] +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1933,6 +1986,7 @@ dependencies = [ "lazy_static 1.4.0", "log", "proptest", + "qrcode", "rand 0.8.5", "regex", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index af155b1..bed8003 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ categories = ["command-line-utilities"] repository = "https://github.com/dyc3/steamguard-cli" license = "GPL-3.0-or-later" +[features] +default = ["qr"] +qr = ["qrcode"] + [[bin]] name = "steamguard-cli" # filename = "steamguard" # TODO: uncomment when https://github.com/rust-lang/cargo/issues/9778 is stablized. @@ -48,6 +52,7 @@ aes = "0.7.4" block-modes = "0.8.1" thiserror = "1.0.26" crossterm = { version = "0.23.2", features = ["event-stream"] } +qrcode = { version = "0.12.0", optional = true } [dev-dependencies] tempdir = "0.3" diff --git a/README.md b/README.md index 9615dfa..3b21f9a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,18 @@ steamguard-cli | xclip -selection clipboard ## Importing 2FA Secret Into Other Applications -It's possible to import your 2FA secret into other applications, like Google Authenticator or KeeWeb. The `uri` field contains a URI in that starts with `otpauth://...`, which you can create a QR code for. +It's possible to import your 2FA secret into other applications. This is useful if you want to use a password manager to generate your 2FA codes, like KeeWeb. + +To make this easy, steamguard-cli can generate a QR code for your 2FA secret. You can then scan this QR code with your password manager. + +```bash +steamguard qr # print QR code for the first account in your maFiles +steamguard -u qr # print QR code for a specific account +``` + +There are some applications that do not generate correct 2fa codes from the secret, so **do not use them**: +- Google Authenticator +- Authy # Contributing diff --git a/src/cli.rs b/src/cli.rs index 30c2710..f131d05 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -55,6 +55,8 @@ pub(crate) enum Subcommands { Encrypt(ArgsEncrypt), Decrypt(ArgsDecrypt), Code(ArgsCode), + #[cfg(feature = "qr")] + Qr(ArgsQr), } #[derive(Debug, Clone, Copy, ArgEnum)] @@ -174,3 +176,14 @@ impl From for ArgsCode { args.code } } + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Generate QR codes. This *will* print sensitive data to stdout.")] +#[cfg(feature = "qr")] +pub(crate) struct ArgsQr { + #[clap( + long, + help = "Force using ASCII chars to generate QR codes. Useful for terminals that don't support unicode." + )] + pub ascii: bool, +} diff --git a/src/main.rs b/src/main.rs index cb5bcee..9a3ed39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ extern crate rpassword; use clap::{IntoApp, Parser}; use crossterm::tty::IsTty; use log::*; +#[cfg(feature = "qr")] +use qrcode::QrCode; use std::time::{SystemTime, UNIX_EPOCH}; use std::{ io::{stdout, Write}, @@ -179,6 +181,10 @@ fn run() -> anyhow::Result<()> { cli::Subcommands::Code(args) => { return do_subcmd_code(args, selected_accounts); } + #[cfg(feature = "qr")] + cli::Subcommands::Qr(args) => { + return do_subcmd_qr(args, selected_accounts); + } s => { error!("Unknown subcommand: {:?}", s); return Err(errors::UserError::UnknownSubcommand.into()); @@ -690,3 +696,40 @@ fn do_subcmd_code( } return Ok(()); } + +#[cfg(feature = "qr")] +fn do_subcmd_qr( + args: cli::ArgsQr, + selected_accounts: Vec>>, +) -> anyhow::Result<()> { + use anyhow::Context; + + info!( + "Generating QR codes for {} accounts", + selected_accounts.len() + ); + + for account in selected_accounts { + let account = account.lock().unwrap(); + let qr = QrCode::new(account.uri.expose_secret()) + .context(format!("generating qr code for {}", account.account_name))?; + + info!("Printing QR code for {}", account.account_name); + let qr_string = if args.ascii { + qr.render() + .light_color(' ') + .dark_color('#') + .module_dimensions(2, 1) + .build() + } else { + use qrcode::render::unicode; + qr.render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build() + }; + + println!("{}", qr_string); + } + return Ok(()); +}