steam_vent/auth/
confirmation.rs

1use crate::auth::SteamGuardToken;
2use another_steam_totp::generate_auth_code;
3use futures_util::future::{select, Either};
4use std::pin::pin;
5use steam_vent_proto::steammessages_auth_steamclient::{
6    CAuthentication_AllowedConfirmation, EAuthSessionGuardType,
7};
8use tokio::io::AsyncBufReadExt;
9use tokio::io::{stdin, stdout, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, Stdin, Stdout};
10
11/// A method that can be used to confirm a login
12#[derive(Debug, Clone)]
13pub struct ConfirmationMethod(CAuthentication_AllowedConfirmation);
14
15impl From<CAuthentication_AllowedConfirmation> for ConfirmationMethod {
16    fn from(value: CAuthentication_AllowedConfirmation) -> Self {
17        Self(value)
18    }
19}
20
21impl ConfirmationMethod {
22    /// Get the human-readable confirmation type
23    pub fn confirmation_type(&self) -> &'static str {
24        match self.0.confirmation_type() {
25            EAuthSessionGuardType::k_EAuthSessionGuardType_Unknown => "unknown",
26            EAuthSessionGuardType::k_EAuthSessionGuardType_None => "none",
27            EAuthSessionGuardType::k_EAuthSessionGuardType_EmailCode => "email",
28            EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode => "device code",
29            EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceConfirmation => {
30                "device confirmation"
31            }
32            EAuthSessionGuardType::k_EAuthSessionGuardType_EmailConfirmation => {
33                "email confirmation"
34            }
35            EAuthSessionGuardType::k_EAuthSessionGuardType_MachineToken => "machine token",
36            EAuthSessionGuardType::k_EAuthSessionGuardType_LegacyMachineAuth => "machine auth",
37        }
38    }
39
40    /// Get the server-provided message for the confirmation
41    pub fn confirmation_details(&self) -> &str {
42        self.0.associated_message()
43    }
44
45    /// Is any action required to confirm the login
46    pub fn action_required(&self) -> bool {
47        self.0.confirmation_type() != EAuthSessionGuardType::k_EAuthSessionGuardType_None
48    }
49
50    /// Get the class of the confirmation
51    pub fn class(&self) -> ConfirmationMethodClass {
52        match self.0.confirmation_type() {
53            EAuthSessionGuardType::k_EAuthSessionGuardType_Unknown => ConfirmationMethodClass::None,
54            EAuthSessionGuardType::k_EAuthSessionGuardType_None => ConfirmationMethodClass::None,
55            EAuthSessionGuardType::k_EAuthSessionGuardType_EmailCode => {
56                ConfirmationMethodClass::Code
57            }
58            EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode => {
59                ConfirmationMethodClass::Code
60            }
61            EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceConfirmation => {
62                ConfirmationMethodClass::Confirmation
63            }
64            EAuthSessionGuardType::k_EAuthSessionGuardType_EmailConfirmation => {
65                ConfirmationMethodClass::Confirmation
66            }
67            EAuthSessionGuardType::k_EAuthSessionGuardType_MachineToken => {
68                ConfirmationMethodClass::Stored
69            }
70            EAuthSessionGuardType::k_EAuthSessionGuardType_LegacyMachineAuth => {
71                ConfirmationMethodClass::Stored
72            }
73        }
74    }
75
76    /// Get the token type required for the confirmation, if the confirmation asks for a code
77    pub fn token_type(&self) -> Option<GuardTokenType> {
78        match self.0.confirmation_type() {
79            EAuthSessionGuardType::k_EAuthSessionGuardType_Unknown => None,
80            EAuthSessionGuardType::k_EAuthSessionGuardType_None => None,
81            EAuthSessionGuardType::k_EAuthSessionGuardType_EmailCode => Some(GuardTokenType::Email),
82            EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode => {
83                Some(GuardTokenType::Device)
84            }
85            EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceConfirmation => None,
86            EAuthSessionGuardType::k_EAuthSessionGuardType_EmailConfirmation => None,
87            EAuthSessionGuardType::k_EAuthSessionGuardType_MachineToken => None,
88            EAuthSessionGuardType::k_EAuthSessionGuardType_LegacyMachineAuth => None,
89        }
90    }
91}
92
93/// The class of confirmation method
94#[derive(Eq, PartialEq, Debug, Clone)]
95pub enum ConfirmationMethodClass {
96    /// Provide a totp token
97    Code,
98    /// Confirm the login out-of-band
99    Confirmation,
100    /// Provide stored guard data
101    Stored,
102    /// No action required
103    None,
104}
105
106/// The action to perform to confirm the login
107#[non_exhaustive]
108#[derive(Debug)]
109pub enum ConfirmationAction {
110    /// A totp token to send to the server
111    GuardToken(SteamGuardToken, GuardTokenType),
112    /// No action required
113    None,
114    /// Login has been canceled by the user
115    Abort,
116}
117
118/// The type of guard token
119#[derive(Debug)]
120pub enum GuardTokenType {
121    Email,
122    Device,
123}
124
125impl From<GuardTokenType> for EAuthSessionGuardType {
126    fn from(value: GuardTokenType) -> Self {
127        match value {
128            GuardTokenType::Device => EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode,
129            GuardTokenType::Email => EAuthSessionGuardType::k_EAuthSessionGuardType_EmailCode,
130        }
131    }
132}
133
134/// A trait for handling login confirmations
135///
136/// The library comes with handlers for:
137///
138/// - Asking for a code from the terminal: [`ConsoleAuthConfirmationHandler`].
139/// - Generating a code from the pre-shared secret: [`SharedSecretAuthConfirmationHandler`].
140/// - Waiting for the user to confirm the login from the mobile app: [`DeviceConfirmationHandler`].
141///
142/// Additionally, apps can implement the trait to integrate the confirmation flow into the app.
143pub trait AuthConfirmationHandler: Sized {
144    /// Perform the confirmation action given a list of allowed confirmations for the login
145    ///
146    /// If the confirmation handler supports any of the allowed confirmations,
147    /// it returns a [`ConfirmationAction`] with the required action.
148    ///
149    /// If the confirmation handler does not support any of the allowed confirmations it returns `None`.
150    /// If no confirmation handler supports the allowed confirmations the login will fail.
151    fn handle_confirmation(
152        self,
153        allowed_confirmations: &[ConfirmationMethod],
154    ) -> impl std::future::Future<Output = Option<ConfirmationAction>> + Send;
155
156    /// Return a new confirmation handler that combines the current one with a new one.
157    ///
158    /// The resulting confirmation handler will handle both handler in parallel.
159    fn or<Right: AuthConfirmationHandler>(
160        self,
161        other: Right,
162    ) -> EitherConfirmationHandler<Self, Right> {
163        EitherConfirmationHandler::new(self, other)
164    }
165}
166
167/// Ask the user for the totp token from the terminal
168pub type ConsoleAuthConfirmationHandler = UserProvidedAuthConfirmationHandler<Stdin, Stdout>;
169
170/// Ask the user to provide the totp token
171pub struct UserProvidedAuthConfirmationHandler<Read, Write> {
172    input: BufReader<Read>,
173    output: Write,
174}
175
176impl Default for ConsoleAuthConfirmationHandler {
177    fn default() -> Self {
178        ConsoleAuthConfirmationHandler {
179            input: BufReader::new(stdin()),
180            output: stdout(),
181        }
182    }
183}
184
185impl<Read, Write> UserProvidedAuthConfirmationHandler<Read, Write>
186where
187    Read: AsyncRead + Unpin + Send + Sync,
188    Write: AsyncWrite + Unpin + Send + Sync,
189{
190    /// Create a confirmation handling using the provided I/O
191    ///
192    /// The handler will write details about the required tokens to the output
193    /// and expect the newline terminated token from the input
194    pub fn new(input: Read, output: Write) -> Self {
195        UserProvidedAuthConfirmationHandler {
196            input: BufReader::new(input),
197            output,
198        }
199    }
200}
201
202impl<Read, Write> AuthConfirmationHandler for UserProvidedAuthConfirmationHandler<Read, Write>
203where
204    Read: AsyncRead + Unpin + Send + Sync,
205    Write: AsyncWrite + Unpin + Send + Sync,
206{
207    async fn handle_confirmation(
208        mut self,
209        allowed_confirmations: &[ConfirmationMethod],
210    ) -> Option<ConfirmationAction> {
211        for method in allowed_confirmations {
212            if let Some(token_type) = method.token_type() {
213                let msg = format!(
214                    "{}: {}",
215                    method.confirmation_type(),
216                    method.confirmation_details()
217                );
218                self.output.write_all(msg.as_bytes()).await.ok();
219                self.output.flush().await.ok();
220                let mut buff = String::with_capacity(16);
221                self.input.read_line(&mut buff).await.ok();
222                buff.truncate(buff.trim().len());
223                return if buff.is_empty() {
224                    Some(ConfirmationAction::Abort)
225                } else {
226                    let token = SteamGuardToken(buff);
227                    Some(ConfirmationAction::GuardToken(token, token_type))
228                };
229            }
230        }
231        None
232    }
233}
234
235/// Generate the steam guard totp token from the shared secret
236///
237/// This requires no user interaction during login but requires the user to retrieve the totp secret in advance
238pub struct SharedSecretAuthConfirmationHandler {
239    shared_secret: String,
240}
241
242impl SharedSecretAuthConfirmationHandler {
243    /// The totp shared secret encoded as base64
244    ///
245    /// Note that the secret as found in `totp://` urls is base32 encoded, not base64
246    pub fn new(shared_secret: &str) -> Self {
247        SharedSecretAuthConfirmationHandler {
248            shared_secret: shared_secret.into(),
249        }
250    }
251}
252
253impl AuthConfirmationHandler for SharedSecretAuthConfirmationHandler {
254    async fn handle_confirmation(
255        self,
256        allowed_confirmations: &[ConfirmationMethod],
257    ) -> Option<ConfirmationAction> {
258        for method in allowed_confirmations {
259            if let Some(token_type) = method.token_type() {
260                let auth_code = generate_auth_code(self.shared_secret, None)
261                    .expect("Could not generate auth code given shared secret.");
262                let token = SteamGuardToken(auth_code);
263                return Some(ConfirmationAction::GuardToken(token, token_type));
264            }
265        }
266        None
267    }
268}
269
270/// Wait for the user to confirm the login in the mobile app
271#[derive(Default)]
272pub struct DeviceConfirmationHandler;
273
274impl AuthConfirmationHandler for DeviceConfirmationHandler {
275    async fn handle_confirmation(
276        self,
277        allowed_confirmations: &[ConfirmationMethod],
278    ) -> Option<ConfirmationAction> {
279        for method in allowed_confirmations {
280            if method.class() == ConfirmationMethodClass::Confirmation {
281                return Some(ConfirmationAction::None);
282            }
283        }
284        None
285    }
286}
287
288/// Use multiple confirmation handlers in parallel.
289///
290/// This is primarily usefully for allowing users to pick between providing a totp code or confirming
291/// the login in the mobile app.
292pub struct EitherConfirmationHandler<Left, Right> {
293    left: Left,
294    right: Right,
295}
296
297impl<Left, Right> EitherConfirmationHandler<Left, Right> {
298    pub fn new(left: Left, right: Right) -> Self {
299        Self { left, right }
300    }
301}
302
303impl<Left, Right> AuthConfirmationHandler for EitherConfirmationHandler<Left, Right>
304where
305    Left: AuthConfirmationHandler + Send + Sync,
306    Right: AuthConfirmationHandler + Send + Sync,
307{
308    async fn handle_confirmation(
309        self,
310        allowed_confirmations: &[ConfirmationMethod],
311    ) -> Option<ConfirmationAction> {
312        match select(
313            pin!(self.left.handle_confirmation(allowed_confirmations)),
314            pin!(self.right.handle_confirmation(allowed_confirmations)),
315        )
316        .await
317        {
318            Either::Left((left_result, right_fut)) => match left_result {
319                None | Some(ConfirmationAction::None) => right_fut.await,
320                _ => left_result,
321            },
322            Either::Right((right_result, left_fut)) => match right_result {
323                None | Some(ConfirmationAction::None) => left_fut.await,
324                _ => right_result,
325            },
326        }
327    }
328}