Merge pull request #94 from dyc3/secrets
Implement a special type to hold 2fa secret that gets zeroed on drop
This commit is contained in:
commit
140b2abda6
10 changed files with 215 additions and 74 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -1552,6 +1552,16 @@ dependencies = [
|
||||||
"tendril",
|
"tendril",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secrecy"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
|
@ -1830,6 +1840,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rsa",
|
"rsa",
|
||||||
"scraper",
|
"scraper",
|
||||||
|
"secrecy",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"standback",
|
"standback",
|
||||||
|
@ -2430,9 +2441,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.2.0"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36"
|
checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zeroize_derive",
|
"zeroize_derive",
|
||||||
]
|
]
|
||||||
|
|
|
@ -243,21 +243,24 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_save_and_load_manifest() {
|
fn test_should_save_and_load_manifest() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new("steamguard-cli-test").unwrap();
|
let tmp_dir = TempDir::new("steamguard-cli-test")?;
|
||||||
let manifest_path = tmp_dir.path().join("manifest.json");
|
let manifest_path = tmp_dir.path().join("manifest.json");
|
||||||
|
println!("tempdir: {}", manifest_path.display());
|
||||||
let mut manifest = Manifest::new(manifest_path.as_path());
|
let mut manifest = Manifest::new(manifest_path.as_path());
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
account.account_name = "asdf1234".into();
|
account.account_name = "asdf1234".into();
|
||||||
account.revocation_code = "R12345".into();
|
account.revocation_code = "R12345".into();
|
||||||
account.shared_secret = "secret".into();
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
||||||
|
)?;
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
assert!(matches!(manifest.save(&None), Ok(_)));
|
manifest.save(&None)?;
|
||||||
|
|
||||||
let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap();
|
let mut loaded_manifest = Manifest::load(manifest_path.as_path())?;
|
||||||
assert_eq!(loaded_manifest.entries.len(), 1);
|
assert_eq!(loaded_manifest.entries.len(), 1);
|
||||||
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile");
|
||||||
assert!(matches!(loaded_manifest.load_accounts(&None), Ok(_)));
|
loaded_manifest.load_accounts(&None)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.entries.len(),
|
loaded_manifest.entries.len(),
|
||||||
loaded_manifest.accounts.len()
|
loaded_manifest.accounts.len()
|
||||||
|
@ -272,8 +275,11 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
||||||
"secret"
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
|
)?,
|
||||||
);
|
);
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -285,7 +291,10 @@ mod tests {
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
account.account_name = "asdf1234".into();
|
account.account_name = "asdf1234".into();
|
||||||
account.revocation_code = "R12345".into();
|
account.revocation_code = "R12345".into();
|
||||||
account.shared_secret = "secret".into();
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
|
manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
|
||||||
assert!(matches!(manifest.save(&passkey), Ok(_)));
|
assert!(matches!(manifest.save(&passkey), Ok(_)));
|
||||||
|
@ -308,7 +317,10 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
||||||
"secret"
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +333,10 @@ mod tests {
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
account.account_name = "asdf1234".into();
|
account.account_name = "asdf1234".into();
|
||||||
account.revocation_code = "R12345".into();
|
account.revocation_code = "R12345".into();
|
||||||
account.shared_secret = "secret".into();
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
account.uri = "otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj".into();
|
account.uri = "otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj".into();
|
||||||
account.token_gid = "asdf1234".into();
|
account.token_gid = "asdf1234".into();
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
|
@ -346,7 +361,10 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
||||||
"secret"
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -360,7 +378,10 @@ mod tests {
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
account.account_name = "asdf1234".into();
|
account.account_name = "asdf1234".into();
|
||||||
account.revocation_code = "R12345".into();
|
account.revocation_code = "R12345".into();
|
||||||
account.shared_secret = "secret".into();
|
account.shared_secret = steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
manifest.add_account(account);
|
manifest.add_account(account);
|
||||||
assert!(matches!(manifest.save(&None), Ok(_)));
|
assert!(matches!(manifest.save(&None), Ok(_)));
|
||||||
std::fs::remove_file(&manifest_path).unwrap();
|
std::fs::remove_file(&manifest_path).unwrap();
|
||||||
|
@ -391,7 +412,10 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
loaded_manifest.accounts[0].lock().unwrap().shared_secret,
|
||||||
"secret"
|
steamguard::token::TwoFactorSecret::parse_shared_secret(
|
||||||
|
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into()
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,12 +100,13 @@ impl EntryEncryptor for LegacySdaCompatible {
|
||||||
let origsize = plaintext.len();
|
let origsize = plaintext.len();
|
||||||
let buffersize: usize = (origsize / 16 + (if origsize % 16 == 0 { 0 } else { 1 })) * 16;
|
let buffersize: usize = (origsize / 16 + (if origsize % 16 == 0 { 0 } else { 1 })) * 16;
|
||||||
let mut buffer = vec![];
|
let mut buffer = vec![];
|
||||||
for chunk in plaintext.as_slice().chunks(256) {
|
for chunk in plaintext.as_slice().chunks(128) {
|
||||||
let chunksize = chunk.len();
|
let chunksize = chunk.len();
|
||||||
let buffersize = (chunksize / 16 + (if chunksize % 16 == 0 { 0 } else { 1 })) * 16;
|
let buffersize = (chunksize / 16 + (if chunksize % 16 == 0 { 0 } else { 1 })) * 16;
|
||||||
let mut chunkbuffer = vec![0xffu8; buffersize];
|
let mut chunkbuffer = vec![0xffu8; buffersize];
|
||||||
chunkbuffer[..chunksize].copy_from_slice(&chunk);
|
chunkbuffer[..chunksize].copy_from_slice(&chunk);
|
||||||
if buffersize != chunksize {
|
if buffersize != chunksize {
|
||||||
|
// pad the last chunk
|
||||||
chunkbuffer = Pkcs7::pad(&mut chunkbuffer, chunksize, buffersize)
|
chunkbuffer = Pkcs7::pad(&mut chunkbuffer, chunksize, buffersize)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
ruZBuGG4+r5RU4JPjjHgkBLB205+1qSJjikDR5jrOpZsYgPSdJV9N4Rp7OnOQ63YBSiu1JpoNIUrSxJnW4UaPBIkgbWHsAngSiuZAhkZq/FJuRie0OcGN2NnwuE2xZUQNfREqU4DTiq8VLRBqvHTueeI5pReJ1vGradhjOgyWVG4MxMlVylVTIPqmZf1NrfBQbEL6Nip3dRGdzXsLhtnxhr8/0meBNOwtk+5sm6b+XVEd81aSsQNYEK5RUdmzNbOdK+UGUTOgACw6rBoBUP3zpv7U0gEC7u+iIIv1CQXp0HUH4p06Edu8r0APDQHIgt8/WmnRtm6EAWfgo/RxbMoTFTKo6Qfa18baGCkQnNJPiiIIQc1e4/31mw9DbGhDfkYJL4O5A9wbWeSRg92qxe2d4odHx0NIfyZ9CsKfIYc4/azq0I7K3hjcpt5JgYOQowm4YrYMQrmkyw90HMAMcOHoKtMVU5i58JumY3cYAnf+skCOra29D0Py7k0mGqm+9W2OWmO+XE3QfhuOK0FwFts5umyVI5AK4qZG90ioPt6CHgDZKgCdzuV7iTWwhuhd+EYalcbuSAQHX5cnNVVt9Z+0c9t+fnjz0t9w8wChpKhRcFGUeMvzOFO0lhP5kneJ3N/xKo0bhO2spjzzUw8lXE5wFv7TFmr058m+rzW/ucvtcl6KSvvr0RGNBcwUo+G8Q4jvJNTDFhqT0ElnWpgF2Rxb9m5UKZz5bfLszlf0KbSO2ZXg9wO65/itqXQnKzq1ALI
|
ZQqJk4KW7pb5bndkra244z3ttIks58UplODn5IgljrOK3bKORSwrnZYb9Iv6YsirmrT0hQ3tx381GhQu4Nyj/PFHHCfFTrduaCoMLRrHDsmsexOW93Yo02acHXNfPSvxtjGfZpsuIZVlhVy8JDH/ESXp88cKn2zqjXQWu6pLnah1YPlpZuTfArw69+Em7V1OH1CoKnsYuhCo/x4u7fXhMNgDlRBvDbO4enGzaixonPu9er5Zp6iNEeuAUqmD0DHASygNmzBhUBHv8Avng8YKbvu611yQVT2KybnIoL+Q11Y36GoFhWskmG3lLwh/1OlReGwJ1iX0lDDthoel/Ygj6EC2+wkR8V7eMQf48R15xdBILYTzcrjtiuLCr9MBc/HM/ToEa3QCGwGkXvshR/meJ1BiqaRARKfvJcj4eMSpiUvhDe1QFXXjfXRdetJcknyJ8Pv6v10G/OV3ELYwdx2dYL5C+Ao4qj9QCjoD8bb/juCjtZoSxMncbm4T7ORbXs/Ulx+TEuOUmRAjxr+zaWzzO7ZfYJMhIPz+LSixKpaVmxP89DEK5LJ1T6jA50QmPft6AbOMuoq99haWH9lMgqrIfBB+ZNNHSEE9PwMUxhX/TQP3oJgmrnZcMV4nBOcovbWM5s+odu2mvzoAcRMHVxEztASbMBwdJ7amJvoRRrXTtp642FQHAe8pFPlW14x1hShXAO4YfYkmmAhLqlujammuQ6bRg7XjBvh0zE88i5UBRiRsH+MV35u3c0ciugUmbrAZ0u9Yfv19MNSMVNMsREDuxQ==
|
|
@ -1 +1 @@
|
||||||
{"encrypted":true,"first_run":true,"entries":[{"encryption_iv":"xLHUZJzinEfUwLzEzqfI7Q==","encryption_salt":"bcp2z6P88A0=","filename":"1234.maFile","steamid":1234}],"periodic_checking":false,"periodic_checking_interval":5,"periodic_checking_checkall":false,"auto_confirm_market_transactions":false,"auto_confirm_trades":false}
|
{"encrypted":true,"first_run":true,"entries":[{"encryption_iv":"ifChnv66eA+/dYqGsQMIOA==","encryption_salt":"O2K8FAOWK9c=","filename":"1234.maFile","steamid":1234}],"periodic_checking":false,"periodic_checking_interval":5,"periodic_checking_checkall":false,"auto_confirm_market_transactions":false,"auto_confirm_trades":false}
|
|
@ -1 +1 @@
|
||||||
{"shared_secret":"secret1234","serial_number":"kljasfhds","revocation_code":"R12345","uri":"otpauth://totp/Steam:example?secret=ASDF&issuer=Steam","server_time":1602522478,"account_name":"example","token_gid":"jkkjlhkhjgf","identity_secret":"kjsdlwowiqe=","secret_1":"sklduhfgsdlkjhf=","status":1,"device_id":"android:99d2ad0e-4bad-4247-b111-26393aae0be3","fully_enrolled":true,"Session":{"SessionID":"a;lskdjf","SteamLogin":"983498437543","SteamLoginSecure":"dlkjdsl;j%7C%32984730298","WebCookie":";lkjsed;klfjas98093","OAuthToken":"asdk;lf;dsjlkfd","SteamID":1234}}
|
{"shared_secret":"zvIayp3JPvtvX/QGHqsqKBk/44s=","serial_number":"kljasfhds","revocation_code":"R12345","uri":"otpauth://totp/Steam:example?secret=ASDF&issuer=Steam","server_time":1602522478,"account_name":"example","token_gid":"jkkjlhkhjgf","identity_secret":"kjsdlwowiqe=","secret_1":"sklduhfgsdlkjhf=","status":1,"device_id":"android:99d2ad0e-4bad-4247-b111-26393aae0be3","fully_enrolled":true,"Session":{"SessionID":"a;lskdjf","SteamLogin":"983498437543","SteamLoginSecure":"dlkjdsl;j%7C%32984730298","WebCookie":";lkjsed;klfjas98093","OAuthToken":"asdk;lf;dsjlkfd","SteamID":1234}}
|
|
@ -24,3 +24,4 @@ log = "0.4.14"
|
||||||
scraper = "0.12.0"
|
scraper = "0.12.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
thiserror = "1.0.26"
|
thiserror = "1.0.26"
|
||||||
|
secrecy = { version = "0.8", features = ["serde"] }
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::token::TwoFactorSecret;
|
||||||
pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError};
|
pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
pub use confirmation::{Confirmation, ConfirmationType};
|
pub use confirmation::{Confirmation, ConfirmationType};
|
||||||
|
@ -24,6 +25,7 @@ extern crate maplit;
|
||||||
mod accountlinker;
|
mod accountlinker;
|
||||||
mod confirmation;
|
mod confirmation;
|
||||||
pub mod steamapi;
|
pub mod steamapi;
|
||||||
|
pub mod token;
|
||||||
mod userlogin;
|
mod userlogin;
|
||||||
|
|
||||||
// const STEAMAPI_BASE: String = "https://api.steampowered.com";
|
// const STEAMAPI_BASE: String = "https://api.steampowered.com";
|
||||||
|
@ -42,7 +44,7 @@ pub struct SteamGuardAccount {
|
||||||
pub account_name: String,
|
pub account_name: String,
|
||||||
pub serial_number: String,
|
pub serial_number: String,
|
||||||
pub revocation_code: String,
|
pub revocation_code: String,
|
||||||
pub shared_secret: String,
|
pub shared_secret: TwoFactorSecret,
|
||||||
pub token_gid: String,
|
pub token_gid: String,
|
||||||
pub identity_secret: String,
|
pub identity_secret: String,
|
||||||
pub server_time: u64,
|
pub server_time: u64,
|
||||||
|
@ -58,12 +60,6 @@ fn build_time_bytes(time: i64) -> [u8; 8] {
|
||||||
return time.to_be_bytes();
|
return time.to_be_bytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_shared_secret(secret: String) -> anyhow::Result<[u8; 20]> {
|
|
||||||
ensure!(secret.len() != 0, "unable to parse empty shared secret");
|
|
||||||
let result = base64::decode(secret)?.try_into();
|
|
||||||
return Ok(result.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String {
|
fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String {
|
||||||
let decode: &[u8] = &base64::decode(&identity_secret).unwrap();
|
let decode: &[u8] = &base64::decode(&identity_secret).unwrap();
|
||||||
let time_bytes = build_time_bytes(time);
|
let time_bytes = build_time_bytes(time);
|
||||||
|
@ -80,7 +76,7 @@ impl SteamGuardAccount {
|
||||||
account_name: String::from(""),
|
account_name: String::from(""),
|
||||||
serial_number: String::from(""),
|
serial_number: String::from(""),
|
||||||
revocation_code: String::from(""),
|
revocation_code: String::from(""),
|
||||||
shared_secret: String::from(""),
|
shared_secret: TwoFactorSecret::new(),
|
||||||
token_gid: String::from(""),
|
token_gid: String::from(""),
|
||||||
identity_secret: String::from(""),
|
identity_secret: String::from(""),
|
||||||
server_time: 0,
|
server_time: 0,
|
||||||
|
@ -93,29 +89,7 @@ impl SteamGuardAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_code(&self, time: i64) -> String {
|
pub fn generate_code(&self, time: i64) -> String {
|
||||||
let steam_guard_code_translations: [u8; 26] = [
|
return self.shared_secret.generate_code(time);
|
||||||
50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84,
|
|
||||||
86, 87, 88, 89,
|
|
||||||
];
|
|
||||||
|
|
||||||
// this effectively makes it so that it creates a new code every 30 seconds.
|
|
||||||
let time_bytes: [u8; 8] = build_time_bytes(time / 30i64);
|
|
||||||
let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()).unwrap();
|
|
||||||
let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes);
|
|
||||||
let mut code_array: [u8; 5] = [0; 5];
|
|
||||||
let b = (hashed_data[19] & 0xF) as usize;
|
|
||||||
let mut code_point: i32 = ((hashed_data[b] & 0x7F) as i32) << 24
|
|
||||||
| ((hashed_data[b + 1] & 0xFF) as i32) << 16
|
|
||||||
| ((hashed_data[b + 2] & 0xFF) as i32) << 8
|
|
||||||
| ((hashed_data[b + 3] & 0xFF) as i32);
|
|
||||||
|
|
||||||
for i in 0..5 {
|
|
||||||
code_array[i] = steam_guard_code_translations
|
|
||||||
[code_point as usize % steam_guard_code_translations.len()];
|
|
||||||
code_point /= steam_guard_code_translations.len() as i32;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> {
|
fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> {
|
||||||
|
@ -306,26 +280,6 @@ fn parse_confirmations(text: String) -> anyhow::Result<Vec<Confirmation>> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_build_time_bytes() {
|
|
||||||
let t1 = build_time_bytes(1617591917i64);
|
|
||||||
let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109];
|
|
||||||
assert!(
|
|
||||||
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
|
|
||||||
"Arrays are not equal, got {:?}",
|
|
||||||
t1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_generate_code() {
|
|
||||||
let mut account = SteamGuardAccount::new();
|
|
||||||
account.shared_secret = String::from("zvIayp3JPvtvX/QGHqsqKBk/44s=");
|
|
||||||
|
|
||||||
let code = account.generate_code(1616374841i64);
|
|
||||||
assert_eq!(code, "2F9J5")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_confirmation_hash_for_time() {
|
fn test_generate_confirmation_hash_for_time() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::token::TwoFactorSecret;
|
||||||
|
use crate::SteamGuardAccount;
|
||||||
use log::*;
|
use log::*;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
blocking::RequestBuilder,
|
blocking::RequestBuilder,
|
||||||
|
@ -12,8 +14,6 @@ use std::iter::FromIterator;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::SteamGuardAccount;
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
||||||
static ref STEAM_API_BASE: String = "https://api.steampowered.com".into();
|
static ref STEAM_API_BASE: String = "https://api.steampowered.com".into();
|
||||||
|
@ -609,9 +609,9 @@ pub struct AddAuthenticatorResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddAuthenticatorResponse {
|
impl AddAuthenticatorResponse {
|
||||||
pub fn to_steam_guard_account(&self) -> SteamGuardAccount {
|
pub fn to_steam_guard_account(self) -> SteamGuardAccount {
|
||||||
SteamGuardAccount {
|
SteamGuardAccount {
|
||||||
shared_secret: self.shared_secret.clone(),
|
shared_secret: TwoFactorSecret::parse_shared_secret(self.shared_secret).unwrap(),
|
||||||
serial_number: self.serial_number.clone(),
|
serial_number: self.serial_number.clone(),
|
||||||
revocation_code: self.revocation_code.clone(),
|
revocation_code: self.revocation_code.clone(),
|
||||||
uri: self.uri.clone(),
|
uri: self.uri.clone(),
|
||||||
|
|
150
steamguard/src/token.rs
Normal file
150
steamguard/src/token.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use secrecy::{ExposeSecret, Secret};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TwoFactorSecret(Secret<[u8; 20]>);
|
||||||
|
// pub struct TwoFactorSecret(Secret<Vec<u8>>);
|
||||||
|
|
||||||
|
impl TwoFactorSecret {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
return Self([0u8; 20].into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_shared_secret(secret: String) -> anyhow::Result<Self> {
|
||||||
|
ensure!(secret.len() != 0, "unable to parse empty shared secret");
|
||||||
|
let result: [u8; 20] = base64::decode(secret)?.try_into().unwrap();
|
||||||
|
return Ok(Self(result.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a 5 character 2FA code to that can be used to log in to Steam.
|
||||||
|
pub fn generate_code(&self, time: i64) -> String {
|
||||||
|
let steam_guard_code_translations: [u8; 26] = [
|
||||||
|
50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84,
|
||||||
|
86, 87, 88, 89,
|
||||||
|
];
|
||||||
|
|
||||||
|
// this effectively makes it so that it creates a new code every 30 seconds.
|
||||||
|
let time_bytes: [u8; 8] = build_time_bytes(time / 30i64);
|
||||||
|
let hashed_data = hmacsha1::hmac_sha1(self.0.expose_secret(), &time_bytes);
|
||||||
|
let mut code_array: [u8; 5] = [0; 5];
|
||||||
|
let b = (hashed_data[19] & 0xF) as usize;
|
||||||
|
let mut code_point: i32 = ((hashed_data[b] & 0x7F) as i32) << 24
|
||||||
|
| ((hashed_data[b + 1] & 0xFF) as i32) << 16
|
||||||
|
| ((hashed_data[b + 2] & 0xFF) as i32) << 8
|
||||||
|
| ((hashed_data[b + 3] & 0xFF) as i32);
|
||||||
|
|
||||||
|
for i in 0..5 {
|
||||||
|
code_array[i] = steam_guard_code_translations
|
||||||
|
[code_point as usize % steam_guard_code_translations.len()];
|
||||||
|
code_point /= steam_guard_code_translations.len() as i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for TwoFactorSecret {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(base64::encode(&self.0.expose_secret()).as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for TwoFactorSecret {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Ok(TwoFactorSecret::parse_shared_secret(String::deserialize(deserializer)?).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for TwoFactorSecret {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
return self.0.expose_secret() == other.0.expose_secret();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for TwoFactorSecret {}
|
||||||
|
|
||||||
|
fn build_time_bytes(time: i64) -> [u8; 8] {
|
||||||
|
return time.to_be_bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize() -> anyhow::Result<()> {
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
struct FooBar {
|
||||||
|
secret: TwoFactorSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret = FooBar {
|
||||||
|
secret: TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized = serde_json::to_string(&secret)?;
|
||||||
|
assert_eq!(serialized, "{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}");
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize() -> anyhow::Result<()> {
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
struct FooBar {
|
||||||
|
secret: TwoFactorSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret: FooBar =
|
||||||
|
serde_json::from_str(&"{\"secret\":\"zvIayp3JPvtvX/QGHqsqKBk/44s=\"}")?;
|
||||||
|
|
||||||
|
let code = secret.secret.generate_code(1616374841i64);
|
||||||
|
assert_eq!(code, "2F9J5");
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_and_deserialize() -> anyhow::Result<()> {
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
struct FooBar {
|
||||||
|
secret: TwoFactorSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret = FooBar {
|
||||||
|
secret: TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized = serde_json::to_string(&secret)?;
|
||||||
|
let deserialized: FooBar = serde_json::from_str(&serialized)?;
|
||||||
|
assert_eq!(deserialized, secret);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_time_bytes() {
|
||||||
|
let t1 = build_time_bytes(1617591917i64);
|
||||||
|
let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109];
|
||||||
|
assert!(
|
||||||
|
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
|
||||||
|
"Arrays are not equal, got {:?}",
|
||||||
|
t1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_code() -> anyhow::Result<()> {
|
||||||
|
let secret = TwoFactorSecret::parse_shared_secret("zvIayp3JPvtvX/QGHqsqKBk/44s=".into())?;
|
||||||
|
|
||||||
|
let code = secret.generate_code(1616374841i64);
|
||||||
|
assert_eq!(code, "2F9J5");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue