From c98ff1c91470d21600827e5f70fcf5c32c85922a Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 14 Aug 2021 23:45:17 -0400 Subject: [PATCH 01/18] allow load_accounts to take encryption passkey --- src/accountmanager.rs | 6 +++--- src/main.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 6c09897..11fb010 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -84,7 +84,7 @@ impl Manifest { return Ok(manifest); } - pub fn load_accounts(&mut self) -> anyhow::Result<()> { + pub fn load_accounts(&mut self, passkey: Option<&str>) -> anyhow::Result<()> { for entry in &mut self.entries { let path = Path::new(&self.folder).join(&entry.filename); debug!("loading account: {:?}", path); @@ -187,7 +187,7 @@ mod tests { let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); - assert!(matches!(loaded_manifest.load_accounts(), Ok(_))); + assert!(matches!(loaded_manifest.load_accounts(None), Ok(_))); assert_eq!( loaded_manifest.entries.len(), loaded_manifest.accounts.len() @@ -257,7 +257,7 @@ mod tests { assert!(matches!(result, Ok(_))); let mut manifest = result.unwrap(); assert!(matches!(manifest.entries.last().unwrap().encryption, None)); - assert!(matches!(manifest.load_accounts(), Ok(_))); + assert!(matches!(manifest.load_accounts(None), Ok(_))); assert_eq!( manifest.entries.last().unwrap().account_name, manifest diff --git a/src/main.rs b/src/main.rs index 2b57a60..3799275 100644 --- a/src/main.rs +++ b/src/main.rs @@ -173,7 +173,7 @@ fn main() { } manifest - .load_accounts() + .load_accounts(matches.value_of("passkey")) .expect("Failed to load accounts in manifest"); if matches.is_present("setup") { From edb2663baf17994c93f173e638d90ac6bfc75c50 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 15 Aug 2021 02:44:16 -0400 Subject: [PATCH 02/18] add test fixtures for encrypted maFiles --- src/fixtures/maFiles/1-account-encrypted/1234.maFile | 1 + src/fixtures/maFiles/1-account-encrypted/manifest.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/fixtures/maFiles/1-account-encrypted/1234.maFile create mode 100644 src/fixtures/maFiles/1-account-encrypted/manifest.json diff --git a/src/fixtures/maFiles/1-account-encrypted/1234.maFile b/src/fixtures/maFiles/1-account-encrypted/1234.maFile new file mode 100644 index 0000000..1ee5507 --- /dev/null +++ b/src/fixtures/maFiles/1-account-encrypted/1234.maFile @@ -0,0 +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 \ No newline at end of file diff --git a/src/fixtures/maFiles/1-account-encrypted/manifest.json b/src/fixtures/maFiles/1-account-encrypted/manifest.json new file mode 100644 index 0000000..fed7737 --- /dev/null +++ b/src/fixtures/maFiles/1-account-encrypted/manifest.json @@ -0,0 +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} \ No newline at end of file From 7d7dd6701e757431812f8782bfbc9b3595c013ac Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 15 Aug 2021 11:52:54 -0400 Subject: [PATCH 03/18] add broken encrypted maFiles test --- Cargo.lock | 80 +++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 ++ src/accountmanager.rs | 79 +++++++++++++++++++++++++++++++++++++++--- src/main.rs | 2 ++ 4 files changed, 158 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e46c786..accc720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495ee669413bfbe9e8cace80f4d3d78e6d8c8d99579f97fb93bde351b185f2d4" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -92,6 +104,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding", + "cipher", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "bumpalo" version = "3.6.1" @@ -135,6 +163,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "clap" version = "2.33.3" @@ -211,6 +248,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -690,9 +736,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.90" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" +checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" [[package]] name = "libm" @@ -896,6 +942,12 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.33" @@ -1337,6 +1389,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rpassword" version = "5.0.1" @@ -1706,8 +1773,10 @@ dependencies = [ name = "steamguard-cli" version = "0.2.0" dependencies = [ + "aes", "anyhow", "base64", + "block-modes", "clap", "cookie", "dirs", @@ -1717,6 +1786,7 @@ dependencies = [ "rand 0.8.4", "regex", "reqwest", + "ring", "rpassword", "rsa", "serde", @@ -2082,6 +2152,12 @@ dependencies = [ "void", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.1" diff --git a/Cargo.toml b/Cargo.toml index 639b619..aaba523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,9 @@ uuid = { version = "0.8", features = ["v4"] } termion = "1.5.6" steamguard = { path = "./steamguard" } dirs = "3.0.2" +ring = "0.16.20" +aes = "0.7.4" +block-modes = "0.8.1" [dev-dependencies] tempdir = "0.3" diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 11fb010..bf1e819 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -1,11 +1,17 @@ +use aes::Aes256; +use block_modes::block_padding::Pkcs7; +use block_modes::{BlockMode, Cbc}; use log::*; +use ring::pbkdf2; use serde::{Deserialize, Serialize}; -use std::io::{BufReader, Write}; +use std::fs::File; +use std::io::{BufReader, Read, Write}; use std::path::Path; use std::sync::{Arc, Mutex}; -use std::{cell::Cell, fs::File}; use steamguard::SteamGuardAccount; +type Aes256Cbc = Cbc; + #[derive(Debug, Serialize, Deserialize)] pub struct Manifest { pub encrypted: bool, @@ -89,8 +95,25 @@ impl Manifest { let path = Path::new(&self.folder).join(&entry.filename); debug!("loading account: {:?}", path); let file = File::open(path)?; - let reader = BufReader::new(file); - let account: SteamGuardAccount = serde_json::from_reader(reader)?; + let mut reader = BufReader::new(file); + let account: SteamGuardAccount; + match (passkey, entry.encryption.as_ref()) { + (Some(passkey), Some(params)) => { + let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; + let iv = base64::decode(¶ms.iv)?; + let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + let mut ciphertext: Vec = vec![]; + reader.read_to_end(&mut ciphertext)?; + let decrypted = cipher.decrypt(&mut ciphertext)?; + account = serde_json::from_slice(decrypted)?; + } + (None, Some(_)) => { + bail!("maFiles are encrypted, but no passkey was provided."); + } + (_, None) => { + account = serde_json::from_reader(reader)?; + } + }; entry.account_name = account.account_name.clone(); self.accounts.push(Arc::new(Mutex::new(account))); } @@ -159,6 +182,25 @@ impl Manifest { } } +const PBKDF2_ITERATIONS: u32 = 50000; +const SALT_LENGTH: usize = 8; +const KEY_SIZE_BYTES: usize = 32; +const IV_LENGTH: usize = 16; + +fn get_encryption_key(passkey: &String, salt: &String) -> anyhow::Result<[u8; KEY_SIZE_BYTES]> { + let password_bytes = passkey.as_bytes(); + let salt_bytes = base64::decode(salt)?; + let mut full_key: [u8; KEY_SIZE_BYTES] = [0u8; KEY_SIZE_BYTES]; + pbkdf2::derive( + pbkdf2::PBKDF2_HMAC_SHA256, + std::num::NonZeroU32::new(PBKDF2_ITERATIONS).unwrap(), + &salt_bytes, + password_bytes, + &mut full_key, + ); + return Ok(full_key); +} + #[cfg(test)] mod tests { use super::*; @@ -269,4 +311,33 @@ mod tests { .account_name ); } + + #[test] + fn test_sda_compatibility_1_encrypted() { + let path = Path::new("src/fixtures/maFiles/1-account-encrypted/manifest.json"); + assert!(path.is_file()); + let result = Manifest::load(path); + assert!(matches!(result, Ok(_))); + let mut manifest = result.unwrap(); + assert!(matches!( + manifest.entries.last().unwrap().encryption, + Some(_) + )); + let result = manifest.load_accounts(Some("password")); + assert!( + matches!(result, Ok(_)), + "error when loading accounts: {:?}", + result.unwrap_err() + ); + assert_eq!( + manifest.entries.last().unwrap().account_name, + manifest + .accounts + .last() + .unwrap() + .lock() + .unwrap() + .account_name + ); + } } diff --git a/src/main.rs b/src/main.rs index 3799275..7300fec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,9 @@ use steamguard::{ extern crate lazy_static; #[macro_use] extern crate anyhow; +extern crate base64; extern crate dirs; +extern crate ring; mod accountmanager; mod demos; mod tui; From 8f6a1d83450737c69a6c54fe0ad186b79ee3f6b2 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 15 Aug 2021 13:41:11 -0400 Subject: [PATCH 04/18] add encryption key tests to verify compatibility --- src/accountmanager.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index bf1e819..9eb53ab 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -192,7 +192,7 @@ fn get_encryption_key(passkey: &String, salt: &String) -> anyhow::Result<[u8; KE let salt_bytes = base64::decode(salt)?; let mut full_key: [u8; KEY_SIZE_BYTES] = [0u8; KEY_SIZE_BYTES]; pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA256, + pbkdf2::PBKDF2_HMAC_SHA1, std::num::NonZeroU32::new(PBKDF2_ITERATIONS).unwrap(), &salt_bytes, password_bytes, @@ -341,3 +341,26 @@ mod tests { ); } } + +#[cfg(test)] +mod encryption_tests { + use super::*; + + /// This test ensures compatibility with SteamDesktopAuthenticator and with previous versions of steamguard-cli + #[test] + fn test_encryption_key() { + assert_eq!( + get_encryption_key(&"password".into(), &"GMhL0N2hqXg=".into()).unwrap(), + base64::decode("KtiRa4/OxW83MlB6URf+Z8rAGj7CBY+pDlwD/NuVo6Y=") + .unwrap() + .as_slice() + ); + + assert_eq!( + get_encryption_key(&"password".into(), &"wTzTE9A6aN8=".into()).unwrap(), + base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=") + .unwrap() + .as_slice() + ); + } +} From 02d8cade2a7c650481fc51da7aeec7c837852019 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 15 Aug 2021 20:04:29 -0400 Subject: [PATCH 05/18] correctly implement SDA compatible decryption --- Cargo.lock | 8 ++++---- src/accountmanager.rs | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index accc720..c9c29b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -950,9 +950,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.33" +version = "0.10.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" dependencies = [ "bitflags", "cfg-if", @@ -970,9 +970,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-sys" -version = "0.9.61" +version = "0.9.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" dependencies = [ "autocfg 1.0.1", "cc", diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 9eb53ab..59f17dd 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -1,5 +1,5 @@ use aes::Aes256; -use block_modes::block_padding::Pkcs7; +use block_modes::block_padding::NoPadding; use block_modes::{BlockMode, Cbc}; use log::*; use ring::pbkdf2; @@ -10,7 +10,7 @@ use std::path::Path; use std::sync::{Arc, Mutex}; use steamguard::SteamGuardAccount; -type Aes256Cbc = Cbc; +type Aes256Cbc = Cbc; #[derive(Debug, Serialize, Deserialize)] pub struct Manifest { @@ -102,9 +102,15 @@ impl Manifest { let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + let mut ciphertext: Vec = vec![]; reader.read_to_end(&mut ciphertext)?; - let decrypted = cipher.decrypt(&mut ciphertext)?; + ciphertext = base64::decode(ciphertext)?; + let size: usize = ciphertext.len() / 16 + 1; + let mut buffer = vec![0xffu8; 16 * size]; + buffer[..ciphertext.len()].copy_from_slice(&ciphertext); + let decrypted = cipher.clone().decrypt(&mut buffer[..ciphertext.len()])?; + account = serde_json::from_slice(decrypted)?; } (None, Some(_)) => { From 1b1f12f423b5a73a679478cc5b9f4f43425749d8 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 15 Aug 2021 23:20:49 -0400 Subject: [PATCH 06/18] implement saving encrypted maFiles --- src/accountmanager.rs | 101 +++++++++++++++++++++++++++++++++++++----- src/main.rs | 32 ++++++++++--- 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 59f17dd..5a2354c 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -1,8 +1,9 @@ use aes::Aes256; -use block_modes::block_padding::NoPadding; +use block_modes::block_padding::{NoPadding, Padding, Pkcs7}; use block_modes::{BlockMode, Cbc}; use log::*; use ring::pbkdf2; +use ring::rand::SecureRandom; use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{BufReader, Read, Write}; @@ -103,15 +104,19 @@ impl Manifest { let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + // This sucks. let mut ciphertext: Vec = vec![]; reader.read_to_end(&mut ciphertext)?; ciphertext = base64::decode(ciphertext)?; let size: usize = ciphertext.len() / 16 + 1; let mut buffer = vec![0xffu8; 16 * size]; buffer[..ciphertext.len()].copy_from_slice(&ciphertext); - let decrypted = cipher.clone().decrypt(&mut buffer[..ciphertext.len()])?; + let decrypted = cipher.decrypt(&mut buffer)?; + let mut padded = &decrypted[..ciphertext.len()]; // This padding doesn't make any sense. + let unpadded = Pkcs7::unpad(&mut padded).unwrap(); - account = serde_json::from_slice(decrypted)?; + let s = std::str::from_utf8(&unpadded).unwrap(); + account = serde_json::from_str(&s)?; } (None, Some(_)) => { bail!("maFiles are encrypted, but no passkey was provided."); @@ -161,21 +166,47 @@ impl Manifest { self.entries.remove(index); } - pub fn save(&self) -> anyhow::Result<()> { + pub fn save(&self, passkey: Option<&str>) -> anyhow::Result<()> { ensure!( self.entries.len() == self.accounts.len(), "Manifest entries don't match accounts." ); for (entry, account) in self.entries.iter().zip(&self.accounts) { debug!("saving {}", entry.filename); - let serialized = serde_json::to_string(account.as_ref())?; + let serialized = serde_json::to_vec(account.as_ref())?; ensure!( serialized.len() > 2, "Something extra weird happened and the account was serialized into nothing." ); + + let final_buffer: Vec; + match (passkey, entry.encryption.as_ref()) { + (Some(passkey), Some(params)) => { + let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; + let iv = base64::decode(¶ms.iv)?; + let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + + let plaintext = serialized; + let size: usize = plaintext.len() / 16 + 1; + let mut buffer = vec![0xffu8; 16 * size]; + assert!(plaintext.len() < 16 * size); + buffer[..plaintext.len()].copy_from_slice(&plaintext.as_slice()); + let mut padded = Pkcs7::pad(&mut buffer, plaintext.len(), 16 * size).unwrap(); + let ciphertext = cipher.encrypt(&mut padded, 16 * size)?; + + final_buffer = base64::encode(&ciphertext).as_bytes().to_vec(); + } + (None, Some(_)) => { + bail!("maFiles are encrypted, but no passkey was provided."); + } + (_, None) => { + final_buffer = serialized; + } + }; + let path = Path::new(&self.folder).join(&entry.filename); let mut file = File::create(path)?; - file.write_all(serialized.as_bytes())?; + file.write_all(final_buffer.as_slice())?; file.sync_data()?; } debug!("saving manifest"); @@ -188,7 +219,7 @@ impl Manifest { } } -const PBKDF2_ITERATIONS: u32 = 50000; +const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive. Is this needed to remain compatible with SDA? const SALT_LENGTH: usize = 8; const KEY_SIZE_BYTES: usize = 32; const IV_LENGTH: usize = 16; @@ -207,6 +238,20 @@ fn get_encryption_key(passkey: &String, salt: &String) -> anyhow::Result<[u8; KE return Ok(full_key); } +impl EntryEncryptionParams { + pub fn generate() -> EntryEncryptionParams { + let rng = ring::rand::SystemRandom::new(); + let mut salt = [0u8; SALT_LENGTH]; + let mut iv = [0u8; IV_LENGTH]; + rng.fill(&mut salt).expect("Unable to generate salt."); + rng.fill(&mut iv).expect("Unable to generate IV."); + EntryEncryptionParams { + salt: base64::encode(salt), + iv: base64::encode(iv), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -217,7 +262,7 @@ mod tests { let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let manifest_path = tmp_dir.path().join("manifest.json"); let manifest = Manifest::new(manifest_path.as_path()); - assert!(matches!(manifest.save(), Ok(_))); + assert!(matches!(manifest.save(None), Ok(_))); } #[test] @@ -230,7 +275,7 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(), Ok(_))); + assert!(matches!(manifest.save(None), Ok(_))); let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); assert_eq!(loaded_manifest.entries.len(), 1); @@ -254,6 +299,42 @@ mod tests { ); } + #[test] + fn test_should_save_and_load_manifest_encrypted() { + let passkey: Option<&str> = Some("password"); + let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); + let manifest_path = tmp_dir.path().join("manifest.json"); + let mut manifest = Manifest::new(manifest_path.as_path()); + let mut account = SteamGuardAccount::new(); + account.account_name = "asdf1234".into(); + account.revocation_code = "R12345".into(); + account.shared_secret = "secret".into(); + manifest.add_account(account); + manifest.entries[0].encryption = Some(EntryEncryptionParams::generate()); + assert!(matches!(manifest.save(passkey), Ok(_))); + + let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); + assert_eq!(loaded_manifest.entries.len(), 1); + assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); + assert!(matches!(loaded_manifest.load_accounts(passkey), Ok(_))); + assert_eq!( + loaded_manifest.entries.len(), + loaded_manifest.accounts.len() + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().account_name, + "asdf1234" + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().revocation_code, + "R12345" + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().shared_secret, + "secret" + ); + } + #[test] fn test_should_import() { let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); @@ -264,7 +345,7 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(), Ok(_))); + assert!(matches!(manifest.save(None), Ok(_))); std::fs::remove_file(&manifest_path).unwrap(); let mut loaded_manifest = Manifest::new(manifest_path.as_path()); diff --git a/src/main.rs b/src/main.rs index 7300fec..427809a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,6 +103,14 @@ fn cli() -> App<'static, 'static> { App::new("remove") .about("Remove the authenticator from an account.") ) + .subcommand( + App::new("encrypt") + .about("Encrypt maFiles.") + ) + .subcommand( + App::new("decrypt") + .about("Decrypt maFiles.") + ) .subcommand( App::new("debug") .arg( @@ -174,8 +182,10 @@ fn main() { } } + let passkey = matches.value_of("passkey"); + manifest - .load_accounts(matches.value_of("passkey")) + .load_accounts(passkey) .expect("Failed to load accounts in manifest"); if matches.is_present("setup") { @@ -217,7 +227,7 @@ fn main() { } } manifest.add_account(account); - match manifest.save() { + match manifest.save(passkey) { Ok(_) => {} Err(err) => { error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); @@ -262,7 +272,7 @@ fn main() { } println!("Authenticator finalized."); - match manifest.save() { + match manifest.save(None) { Ok(_) => {} Err(err) => { println!( @@ -286,7 +296,13 @@ fn main() { } } - manifest.save().expect("Failed to save manifest."); + manifest.save(passkey).expect("Failed to save manifest."); + return; + } else if matches.is_present("encrypt") { + for entry in &mut manifest.entries { + entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); + } + manifest.save(passkey).expect("Failed to save manifest."); return; } @@ -363,7 +379,9 @@ fn main() { } } - manifest.save().expect("Failed to save manifest"); + manifest + .save(passkey) + .expect("Failed to save manifest"); } else if let Some(_) = matches.subcommand_matches("remove") { println!( "This will remove the mobile authenticator from {} accounts: {}", @@ -411,7 +429,9 @@ fn main() { manifest.remove_account(account_name); } - manifest.save().expect("Failed to save manifest."); + manifest + .save(passkey) + .expect("Failed to save manifest."); } else { let server_time = steamapi::get_server_time(); for account in selected_accounts { From 531e69ea880e354c56224aa853d7a8a081deddc6 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 16 Aug 2021 21:13:58 -0400 Subject: [PATCH 07/18] prompt for passkey if not provided --- Cargo.lock | 1 + Cargo.toml | 1 + src/accountmanager.rs | 70 ++++++++++++++++++++++++++++++++++--------- src/main.rs | 37 ++++++++++++++--------- 4 files changed, 81 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9c29b9..432943f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1797,6 +1797,7 @@ dependencies = [ "tempdir", "termion", "text_io", + "thiserror", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index aaba523..2e1daea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ dirs = "3.0.2" ring = "0.16.20" aes = "0.7.4" block-modes = "0.8.1" +thiserror = "1.0.26" [dev-dependencies] tempdir = "0.3" diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 5a2354c..2d7bacc 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -10,6 +10,7 @@ use std::io::{BufReader, Read, Write}; use std::path::Path; use std::sync::{Arc, Mutex}; use steamguard::SteamGuardAccount; +use thiserror::Error; type Aes256Cbc = Cbc; @@ -91,7 +92,10 @@ impl Manifest { return Ok(manifest); } - pub fn load_accounts(&mut self, passkey: Option<&str>) -> anyhow::Result<()> { + pub fn load_accounts( + &mut self, + passkey: &Option, + ) -> anyhow::Result<(), ManifestAccountLoadError> { for entry in &mut self.entries { let path = Path::new(&self.folder).join(&entry.filename); debug!("loading account: {:?}", path); @@ -112,14 +116,17 @@ impl Manifest { let mut buffer = vec![0xffu8; 16 * size]; buffer[..ciphertext.len()].copy_from_slice(&ciphertext); let decrypted = cipher.decrypt(&mut buffer)?; - let mut padded = &decrypted[..ciphertext.len()]; // This padding doesn't make any sense. - let unpadded = Pkcs7::unpad(&mut padded).unwrap(); + // This padding doesn't make any sense. + let mut padded = &decrypted[..ciphertext.len()]; + // Also, UnpadError does not implement Error for some fucking reason, so we have to do this. + let unpadded = Pkcs7::unpad(&mut padded) + .map_err(|_| ManifestAccountLoadError::DecryptionFailed)?; let s = std::str::from_utf8(&unpadded).unwrap(); account = serde_json::from_str(&s)?; } (None, Some(_)) => { - bail!("maFiles are encrypted, but no passkey was provided."); + return Err(ManifestAccountLoadError::MissingPasskey); } (_, None) => { account = serde_json::from_reader(reader)?; @@ -166,7 +173,7 @@ impl Manifest { self.entries.remove(index); } - pub fn save(&self, passkey: Option<&str>) -> anyhow::Result<()> { + pub fn save(&self, passkey: &Option) -> anyhow::Result<()> { ensure!( self.entries.len() == self.accounts.len(), "Manifest entries don't match accounts." @@ -252,6 +259,41 @@ impl EntryEncryptionParams { } } +#[derive(Debug, Error)] +pub enum ManifestAccountLoadError { + #[error("Manifest accounts are encrypted, but no passkey was provided.")] + MissingPasskey, + #[error("Failed to decrypt account.")] + #[from(block_modes::block_padding::UnpadError)] + DecryptionFailed, + #[error("Failed to deserialize the account.")] + DeserializationFailed(#[from] serde_json::Error), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +/// For some reason, these errors do not get converted to `ManifestAccountLoadError`s, even though they get converted into `anyhow::Error` just fine. I am too lazy to figure out why right now. +impl From for ManifestAccountLoadError { + fn from(error: block_modes::BlockModeError) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for ManifestAccountLoadError { + fn from(error: base64::DecodeError) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for ManifestAccountLoadError { + fn from(error: block_modes::InvalidKeyIvLength) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for ManifestAccountLoadError { + fn from(error: std::io::Error) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} + #[cfg(test)] mod tests { use super::*; @@ -262,7 +304,7 @@ mod tests { let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let manifest_path = tmp_dir.path().join("manifest.json"); let manifest = Manifest::new(manifest_path.as_path()); - assert!(matches!(manifest.save(None), Ok(_))); + assert!(matches!(manifest.save(&None), Ok(_))); } #[test] @@ -275,12 +317,12 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(None), Ok(_))); + assert!(matches!(manifest.save(&None), Ok(_))); let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); - assert!(matches!(loaded_manifest.load_accounts(None), Ok(_))); + assert!(matches!(loaded_manifest.load_accounts(&None), Ok(_))); assert_eq!( loaded_manifest.entries.len(), loaded_manifest.accounts.len() @@ -301,7 +343,7 @@ mod tests { #[test] fn test_should_save_and_load_manifest_encrypted() { - let passkey: Option<&str> = Some("password"); + let passkey: Option = Some("password".into()); let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); let manifest_path = tmp_dir.path().join("manifest.json"); let mut manifest = Manifest::new(manifest_path.as_path()); @@ -311,12 +353,12 @@ mod tests { account.shared_secret = "secret".into(); manifest.add_account(account); manifest.entries[0].encryption = Some(EntryEncryptionParams::generate()); - assert!(matches!(manifest.save(passkey), Ok(_))); + assert!(matches!(manifest.save(&passkey), Ok(_))); let mut loaded_manifest = Manifest::load(manifest_path.as_path()).unwrap(); assert_eq!(loaded_manifest.entries.len(), 1); assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); - assert!(matches!(loaded_manifest.load_accounts(passkey), Ok(_))); + assert!(matches!(loaded_manifest.load_accounts(&passkey), Ok(_))); assert_eq!( loaded_manifest.entries.len(), loaded_manifest.accounts.len() @@ -345,7 +387,7 @@ mod tests { account.revocation_code = "R12345".into(); account.shared_secret = "secret".into(); manifest.add_account(account); - assert!(matches!(manifest.save(None), Ok(_))); + assert!(matches!(manifest.save(&None), Ok(_))); std::fs::remove_file(&manifest_path).unwrap(); let mut loaded_manifest = Manifest::new(manifest_path.as_path()); @@ -386,7 +428,7 @@ mod tests { assert!(matches!(result, Ok(_))); let mut manifest = result.unwrap(); assert!(matches!(manifest.entries.last().unwrap().encryption, None)); - assert!(matches!(manifest.load_accounts(None), Ok(_))); + assert!(matches!(manifest.load_accounts(&None), Ok(_))); assert_eq!( manifest.entries.last().unwrap().account_name, manifest @@ -410,7 +452,7 @@ mod tests { manifest.entries.last().unwrap().encryption, Some(_) )); - let result = manifest.load_accounts(Some("password")); + let result = manifest.load_accounts(&Some("password".into())); assert!( matches!(result, Ok(_)), "error when loading accounts: {:?}", diff --git a/src/main.rs b/src/main.rs index 427809a..1dbeae0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,11 +182,24 @@ fn main() { } } - let passkey = matches.value_of("passkey"); + let mut passkey: Option = matches.value_of("passkey").map(|s| s.into()); - manifest - .load_accounts(passkey) - .expect("Failed to load accounts in manifest"); + loop { + match manifest.load_accounts(&passkey) { + Ok(_) => break, + Err( + accountmanager::ManifestAccountLoadError::MissingPasskey + | accountmanager::ManifestAccountLoadError::DecryptionFailed, + ) => { + error!("Incorrect passkey"); + passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); + } + Err(e) => { + error!("Could not load accounts: {}", e); + return; + } + } + } if matches.is_present("setup") { println!("Log in to the account that you want to link to steamguard-cli"); @@ -227,7 +240,7 @@ fn main() { } } manifest.add_account(account); - match manifest.save(passkey) { + match manifest.save(&passkey) { Ok(_) => {} Err(err) => { error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); @@ -272,7 +285,7 @@ fn main() { } println!("Authenticator finalized."); - match manifest.save(None) { + match manifest.save(&None) { Ok(_) => {} Err(err) => { println!( @@ -296,13 +309,13 @@ fn main() { } } - manifest.save(passkey).expect("Failed to save manifest."); + manifest.save(&passkey).expect("Failed to save manifest."); return; } else if matches.is_present("encrypt") { for entry in &mut manifest.entries { entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); } - manifest.save(passkey).expect("Failed to save manifest."); + manifest.save(&passkey).expect("Failed to save manifest."); return; } @@ -379,9 +392,7 @@ fn main() { } } - manifest - .save(passkey) - .expect("Failed to save manifest"); + manifest.save(&passkey).expect("Failed to save manifest"); } else if let Some(_) = matches.subcommand_matches("remove") { println!( "This will remove the mobile authenticator from {} accounts: {}", @@ -429,9 +440,7 @@ fn main() { manifest.remove_account(account_name); } - manifest - .save(passkey) - .expect("Failed to save manifest."); + manifest.save(&passkey).expect("Failed to save manifest."); } else { let server_time = steamapi::get_server_time(); for account in selected_accounts { From f1158f9a3da52082cecd87b15f8d0c24962dcf46 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 17 Aug 2021 18:54:16 -0400 Subject: [PATCH 08/18] implement decrypt --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index 1dbeae0..a6c6a26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -317,6 +317,12 @@ fn main() { } manifest.save(&passkey).expect("Failed to save manifest."); return; + } else if matches.is_present("decrypt") { + for entry in &mut manifest.entries { + entry.encryption = None; + } + manifest.save(&passkey).expect("Failed to save manifest."); + return; } let mut selected_accounts: Vec>> = vec![]; From e1f86d3750ce982e983aaa8531563538d05c1aee Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 17 Aug 2021 19:04:02 -0400 Subject: [PATCH 09/18] fix passkey arg --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index a6c6a26..243a513 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,7 @@ fn cli() -> App<'static, 'static> { .long("passkey") .short("p") .help("Specify your encryption passkey.") + .takes_value(true) ) .arg( Arg::with_name("verbosity") From d3cdc309ed26095e31cf41b403ae523d5be01b4d Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 17 Aug 2021 19:20:57 -0400 Subject: [PATCH 10/18] prompt user for encryption passkey if not provided --- src/main.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 243a513..55be175 100644 --- a/src/main.rs +++ b/src/main.rs @@ -192,7 +192,9 @@ fn main() { accountmanager::ManifestAccountLoadError::MissingPasskey | accountmanager::ManifestAccountLoadError::DecryptionFailed, ) => { - error!("Incorrect passkey"); + if passkey.is_some() { + error!("Incorrect passkey"); + } passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); } Err(e) => { @@ -313,6 +315,16 @@ fn main() { manifest.save(&passkey).expect("Failed to save manifest."); return; } else if matches.is_present("encrypt") { + if passkey.is_none() { + loop { + passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); + let passkey_confirm = rpassword::prompt_password_stdout("Confirm encryption passkey: ").ok(); + if passkey == passkey_confirm { + break; + } + error!("Passkeys do not match, try again."); + } + } for entry in &mut manifest.entries { entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); } From 49aea800800e9cc979257b01f01f1cc1132e1367 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 19 Aug 2021 09:55:52 -0400 Subject: [PATCH 11/18] decryption makes slightly more sense now --- src/accountmanager.rs | 67 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 2d7bacc..8af81ea 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -112,14 +112,15 @@ impl Manifest { let mut ciphertext: Vec = vec![]; reader.read_to_end(&mut ciphertext)?; ciphertext = base64::decode(ciphertext)?; - let size: usize = ciphertext.len() / 16 + 1; + let size: usize = ciphertext.len() / 16 + (if ciphertext.len() % 16 == 0 { 0 } else { 1 }); let mut buffer = vec![0xffu8; 16 * size]; buffer[..ciphertext.len()].copy_from_slice(&ciphertext); - let decrypted = cipher.decrypt(&mut buffer)?; - // This padding doesn't make any sense. - let mut padded = &decrypted[..ciphertext.len()]; - // Also, UnpadError does not implement Error for some fucking reason, so we have to do this. - let unpadded = Pkcs7::unpad(&mut padded) + let mut decrypted = cipher.decrypt(&mut buffer)?; + if decrypted[0] != '{' as u8 { + return Err(ManifestAccountLoadError::DecryptionFailed); + } + // UnpadError does not implement Error for some fucking reason, so we have to do this. + let unpadded = Pkcs7::unpad(&mut decrypted) .map_err(|_| ManifestAccountLoadError::DecryptionFailed)?; let s = std::str::from_utf8(&unpadded).unwrap(); @@ -194,12 +195,14 @@ impl Manifest { let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; let plaintext = serialized; - let size: usize = plaintext.len() / 16 + 1; - let mut buffer = vec![0xffu8; 16 * size]; - assert!(plaintext.len() < 16 * size); - buffer[..plaintext.len()].copy_from_slice(&plaintext.as_slice()); - let mut padded = Pkcs7::pad(&mut buffer, plaintext.len(), 16 * size).unwrap(); - let ciphertext = cipher.encrypt(&mut padded, 16 * size)?; + let origsize = plaintext.len(); + let buffersize: usize = (plaintext.len() / 16 + 1) * 16; + let mut buffer = vec![0xffu8; buffersize]; + assert!(origsize < buffersize); + buffer[..origsize].copy_from_slice(&plaintext.as_slice()); + // The block that is being padded must not be larger than 255 bytes, otherwise padding will fail. + let mut padded = Pkcs7::pad(&mut buffer, origsize, buffersize).unwrap(); + let ciphertext = cipher.encrypt(&mut padded, buffersize)?; final_buffer = base64::encode(&ciphertext).as_bytes().to_vec(); } @@ -377,6 +380,46 @@ mod tests { ); } + #[test] + fn test_should_save_and_load_manifest_encrypted_longer() -> anyhow::Result<()> { + let passkey: Option = Some("password".into()); + let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); + let manifest_path = tmp_dir.path().join("manifest.json"); + let mut manifest = Manifest::new(manifest_path.as_path()); + let mut account = SteamGuardAccount::new(); + account.account_name = "asdf1234".into(); + account.revocation_code = "R12345".into(); + account.shared_secret = "secret".into(); + account.uri = "otpauth://;laksdjf;lkasdjf;lkasdj;flkasdjlkf;asjdlkfjslk;adjfl;kasdjf;lksdjflk;asjd;lfajs;ldkfjaslk;djf;lsakdjf;lksdj".into(); + account.token_gid = "asdf1234".into(); + manifest.add_account(account); + manifest.entries[0].encryption = Some(EntryEncryptionParams::generate()); + manifest.save(&passkey)?; + + let mut loaded_manifest = Manifest::load(manifest_path.as_path())?; + assert_eq!(loaded_manifest.entries.len(), 1); + assert_eq!(loaded_manifest.entries[0].filename, "asdf1234.maFile"); + loaded_manifest.load_accounts(&passkey)?; + assert_eq!( + loaded_manifest.entries.len(), + loaded_manifest.accounts.len() + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().account_name, + "asdf1234" + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().revocation_code, + "R12345" + ); + assert_eq!( + loaded_manifest.accounts[0].lock().unwrap().shared_secret, + "secret" + ); + + return Ok(()); + } + #[test] fn test_should_import() { let tmp_dir = TempDir::new("steamguard-cli-test").unwrap(); From aa1fa6a31841c3bb0448bac44f380598cb85347a Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 19 Aug 2021 12:54:52 -0400 Subject: [PATCH 12/18] fix not being able to encrypt file contents if it was longer than a certain amount of bytes --- src/accountmanager.rs | 28 ++++++++++++++++++++-------- src/main.rs | 3 ++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 8af81ea..c5669f5 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -112,7 +112,8 @@ impl Manifest { let mut ciphertext: Vec = vec![]; reader.read_to_end(&mut ciphertext)?; ciphertext = base64::decode(ciphertext)?; - let size: usize = ciphertext.len() / 16 + (if ciphertext.len() % 16 == 0 { 0 } else { 1 }); + let size: usize = + ciphertext.len() / 16 + (if ciphertext.len() % 16 == 0 { 0 } else { 1 }); let mut buffer = vec![0xffu8; 16 * size]; buffer[..ciphertext.len()].copy_from_slice(&ciphertext); let mut decrypted = cipher.decrypt(&mut buffer)?; @@ -194,15 +195,26 @@ impl Manifest { let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + // This also sucks. Extremely confusing. let plaintext = serialized; let origsize = plaintext.len(); - let buffersize: usize = (plaintext.len() / 16 + 1) * 16; - let mut buffer = vec![0xffu8; buffersize]; - assert!(origsize < buffersize); - buffer[..origsize].copy_from_slice(&plaintext.as_slice()); - // The block that is being padded must not be larger than 255 bytes, otherwise padding will fail. - let mut padded = Pkcs7::pad(&mut buffer, origsize, buffersize).unwrap(); - let ciphertext = cipher.encrypt(&mut padded, buffersize)?; + let buffersize: usize = + (origsize / 16 + (if origsize % 16 == 0 { 0 } else { 1 })) * 16; + let mut buffer = vec![]; + for chunk in plaintext.as_slice().chunks(256) { + let chunksize = chunk.len(); + let buffersize = + (chunksize / 16 + (if chunksize % 16 == 0 { 0 } else { 1 })) * 16; + let mut chunkbuffer = vec![0xffu8; buffersize]; + chunkbuffer[..chunksize].copy_from_slice(&chunk); + if buffersize != chunksize { + chunkbuffer = Pkcs7::pad(&mut chunkbuffer, chunksize, buffersize) + .unwrap() + .to_vec(); + } + buffer.append(&mut chunkbuffer); + } + let ciphertext = cipher.encrypt(&mut buffer, buffersize)?; final_buffer = base64::encode(&ciphertext).as_bytes().to_vec(); } diff --git a/src/main.rs b/src/main.rs index 55be175..c361e7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -318,7 +318,8 @@ fn main() { if passkey.is_none() { loop { passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); - let passkey_confirm = rpassword::prompt_password_stdout("Confirm encryption passkey: ").ok(); + let passkey_confirm = + rpassword::prompt_password_stdout("Confirm encryption passkey: ").ok(); if passkey == passkey_confirm { break; } From 58a13d8babb608658e3bd007fafe908f2d7bd615 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 19 Aug 2021 14:29:59 -0400 Subject: [PATCH 13/18] update comment --- src/accountmanager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index c5669f5..9312206 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -241,7 +241,7 @@ impl Manifest { } } -const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive. Is this needed to remain compatible with SDA? +const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive, but necessary to maintain compatibility with SteamDesktopAuthenticator. const SALT_LENGTH: usize = 8; const KEY_SIZE_BYTES: usize = 32; const IV_LENGTH: usize = 16; From e611e31ad3a4b223ee83def4eb175409a045a663 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 19 Aug 2021 16:54:18 -0400 Subject: [PATCH 14/18] move encryption stuff into new module --- src/accountmanager.rs | 134 +++------------------------- src/encryption.rs | 197 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 +- 3 files changed, 214 insertions(+), 122 deletions(-) create mode 100644 src/encryption.rs diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 9312206..f317679 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -1,9 +1,6 @@ -use aes::Aes256; -use block_modes::block_padding::{NoPadding, Padding, Pkcs7}; -use block_modes::{BlockMode, Cbc}; +pub use crate::encryption::EntryEncryptionParams; +use crate::encryption::EntryEncryptor; use log::*; -use ring::pbkdf2; -use ring::rand::SecureRandom; use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{BufReader, Read, Write}; @@ -12,12 +9,11 @@ use std::sync::{Arc, Mutex}; use steamguard::SteamGuardAccount; use thiserror::Error; -type Aes256Cbc = Cbc; - #[derive(Debug, Serialize, Deserialize)] pub struct Manifest { - pub encrypted: bool, pub entries: Vec, + /// Not really used, kept mostly for compatibility with SDA. + pub encrypted: bool, /// Not implemented, kept for compatibility with SDA. pub first_run: bool, /// Not implemented, kept for compatibility with SDA. @@ -48,14 +44,6 @@ pub struct ManifestEntry { pub encryption: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EntryEncryptionParams { - #[serde(rename = "encryption_iv")] - pub iv: String, - #[serde(rename = "encryption_salt")] - pub salt: String, -} - impl Default for Manifest { fn default() -> Self { Manifest { @@ -104,27 +92,12 @@ impl Manifest { let account: SteamGuardAccount; match (passkey, entry.encryption.as_ref()) { (Some(passkey), Some(params)) => { - let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; - let iv = base64::decode(¶ms.iv)?; - let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; - - // This sucks. let mut ciphertext: Vec = vec![]; reader.read_to_end(&mut ciphertext)?; - ciphertext = base64::decode(ciphertext)?; - let size: usize = - ciphertext.len() / 16 + (if ciphertext.len() % 16 == 0 { 0 } else { 1 }); - let mut buffer = vec![0xffu8; 16 * size]; - buffer[..ciphertext.len()].copy_from_slice(&ciphertext); - let mut decrypted = cipher.decrypt(&mut buffer)?; - if decrypted[0] != '{' as u8 { - return Err(ManifestAccountLoadError::DecryptionFailed); - } - // UnpadError does not implement Error for some fucking reason, so we have to do this. - let unpadded = Pkcs7::unpad(&mut decrypted) - .map_err(|_| ManifestAccountLoadError::DecryptionFailed)?; - - let s = std::str::from_utf8(&unpadded).unwrap(); + let plaintext = crate::encryption::LegacySdaCompatible::decrypt( + passkey, params, ciphertext, + )?; + let s = std::str::from_utf8(&plaintext).unwrap(); account = serde_json::from_str(&s)?; } (None, Some(_)) => { @@ -191,32 +164,9 @@ impl Manifest { let final_buffer: Vec; match (passkey, entry.encryption.as_ref()) { (Some(passkey), Some(params)) => { - let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; - let iv = base64::decode(¶ms.iv)?; - let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; - - // This also sucks. Extremely confusing. - let plaintext = serialized; - let origsize = plaintext.len(); - let buffersize: usize = - (origsize / 16 + (if origsize % 16 == 0 { 0 } else { 1 })) * 16; - let mut buffer = vec![]; - for chunk in plaintext.as_slice().chunks(256) { - let chunksize = chunk.len(); - let buffersize = - (chunksize / 16 + (if chunksize % 16 == 0 { 0 } else { 1 })) * 16; - let mut chunkbuffer = vec![0xffu8; buffersize]; - chunkbuffer[..chunksize].copy_from_slice(&chunk); - if buffersize != chunksize { - chunkbuffer = Pkcs7::pad(&mut chunkbuffer, chunksize, buffersize) - .unwrap() - .to_vec(); - } - buffer.append(&mut chunkbuffer); - } - let ciphertext = cipher.encrypt(&mut buffer, buffersize)?; - - final_buffer = base64::encode(&ciphertext).as_bytes().to_vec(); + final_buffer = crate::encryption::LegacySdaCompatible::encrypt( + passkey, params, serialized, + )?; } (None, Some(_)) => { bail!("maFiles are encrypted, but no passkey was provided."); @@ -241,53 +191,18 @@ impl Manifest { } } -const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive, but necessary to maintain compatibility with SteamDesktopAuthenticator. -const SALT_LENGTH: usize = 8; -const KEY_SIZE_BYTES: usize = 32; -const IV_LENGTH: usize = 16; - -fn get_encryption_key(passkey: &String, salt: &String) -> anyhow::Result<[u8; KEY_SIZE_BYTES]> { - let password_bytes = passkey.as_bytes(); - let salt_bytes = base64::decode(salt)?; - let mut full_key: [u8; KEY_SIZE_BYTES] = [0u8; KEY_SIZE_BYTES]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA1, - std::num::NonZeroU32::new(PBKDF2_ITERATIONS).unwrap(), - &salt_bytes, - password_bytes, - &mut full_key, - ); - return Ok(full_key); -} - -impl EntryEncryptionParams { - pub fn generate() -> EntryEncryptionParams { - let rng = ring::rand::SystemRandom::new(); - let mut salt = [0u8; SALT_LENGTH]; - let mut iv = [0u8; IV_LENGTH]; - rng.fill(&mut salt).expect("Unable to generate salt."); - rng.fill(&mut iv).expect("Unable to generate IV."); - EntryEncryptionParams { - salt: base64::encode(salt), - iv: base64::encode(iv), - } - } -} - #[derive(Debug, Error)] pub enum ManifestAccountLoadError { #[error("Manifest accounts are encrypted, but no passkey was provided.")] MissingPasskey, - #[error("Failed to decrypt account.")] - #[from(block_modes::block_padding::UnpadError)] - DecryptionFailed, + #[error("Failed to decrypt account. {self:?}")] + DecryptionFailed(#[from] crate::encryption::EntryEncryptionError), #[error("Failed to deserialize the account.")] DeserializationFailed(#[from] serde_json::Error), #[error(transparent)] Unknown(#[from] anyhow::Error), } -/// For some reason, these errors do not get converted to `ManifestAccountLoadError`s, even though they get converted into `anyhow::Error` just fine. I am too lazy to figure out why right now. impl From for ManifestAccountLoadError { fn from(error: block_modes::BlockModeError) -> Self { return Self::Unknown(anyhow::Error::from(error)); @@ -525,26 +440,3 @@ mod tests { ); } } - -#[cfg(test)] -mod encryption_tests { - use super::*; - - /// This test ensures compatibility with SteamDesktopAuthenticator and with previous versions of steamguard-cli - #[test] - fn test_encryption_key() { - assert_eq!( - get_encryption_key(&"password".into(), &"GMhL0N2hqXg=".into()).unwrap(), - base64::decode("KtiRa4/OxW83MlB6URf+Z8rAGj7CBY+pDlwD/NuVo6Y=") - .unwrap() - .as_slice() - ); - - assert_eq!( - get_encryption_key(&"password".into(), &"wTzTE9A6aN8=".into()).unwrap(), - base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=") - .unwrap() - .as_slice() - ); - } -} diff --git a/src/encryption.rs b/src/encryption.rs new file mode 100644 index 0000000..4aeadc9 --- /dev/null +++ b/src/encryption.rs @@ -0,0 +1,197 @@ +use aes::Aes256; +use block_modes::block_padding::{NoPadding, Padding, Pkcs7}; +use block_modes::{BlockMode, Cbc}; +use ring::pbkdf2; +use ring::rand::SecureRandom; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive, but necessary to maintain compatibility with SteamDesktopAuthenticator. +const SALT_LENGTH: usize = 8; +const KEY_SIZE_BYTES: usize = 32; +const IV_LENGTH: usize = 16; + +fn get_encryption_key(passkey: &String, salt: &String) -> anyhow::Result<[u8; KEY_SIZE_BYTES]> { + let password_bytes = passkey.as_bytes(); + let salt_bytes = base64::decode(salt)?; + let mut full_key: [u8; KEY_SIZE_BYTES] = [0u8; KEY_SIZE_BYTES]; + pbkdf2::derive( + pbkdf2::PBKDF2_HMAC_SHA1, + std::num::NonZeroU32::new(PBKDF2_ITERATIONS).unwrap(), + &salt_bytes, + password_bytes, + &mut full_key, + ); + return Ok(full_key); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EntryEncryptionParams { + #[serde(rename = "encryption_iv")] + pub iv: String, + #[serde(rename = "encryption_salt")] + pub salt: String, + #[serde(default, rename = "encryption_scheme")] + pub scheme: EncryptionScheme, +} + +impl EntryEncryptionParams { + pub fn generate() -> EntryEncryptionParams { + let rng = ring::rand::SystemRandom::new(); + let mut salt = [0u8; SALT_LENGTH]; + let mut iv = [0u8; IV_LENGTH]; + rng.fill(&mut salt).expect("Unable to generate salt."); + rng.fill(&mut iv).expect("Unable to generate IV."); + EntryEncryptionParams { + salt: base64::encode(salt), + iv: base64::encode(iv), + scheme: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EncryptionScheme { + /// Encryption scheme that is compatible with SteamDesktopAuthenticator. + LegacySdaCompatible = 0, +} + +impl Default for EncryptionScheme { + fn default() -> Self { + Self::LegacySdaCompatible + } +} + +pub trait EntryEncryptor { + fn encrypt( + passkey: &String, + params: &EntryEncryptionParams, + plaintext: Vec, + ) -> anyhow::Result, EntryEncryptionError>; + fn decrypt( + passkey: &String, + params: &EntryEncryptionParams, + ciphertext: Vec, + ) -> anyhow::Result, EntryEncryptionError>; +} + +/// Encryption scheme that is compatible with SteamDesktopAuthenticator. +pub struct LegacySdaCompatible; + +type Aes256Cbc = Cbc; +impl EntryEncryptor for LegacySdaCompatible { + // ngl, this logic sucks ass. its kinda annoying that the logic is not completely symetric. + + fn encrypt( + passkey: &String, + params: &EntryEncryptionParams, + plaintext: Vec, + ) -> anyhow::Result, EntryEncryptionError> { + let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; + let iv = base64::decode(¶ms.iv)?; + let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + + let origsize = plaintext.len(); + let buffersize: usize = (origsize / 16 + (if origsize % 16 == 0 { 0 } else { 1 })) * 16; + let mut buffer = vec![]; + for chunk in plaintext.as_slice().chunks(256) { + let chunksize = chunk.len(); + let buffersize = (chunksize / 16 + (if chunksize % 16 == 0 { 0 } else { 1 })) * 16; + let mut chunkbuffer = vec![0xffu8; buffersize]; + chunkbuffer[..chunksize].copy_from_slice(&chunk); + if buffersize != chunksize { + chunkbuffer = Pkcs7::pad(&mut chunkbuffer, chunksize, buffersize) + .unwrap() + .to_vec(); + } + buffer.append(&mut chunkbuffer); + } + let ciphertext = cipher.encrypt(&mut buffer, buffersize)?; + let final_buffer = base64::encode(&ciphertext); + return Ok(final_buffer.as_bytes().to_vec()); + } + + fn decrypt( + passkey: &String, + params: &EntryEncryptionParams, + ciphertext: Vec, + ) -> anyhow::Result, EntryEncryptionError> { + let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; + let iv = base64::decode(¶ms.iv)?; + let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + + let decoded = base64::decode(ciphertext)?; + let size: usize = decoded.len() / 16 + (if decoded.len() % 16 == 0 { 0 } else { 1 }); + let mut buffer = vec![0xffu8; 16 * size]; + buffer[..decoded.len()].copy_from_slice(&decoded); + let mut decrypted = cipher.decrypt(&mut buffer)?; + if decrypted[0] != '{' as u8 && decrypted[decrypted.len() - 1] != '}' as u8 { + return Err(EntryEncryptionError::IncorrectPasskey); + } + let unpadded = Pkcs7::unpad(&mut decrypted)?; + return Ok(unpadded.to_vec()); + } +} + +#[derive(Debug, Error)] +pub enum EntryEncryptionError { + #[error("Incorrect passkey provided.")] + IncorrectPasskey, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +/// For some reason, these errors do not get converted to `ManifestAccountLoadError`s, even though they get converted into `anyhow::Error` just fine. I am too lazy to figure out why right now. +impl From for EntryEncryptionError { + fn from(error: block_modes::BlockModeError) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for EntryEncryptionError { + fn from(error: block_modes::InvalidKeyIvLength) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for EntryEncryptionError { + fn from(error: block_modes::block_padding::PadError) -> Self { + return Self::Unknown(anyhow!("PadError")); + } +} +impl From for EntryEncryptionError { + fn from(error: block_modes::block_padding::UnpadError) -> Self { + return Self::Unknown(anyhow!("UnpadError")); + } +} +impl From for EntryEncryptionError { + fn from(error: base64::DecodeError) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} +impl From for EntryEncryptionError { + fn from(error: std::io::Error) -> Self { + return Self::Unknown(anyhow::Error::from(error)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// This test ensures compatibility with SteamDesktopAuthenticator and with previous versions of steamguard-cli + #[test] + fn test_encryption_key() { + assert_eq!( + get_encryption_key(&"password".into(), &"GMhL0N2hqXg=".into()).unwrap(), + base64::decode("KtiRa4/OxW83MlB6URf+Z8rAGj7CBY+pDlwD/NuVo6Y=") + .unwrap() + .as_slice() + ); + + assert_eq!( + get_encryption_key(&"password".into(), &"wTzTE9A6aN8=".into()).unwrap(), + base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=") + .unwrap() + .as_slice() + ); + } +} diff --git a/src/main.rs b/src/main.rs index c361e7c..c4f3156 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ extern crate dirs; extern crate ring; mod accountmanager; mod demos; +mod encryption; mod tui; fn cli() -> App<'static, 'static> { @@ -190,7 +191,9 @@ fn main() { Ok(_) => break, Err( accountmanager::ManifestAccountLoadError::MissingPasskey - | accountmanager::ManifestAccountLoadError::DecryptionFailed, + | accountmanager::ManifestAccountLoadError::DecryptionFailed( + encryption::EntryEncryptionError::IncorrectPasskey, + ), ) => { if passkey.is_some() { error!("Incorrect passkey"); From 71edb794c141bc00a7184674bd1b2d4bb6391368 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 19 Aug 2021 17:15:10 -0400 Subject: [PATCH 15/18] move encryption key maker into LegacySdaCompatible impl --- src/encryption.rs | 50 +++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/src/encryption.rs b/src/encryption.rs index 4aeadc9..f31a343 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -6,25 +6,9 @@ use ring::rand::SecureRandom; use serde::{Deserialize, Serialize}; use thiserror::Error; -const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive, but necessary to maintain compatibility with SteamDesktopAuthenticator. const SALT_LENGTH: usize = 8; -const KEY_SIZE_BYTES: usize = 32; const IV_LENGTH: usize = 16; -fn get_encryption_key(passkey: &String, salt: &String) -> anyhow::Result<[u8; KEY_SIZE_BYTES]> { - let password_bytes = passkey.as_bytes(); - let salt_bytes = base64::decode(salt)?; - let mut full_key: [u8; KEY_SIZE_BYTES] = [0u8; KEY_SIZE_BYTES]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA1, - std::num::NonZeroU32::new(PBKDF2_ITERATIONS).unwrap(), - &salt_bytes, - password_bytes, - &mut full_key, - ); - return Ok(full_key); -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EntryEncryptionParams { #[serde(rename = "encryption_iv")] @@ -53,7 +37,7 @@ impl EntryEncryptionParams { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum EncryptionScheme { /// Encryption scheme that is compatible with SteamDesktopAuthenticator. - LegacySdaCompatible = 0, + LegacySdaCompatible = -1, } impl Default for EncryptionScheme { @@ -78,6 +62,28 @@ pub trait EntryEncryptor { /// Encryption scheme that is compatible with SteamDesktopAuthenticator. pub struct LegacySdaCompatible; +impl LegacySdaCompatible { + const PBKDF2_ITERATIONS: u32 = 50000; // This is excessive, but necessary to maintain compatibility with SteamDesktopAuthenticator. + const KEY_SIZE_BYTES: usize = 32; + + fn get_encryption_key( + passkey: &String, + salt: &String, + ) -> anyhow::Result<[u8; Self::KEY_SIZE_BYTES]> { + let password_bytes = passkey.as_bytes(); + let salt_bytes = base64::decode(salt)?; + let mut full_key: [u8; Self::KEY_SIZE_BYTES] = [0u8; Self::KEY_SIZE_BYTES]; + pbkdf2::derive( + pbkdf2::PBKDF2_HMAC_SHA1, + std::num::NonZeroU32::new(Self::PBKDF2_ITERATIONS).unwrap(), + &salt_bytes, + password_bytes, + &mut full_key, + ); + return Ok(full_key); + } +} + type Aes256Cbc = Cbc; impl EntryEncryptor for LegacySdaCompatible { // ngl, this logic sucks ass. its kinda annoying that the logic is not completely symetric. @@ -87,7 +93,7 @@ impl EntryEncryptor for LegacySdaCompatible { params: &EntryEncryptionParams, plaintext: Vec, ) -> anyhow::Result, EntryEncryptionError> { - let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; + let key = Self::get_encryption_key(&passkey.into(), ¶ms.salt)?; let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; @@ -116,7 +122,7 @@ impl EntryEncryptor for LegacySdaCompatible { params: &EntryEncryptionParams, ciphertext: Vec, ) -> anyhow::Result, EntryEncryptionError> { - let key = get_encryption_key(&passkey.into(), ¶ms.salt)?; + let key = Self::get_encryption_key(&passkey.into(), ¶ms.salt)?; let iv = base64::decode(¶ms.iv)?; let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; @@ -181,14 +187,16 @@ mod tests { #[test] fn test_encryption_key() { assert_eq!( - get_encryption_key(&"password".into(), &"GMhL0N2hqXg=".into()).unwrap(), + LegacySdaCompatible::get_encryption_key(&"password".into(), &"GMhL0N2hqXg=".into()) + .unwrap(), base64::decode("KtiRa4/OxW83MlB6URf+Z8rAGj7CBY+pDlwD/NuVo6Y=") .unwrap() .as_slice() ); assert_eq!( - get_encryption_key(&"password".into(), &"wTzTE9A6aN8=".into()).unwrap(), + LegacySdaCompatible::get_encryption_key(&"password".into(), &"wTzTE9A6aN8=".into()) + .unwrap(), base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=") .unwrap() .as_slice() From 8722e9f294e02c3bbe4ddfa3f938d6beb75e0d2f Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 19 Aug 2021 17:45:20 -0400 Subject: [PATCH 16/18] move around fixtures --- src/accountmanager.rs | 4 ++-- .../maFiles/{ => compat}/1-account-encrypted/1234.maFile | 0 .../maFiles/{ => compat}/1-account-encrypted/manifest.json | 0 src/fixtures/maFiles/{ => compat}/1-account/1234.maFile | 0 src/fixtures/maFiles/{ => compat}/1-account/manifest.json | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename src/fixtures/maFiles/{ => compat}/1-account-encrypted/1234.maFile (100%) rename src/fixtures/maFiles/{ => compat}/1-account-encrypted/manifest.json (100%) rename src/fixtures/maFiles/{ => compat}/1-account/1234.maFile (100%) rename src/fixtures/maFiles/{ => compat}/1-account/manifest.json (100%) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index f317679..160b971 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -392,7 +392,7 @@ mod tests { #[test] fn test_sda_compatibility_1() { - let path = Path::new("src/fixtures/maFiles/1-account/manifest.json"); + let path = Path::new("src/fixtures/maFiles/compat/1-account/manifest.json"); assert!(path.is_file()); let result = Manifest::load(path); assert!(matches!(result, Ok(_))); @@ -413,7 +413,7 @@ mod tests { #[test] fn test_sda_compatibility_1_encrypted() { - let path = Path::new("src/fixtures/maFiles/1-account-encrypted/manifest.json"); + let path = Path::new("src/fixtures/maFiles/compat/1-account-encrypted/manifest.json"); assert!(path.is_file()); let result = Manifest::load(path); assert!(matches!(result, Ok(_))); diff --git a/src/fixtures/maFiles/1-account-encrypted/1234.maFile b/src/fixtures/maFiles/compat/1-account-encrypted/1234.maFile similarity index 100% rename from src/fixtures/maFiles/1-account-encrypted/1234.maFile rename to src/fixtures/maFiles/compat/1-account-encrypted/1234.maFile diff --git a/src/fixtures/maFiles/1-account-encrypted/manifest.json b/src/fixtures/maFiles/compat/1-account-encrypted/manifest.json similarity index 100% rename from src/fixtures/maFiles/1-account-encrypted/manifest.json rename to src/fixtures/maFiles/compat/1-account-encrypted/manifest.json diff --git a/src/fixtures/maFiles/1-account/1234.maFile b/src/fixtures/maFiles/compat/1-account/1234.maFile similarity index 100% rename from src/fixtures/maFiles/1-account/1234.maFile rename to src/fixtures/maFiles/compat/1-account/1234.maFile diff --git a/src/fixtures/maFiles/1-account/manifest.json b/src/fixtures/maFiles/compat/1-account/manifest.json similarity index 100% rename from src/fixtures/maFiles/1-account/manifest.json rename to src/fixtures/maFiles/compat/1-account/manifest.json From fdc606fb0e9d6d0733c54c44a8c742a1fc1f41d9 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Fri, 20 Aug 2021 09:37:55 -0400 Subject: [PATCH 17/18] add a unit test for encryption --- Cargo.lock | 91 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/encryption.rs | 42 ++++++++++++++++++++++ src/main.rs | 2 ++ 4 files changed, 137 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 432943f..5d741aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5" +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.2.1" @@ -793,6 +808,18 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "memoize" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb49e4361c7534fd1fd1d4a1da51b1bb4b254c5ebc519fc4e5dce578fd69f5d9" +dependencies = [ + "lazy_static 1.4.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "mime" version = "0.3.16" @@ -1148,6 +1175,26 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static 1.4.0", + "num-traits", + "quick-error 2.0.1", + "rand 0.8.4", + "rand_chacha 0.3.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + [[package]] name = "publicsuffix" version = "1.5.6" @@ -1158,6 +1205,18 @@ dependencies = [ "url", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.9" @@ -1286,6 +1345,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.2", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -1452,6 +1520,18 @@ dependencies = [ "semver 0.11.0", ] +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.5" @@ -1783,6 +1863,8 @@ dependencies = [ "hmac-sha1", "lazy_static 1.4.0", "log", + "memoize", + "proptest", "rand 0.8.4", "regex", "reqwest", @@ -2210,6 +2292,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 2e1daea..d85bacd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ ring = "0.16.20" aes = "0.7.4" block-modes = "0.8.1" thiserror = "1.0.26" +memoize = "0.1.9" [dev-dependencies] tempdir = "0.3" +proptest = "1" diff --git a/src/encryption.rs b/src/encryption.rs index f31a343..07fbdfc 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -182,6 +182,7 @@ impl From for EntryEncryptionError { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; /// This test ensures compatibility with SteamDesktopAuthenticator and with previous versions of steamguard-cli #[test] @@ -202,4 +203,45 @@ mod tests { .as_slice() ); } + + #[test] + fn test_ensure_encryption_symmetric() -> anyhow::Result<()> { + let passkey = "password"; + let params = EntryEncryptionParams::generate(); + let orig = "{{tactical glizzy}}".as_bytes().to_vec(); + let encrypted = + LegacySdaCompatible::encrypt(&passkey.clone().into(), ¶ms, orig.clone()).unwrap(); + let result = LegacySdaCompatible::decrypt(&passkey.into(), ¶ms, encrypted).unwrap(); + assert_eq!(orig, result.to_vec()); + return Ok(()); + } + + prop_compose! { + /// An insecure but reproducible strategy for generating encryption params. + fn encryption_params()(salt in any::<[u8; SALT_LENGTH]>(), iv in any::<[u8; IV_LENGTH]>()) -> EntryEncryptionParams { + EntryEncryptionParams { + salt: base64::encode(&salt), + iv: base64::encode(&iv), + scheme: EncryptionScheme::LegacySdaCompatible, + } + } + } + + // proptest! { + // #[test] + // fn ensure_encryption_symmetric( + // passkey in ".{1,}", + // params in encryption_params(), + // data in any::>(), + // ) { + // prop_assume!(data.len() >= 2); + // let mut orig = data; + // orig[0] = '{' as u8; + // let n = orig.len() - 1; + // orig[n] = '}' as u8; + // let encrypted = LegacySdaCompatible::encrypt(&passkey.clone().into(), ¶ms, orig.clone()).unwrap(); + // let result = LegacySdaCompatible::decrypt(&passkey.into(), ¶ms, encrypted).unwrap(); + // prop_assert_eq!(orig, result.to_vec()); + // } + // } } diff --git a/src/main.rs b/src/main.rs index c4f3156..d6856bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,8 @@ extern crate lazy_static; extern crate anyhow; extern crate base64; extern crate dirs; +#[cfg(test)] +extern crate proptest; extern crate ring; mod accountmanager; mod demos; From 49a264ba3fac2330f4a1ecb6f9ad8dc7d286b998 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Fri, 20 Aug 2021 10:01:23 -0400 Subject: [PATCH 18/18] move incorrect passkey error --- Cargo.lock | 13 ------------- Cargo.toml | 1 - src/accountmanager.rs | 5 +++++ src/encryption.rs | 7 +------ src/main.rs | 4 +--- 5 files changed, 7 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d741aa..dc5c2dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,18 +808,6 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" -[[package]] -name = "memoize" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb49e4361c7534fd1fd1d4a1da51b1bb4b254c5ebc519fc4e5dce578fd69f5d9" -dependencies = [ - "lazy_static 1.4.0", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "mime" version = "0.3.16" @@ -1863,7 +1851,6 @@ dependencies = [ "hmac-sha1", "lazy_static 1.4.0", "log", - "memoize", "proptest", "rand 0.8.4", "regex", diff --git a/Cargo.toml b/Cargo.toml index d85bacd..12672c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ ring = "0.16.20" aes = "0.7.4" block-modes = "0.8.1" thiserror = "1.0.26" -memoize = "0.1.9" [dev-dependencies] tempdir = "0.3" diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 160b971..cb9fb1e 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -97,6 +97,9 @@ impl Manifest { let plaintext = crate::encryption::LegacySdaCompatible::decrypt( passkey, params, ciphertext, )?; + if plaintext[0] != '{' as u8 && plaintext[plaintext.len() - 1] != '}' as u8 { + return Err(ManifestAccountLoadError::IncorrectPasskey); + } let s = std::str::from_utf8(&plaintext).unwrap(); account = serde_json::from_str(&s)?; } @@ -195,6 +198,8 @@ impl Manifest { pub enum ManifestAccountLoadError { #[error("Manifest accounts are encrypted, but no passkey was provided.")] MissingPasskey, + #[error("Incorrect passkey provided.")] + IncorrectPasskey, #[error("Failed to decrypt account. {self:?}")] DecryptionFailed(#[from] crate::encryption::EntryEncryptionError), #[error("Failed to deserialize the account.")] diff --git a/src/encryption.rs b/src/encryption.rs index 07fbdfc..a1a74cc 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -131,9 +131,6 @@ impl EntryEncryptor for LegacySdaCompatible { let mut buffer = vec![0xffu8; 16 * size]; buffer[..decoded.len()].copy_from_slice(&decoded); let mut decrypted = cipher.decrypt(&mut buffer)?; - if decrypted[0] != '{' as u8 && decrypted[decrypted.len() - 1] != '}' as u8 { - return Err(EntryEncryptionError::IncorrectPasskey); - } let unpadded = Pkcs7::unpad(&mut decrypted)?; return Ok(unpadded.to_vec()); } @@ -141,8 +138,6 @@ impl EntryEncryptor for LegacySdaCompatible { #[derive(Debug, Error)] pub enum EntryEncryptionError { - #[error("Incorrect passkey provided.")] - IncorrectPasskey, #[error(transparent)] Unknown(#[from] anyhow::Error), } @@ -208,7 +203,7 @@ mod tests { fn test_ensure_encryption_symmetric() -> anyhow::Result<()> { let passkey = "password"; let params = EntryEncryptionParams::generate(); - let orig = "{{tactical glizzy}}".as_bytes().to_vec(); + let orig = "tactical glizzy".as_bytes().to_vec(); let encrypted = LegacySdaCompatible::encrypt(&passkey.clone().into(), ¶ms, orig.clone()).unwrap(); let result = LegacySdaCompatible::decrypt(&passkey.into(), ¶ms, encrypted).unwrap(); diff --git a/src/main.rs b/src/main.rs index d6856bb..5311a42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,9 +193,7 @@ fn main() { Ok(_) => break, Err( accountmanager::ManifestAccountLoadError::MissingPasskey - | accountmanager::ManifestAccountLoadError::DecryptionFailed( - encryption::EntryEncryptionError::IncorrectPasskey, - ), + | accountmanager::ManifestAccountLoadError::IncorrectPasskey, ) => { if passkey.is_some() { error!("Incorrect passkey");