another_steam_totp/
lib.rs1#![cfg_attr(docsrs, feature(doc_cfg))]
19
20mod error;
21mod tag;
22mod decode;
23
24#[cfg(any(feature = "reqwest", feature = "ureq"))]
25mod http;
26
27pub use error::Error;
28pub use tag::Tag;
29
30#[cfg(feature = "reqwest")]
31pub use http::get_steam_server_time_offset;
32
33#[cfg(feature = "ureq")]
34pub use http::get_steam_server_time_offset_sync;
35
36use decode::decode_secret;
37use std::time::{SystemTime, UNIX_EPOCH};
38use std::fmt::Write;
39use hmac::{Hmac, Mac};
40use sha1::{Sha1, Digest};
41use base64::Engine;
42
43const CHARS: &[char] = &[
44 '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G',
45 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y',
46];
47
48type HmacSha1 = Hmac<Sha1>;
49
50pub fn generate_auth_code<T: AsRef<[u8]>>(
65 shared_secret: T,
66 time_offset: Option<i64>,
67) -> Result<String, Error> {
68 let timestamp = get_offset_timestamp(time_offset)?;
69
70 generate_auth_code_for_time(shared_secret, timestamp)
71}
72
73pub fn generate_confirmation_key<T: AsRef<[u8]>>(
92 identity_secret: T,
93 tag: Tag,
94 time_offset: Option<i64>,
95) -> Result<(String, u64), Error> {
96 let timestamp = get_offset_timestamp(time_offset)?;
97 let confirmation_key = generate_confirmation_key_for_time(identity_secret, tag, timestamp)?;
98
99 Ok((confirmation_key, timestamp))
100}
101
102pub fn get_device_id(steamid: u64) -> String {
114 generate_device_id(steamid, None)
115}
116
117pub fn get_device_id_with_salt(steamid: u64, salt: &str) -> String {
129 generate_device_id(steamid, Some(salt))
130}
131
132fn generate_device_id(steamid: u64, salt: Option<&str>) -> String {
134 let mut hasher = Sha1::new();
135
136 if let Some(salt) = salt {
137 hasher.update(format!("{steamid}{salt}"));
138 } else {
139 hasher.update(steamid.to_string());
140 }
141
142 let result = hasher.finalize();
143 let hash = result
144 .iter()
145 .fold(String::new(), |mut output, b| {
146 let _ = write!(output, "{b:02x}");
147 output
148 });
149 let (p1, rest) = hash.split_at(8);
151 let (p2, rest) = rest.split_at(4);
152 let (p3, rest) = rest.split_at(4);
153 let (p4, rest) = rest.split_at(4);
154 let (p5, _) = rest.split_at(12);
155
156 format!("android:{p1}-{p2}-{p3}-{p4}-{p5}")
157}
158
159fn current_timestamp() -> Result<u64, Error> {
161 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
162}
163
164fn generate_auth_code_for_time<T: AsRef<[u8]>>(
166 shared_secret: T,
167 timestamp: u64,
168) -> Result<String, Error> {
169 let mut full_code = {
170 let bytes = (timestamp / 30).to_be_bytes();
171 let hmac = get_hmac_msg(shared_secret, &bytes)?;
172 let result = hmac.finalize().into_bytes();
173 let slice_start = result[19] & 0x0F;
174 let slice_end = slice_start + 4;
175 let slice: &[u8] = &result[slice_start as usize..slice_end as usize];
176 let full_code_slice: [u8; 4] = slice.try_into()
177 .map_err(|_e| std::io::Error::new(
179 std::io::ErrorKind::Other,
180 "Failed to convert slice to array.",
181 ))?;
182 let full_code_bytes = u32::from_be_bytes(full_code_slice);
183
184 full_code_bytes & 0x7FFFFFFF
185 };
186 let chars_len = CHARS.len() as u32;
187 let code = (0..5)
188 .map(|_i| {
189 let char_code = CHARS[(full_code % chars_len) as usize];
190
191 full_code /= chars_len;
192 char_code
193 })
194 .collect::<String>();
195
196 Ok(code)
197}
198
199fn generate_confirmation_key_for_time<T: AsRef<[u8]>>(
201 identity_secret: T,
202 tag: Tag,
203 timestamp: u64,
204) -> Result<String, Error> {
205 let timestamp_bytes = timestamp.to_be_bytes();
206 let tag_string = tag.to_string();
207 let tag_bytes = tag_string.as_bytes();
208 let array = [×tamp_bytes, tag_bytes].concat();
209 let hmac = get_hmac_msg(identity_secret, &array)?;
210 let code_bytes = hmac.finalize().into_bytes();
211
212 Ok(base64::engine::general_purpose::STANDARD.encode(code_bytes))
213}
214
215fn get_hmac_msg<T: AsRef<[u8]>>(
217 secret: T,
218 bytes: &[u8],
219) -> Result<HmacSha1, Error> {
220 let decoded = decode_secret(secret)?;
221 let mut mac = HmacSha1::new_from_slice(&decoded[..])
222 .map_err(|_e| Error::EmptySecret)?;
223
224 mac.update(bytes);
225 Ok(mac)
226}
227
228fn subtract_unsigned_signed(u: u64, i: i64) -> u64 {
230 if i >= 0 {
231 u.saturating_sub(i as u64)
232 } else {
233 u.saturating_add((-i) as u64)
234 }
235}
236
237fn get_offset_timestamp(time_offset: Option<i64>) -> Result<u64, Error> {
239 let current_timestamp = current_timestamp()?;
240 let time_offset = time_offset.unwrap_or(0);
241 let timestamp = subtract_unsigned_signed(
242 current_timestamp,
243 time_offset,
244 );
245
246 Ok(timestamp)
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn generates_confirmation_key_for_time() {
255 let identity_secret: &'static str = "000000000000000000000000000=";
256 let timestamp = 1634603498;
257 let hash = generate_confirmation_key_for_time(
258 identity_secret,
259 Tag::Allow,
260 timestamp,
261 ).unwrap();
262
263 assert_eq!(hash, "9/OyNC3rk7VNsMFklzayOuznImU=");
264 }
265
266 #[test]
267 fn generating_a_code_works() {
268 let shared_secret = "000000000000000000000000000=";
269 let timestamp = 1634603498;
270 let code = generate_auth_code_for_time(shared_secret, timestamp).unwrap();
271
272 assert_eq!(code, "2C5H2");
273 }
274
275 #[test]
276 fn generating_a_code_from_hex_works() {
277 let shared_secret = "D34D34D34D34D34D34D34D34D34D34D34D34D34D";
279 let timestamp = 1634603498;
280 let code = generate_auth_code_for_time(shared_secret, timestamp).unwrap();
281
282 assert_eq!(code, "2C5H2");
283 }
284
285 #[test]
286 fn gets_device_id() {
287 let device_id = get_device_id(76561197960287930);
288
289 assert_eq!(device_id, "android:6d3f10d9-6369-a1ae-97a0-94df28b95192");
290 }
291}