From f8ae7d4e0e31a484930bf0c4b03fb1bb5f343053 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 26 Jun 2023 19:02:48 -0400 Subject: [PATCH] fix SDA encryption compatibility (#236) fixes #233 --- Cargo.lock | 67 ++++++++----- Cargo.toml | 7 +- src/accountmanager.rs | 19 +--- src/accountmanager/migrate.rs | 11 +-- src/encryption.rs | 99 ++++++++++--------- .../1-account-encrypted/1234.maFile | 2 +- .../1-account-encrypted/1234.maFile.bak | 1 - .../1-account-encrypted/manifest.json.bak | 17 ---- 8 files changed, 106 insertions(+), 117 deletions(-) delete mode 100644 src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile.bak delete mode 100644 src/fixtures/maFiles/manifest-v1/1-account-encrypted/manifest.json.bak diff --git a/Cargo.lock b/Cargo.lock index cb03993..f93ca95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,14 +10,13 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.7.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", "cpufeatures", - "opaque-debug", ] [[package]] @@ -122,21 +121,14 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[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" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -162,6 +154,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.0.73" @@ -195,11 +196,12 @@ dependencies = [ [[package]] name = "cipher" -version = "0.3.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "generic-array", + "crypto-common", + "inout", ] [[package]] @@ -366,6 +368,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cssparser" version = "0.27.2" @@ -842,6 +854,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1139,12 +1161,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2" -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - [[package]] name = "option-ext" version = "0.2.0" @@ -2214,7 +2230,7 @@ dependencies = [ "aes", "anyhow", "base64", - "block-modes", + "cbc", "clap", "clap_complete", "cookie 0.14.4", @@ -2222,6 +2238,7 @@ dependencies = [ "dirs", "gethostname", "hmac-sha1", + "inout", "lazy_static 1.4.0", "log", "phonenumber", diff --git a/Cargo.toml b/Cargo.toml index 489ebcc..24da74d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,9 +50,8 @@ lazy_static = "1.4.0" uuid = { version = "0.8", features = ["v4"] } steamguard = { version = "^0.9.0", path = "./steamguard" } dirs = "3.0.2" -ring = "0.16.20" -aes = "0.7.4" -block-modes = "0.8.1" +ring = { version = "0.16.20", features = ["std"] } +aes = "0.8.3" thiserror = "1.0.26" crossterm = { version = "0.23.2", features = ["event-stream"] } qrcode = { version = "0.12.0", optional = true } @@ -62,6 +61,8 @@ zeroize = "^1.4.3" serde_path_to_error = "0.1.11" update-informer = { version = "1.0.0", optional = true, default-features = false, features = ["github"] } phonenumber = "0.3" +cbc = { version = "0.1.2", features = ["std"] } +inout = { version = "0.1.3", features = ["std"] } [dev-dependencies] tempdir = "0.3" diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 7494d32..8129ed5 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -412,21 +412,11 @@ pub enum ManifestAccountLoadError { Unknown(#[from] anyhow::Error), } -impl From for ManifestAccountLoadError { - fn from(error: block_modes::BlockModeError) -> Self { - Self::Unknown(anyhow::Error::from(error)) - } -} impl From for ManifestAccountLoadError { fn from(error: base64::DecodeError) -> Self { Self::Unknown(anyhow::Error::from(error)) } } -impl From for ManifestAccountLoadError { - fn from(error: block_modes::InvalidKeyIvLength) -> Self { - Self::Unknown(anyhow::Error::from(error)) - } -} impl From for ManifestAccountLoadError { fn from(error: std::io::Error) -> Self { Self::Unknown(anyhow::Error::from(error)) @@ -639,11 +629,10 @@ mod tests { manifest: "src/fixtures/maFiles/manifest-v1/1-account/manifest.json", passkey: None, }, - // FIXME: disabled because of #233 - // Test { - // manifest: "src/fixtures/maFiles/manifest-v1/1-account-encrypted/manifest.json", - // passkey: Some(SecretString::new("password".into())), - // }, + Test { + manifest: "src/fixtures/maFiles/manifest-v1/1-account-encrypted/manifest.json", + passkey: Some(SecretString::new("password".into())), + }, Test { manifest: "src/fixtures/maFiles/manifest-v1/2-account/manifest.json", passkey: None, diff --git a/src/accountmanager/migrate.rs b/src/accountmanager/migrate.rs index de681af..97beb43 100644 --- a/src/accountmanager/migrate.rs +++ b/src/accountmanager/migrate.rs @@ -374,11 +374,10 @@ mod tests { dir: "src/fixtures/maFiles/compat/1-account/", passkey: None, }, - // FIXME: disabled because of #233 - // Test { - // manifest: "src/fixtures/maFiles/compat/1-account-encrypted/", - // passkey: Some(SecretString::new("password".into())), - // }, + Test { + dir: "src/fixtures/maFiles/compat/1-account-encrypted/", + passkey: Some(SecretString::new("password".into())), + }, Test { dir: "src/fixtures/maFiles/compat/2-account/", passkey: None, @@ -402,9 +401,7 @@ mod tests { for file in std::fs::read_dir(case.dir)? { let file = file?; let path = file.path(); - eprintln!("copying {:?}", path); let dest = temp.path().join(path.file_name().unwrap()); - eprintln!("to {:?}", dest); std::fs::copy(&path, dest)?; } diff --git a/src/encryption.rs b/src/encryption.rs index d6617cb..7c3c2c2 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -1,6 +1,6 @@ +use aes::cipher::block_padding::Pkcs7; +use aes::cipher::{BlockDecryptMut, BlockEncryptMut, InvalidLength, KeyIvInit}; 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}; @@ -78,38 +78,22 @@ impl 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: &str, params: &EntryEncryptionParams, plaintext: Vec, ) -> anyhow::Result, EntryEncryptionError> { let key = Self::get_encryption_key(passkey, ¶ms.salt)?; - let iv = base64::decode(¶ms.iv)?; - let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; + let mut iv = [0u8; IV_LENGTH]; + base64::decode_config_slice(¶ms.iv, base64::STANDARD, &mut 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(128) { - 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 { - // pad the last chunk - 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()); + let cipher = cbc::Encryptor::::new_from_slices(&key, &iv)?; + + let ciphertext = cipher.encrypt_padded_vec_mut::(&plaintext); + + let encoded = base64::encode(ciphertext); + Ok(encoded.as_bytes().to_vec()) } fn decrypt( @@ -118,46 +102,51 @@ impl EntryEncryptor for LegacySdaCompatible { ciphertext: Vec, ) -> anyhow::Result, EntryEncryptionError> { let key = Self::get_encryption_key(passkey, ¶ms.salt)?; - let iv = base64::decode(¶ms.iv)?; - let cipher = Aes256Cbc::new_from_slices(&key, &iv)?; - + let mut iv = [0u8; IV_LENGTH]; + base64::decode_config_slice(¶ms.iv, base64::STANDARD, &mut iv)?; + let cipher = cbc::Decryptor::::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 decrypted = cipher.decrypt(&mut buffer)?; - let unpadded = Pkcs7::unpad(decrypted)?; - Ok(unpadded.to_vec()) + let decrypted = cipher.decrypt_padded_mut::(&mut buffer)?; + Ok(decrypted.to_vec()) } } #[derive(Debug, Error)] pub enum EntryEncryptionError { + #[error("Invalid ciphertext length. The ciphertext must be a multiple of 16 bytes.")] + InvalidCipherTextLength, #[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 { +impl From for EntryEncryptionError { + fn from(error: InvalidLength) -> Self { Self::Unknown(anyhow::Error::from(error)) } } -impl From for EntryEncryptionError { - fn from(error: block_modes::InvalidKeyIvLength) -> Self { + +impl From for EntryEncryptionError { + fn from(error: inout::NotEqualError) -> Self { Self::Unknown(anyhow::Error::from(error)) } } -impl From for EntryEncryptionError { - fn from(_error: block_modes::block_padding::PadError) -> Self { - Self::Unknown(anyhow!("PadError")) + +impl From for EntryEncryptionError { + fn from(error: inout::PadError) -> Self { + Self::Unknown(anyhow::Error::from(error)) } } -impl From for EntryEncryptionError { - fn from(_error: block_modes::block_padding::UnpadError) -> Self { - Self::Unknown(anyhow!("UnpadError")) + +impl From for EntryEncryptionError { + fn from(error: inout::block_padding::UnpadError) -> Self { + Self::Unknown(anyhow::Error::from(error)) } } + impl From for EntryEncryptionError { fn from(error: base64::DecodeError) -> Self { Self::Unknown(anyhow::Error::from(error)) @@ -178,14 +167,18 @@ mod tests { #[test] fn test_encryption_key() { assert_eq!( - LegacySdaCompatible::get_encryption_key("password", "GMhL0N2hqXg=").unwrap(), + LegacySdaCompatible::get_encryption_key("password", "GMhL0N2hqXg=") + .unwrap() + .as_slice(), base64::decode("KtiRa4/OxW83MlB6URf+Z8rAGj7CBY+pDlwD/NuVo6Y=") .unwrap() .as_slice() ); assert_eq!( - LegacySdaCompatible::get_encryption_key("password", "wTzTE9A6aN8=").unwrap(), + LegacySdaCompatible::get_encryption_key("password", "wTzTE9A6aN8=") + .unwrap() + .as_slice(), base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=") .unwrap() .as_slice() @@ -194,12 +187,22 @@ mod tests { #[test] fn test_ensure_encryption_symmetric() -> anyhow::Result<()> { + let cases = [ + "foo", + "tactical glizzy", + "glizzy gladiator", + "shadow wizard money gang", + "shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells, shadow wizard money gang, we love casting spells", + ]; let passkey = "password"; let params = EntryEncryptionParams::generate(); - let orig = "tactical glizzy".as_bytes().to_vec(); - let encrypted = LegacySdaCompatible::encrypt(passkey, ¶ms, orig.clone()).unwrap(); - let result = LegacySdaCompatible::decrypt(passkey, ¶ms, encrypted).unwrap(); - assert_eq!(orig, result.to_vec()); + for case in cases { + eprintln!("testing case: {} (len {})", case, case.len()); + let orig = case.as_bytes().to_vec(); + let encrypted = LegacySdaCompatible::encrypt(passkey, ¶ms, orig.clone()).unwrap(); + let result = LegacySdaCompatible::decrypt(passkey, ¶ms, encrypted).unwrap(); + assert_eq!(orig, result.to_vec()); + } Ok(()) } diff --git a/src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile b/src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile index 7b8a9f9..de17fcc 100644 --- a/src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile +++ b/src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile @@ -1 +1 @@ -Z0HJDSN9EuFOpKEeBzftCWxTsh0sV6QQriLTVrn37FyGNaXhGgzeHlvPfHgkXKCYbALTgx/B2fLh1CEojKO1/eqEgN+982CadR3EXk+vH1k5AMuGhMXPpsEeIh27ltxrdAEzWdlPlAyentBgOKlTCoN6iF+EZVORvp2pPaMrebyHi8/5Y+XC3HrMgfgmP7lFGpUgZK8f0mKB/pGaW+0/3oVikggBK3MIWlh4s9bC9LlMy5H+oU0n/Iu3P9dpbko1bDMKIbUKEPzS3wHXyQRg32zPIfONR0bswb7QTfAhoKixZrAenQluX3lXRL0JFafNEPzUY4r/DJ1pIMLK9cEvbzqwsPth6jrIZRd+zvgnshfNnGLCblYkPo4fGwePuhX2W2w6qgFMpo69rkSp1Zz6JKC/gH9YyL4a8N768ml9H1so5XBm7eB+fMRIL7bHof+V1CxGXX3z1RvjGRHPwKcrKLvffxTTs/dBHb9UDFtTprk= \ No newline at end of file +Z0HJDSN9EuFOpKEeBzftCWxTsh0sV6QQriLTVrn37FyGNaXhGgzeHlvPfHgkXKCYbALTgx/B2fLh1CEojKO1/eqEgN+982CadR3EXk+vH1k5AMuGhMXPpsEeIh27ltxrdAEzWdlPlAyentBgOKlTCoN6iF+EZVORvp2pPaMrebyHi8/5Y+XC3HrMgfgmP7lFGpUgZK8f0mKB/pGaW+0/3oVikggBK3MIWlh4s9bC9LlMy5H+oU0n/Iu3P9dpbko1bDMKIbUKEPzS3wHXyQRg32zPIfONR0bswb7QTfAhoKixZrAenQluX3lXRL0JFafNEPzUY4r/DJ1pIMLK9cEvbzqwsPth6jrIZRd+zvgnshfNnGLCblYkPo4fGwePuhX2W2w6qgFMpo69rkSp1Zz6JKC/gH9YyL4a8N768ml9H1so5XBm7eB+fMRIL7bHof+V1CxGXX3z1RvjGRHPwKcrKLvffxTTs/dBHb9UDFtTprlymLZf6C53c5vMBQ/hk4fm \ No newline at end of file diff --git a/src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile.bak b/src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile.bak deleted file mode 100644 index e4a9df8..0000000 --- a/src/fixtures/maFiles/manifest-v1/1-account-encrypted/1234.maFile.bak +++ /dev/null @@ -1 +0,0 @@ -ZQqJk4KW7pb5bndkra244z3ttIks58UplODn5IgljrOK3bKORSwrnZYb9Iv6YsirmrT0hQ3tx381GhQu4Nyj/PFHHCfFTrduaCoMLRrHDsmsexOW93Yo02acHXNfPSvxtjGfZpsuIZVlhVy8JDH/ESXp88cKn2zqjXQWu6pLnah1YPlpZuTfArw69+Em7V1OH1CoKnsYuhCo/x4u7fXhMNgDlRBvDbO4enGzaixonPu9er5Zp6iNEeuAUqmD0DHASygNmzBhUBHv8Avng8YKbvu611yQVT2KybnIoL+Q11Y36GoFhWskmG3lLwh/1OlReGwJ1iX0lDDthoel/Ygj6EC2+wkR8V7eMQf48R15xdBILYTzcrjtiuLCr9MBc/HM/ToEa3QCGwGkXvshR/meJ1BiqaRARKfvJcj4eMSpiUvhDe1QFXXjfXRdetJcknyJ8Pv6v10G/OV3ELYwdx2dYL5C+Ao4qj9QCjoD8bb/juCjtZoSxMncbm4T7ORbXs/Ulx+TEuOUmRAjxr+zaWzzO7ZfYJMhIPz+LSixKpaVmxP89DEK5LJ1T6jA50QmPft6AbOMuoq99haWH9lMgqrIfBB+ZNNHSEE9PwMUxhX/TQP3oJgmrnZcMV4nBOcovbWM5s+odu2mvzoAcRMHVxEztASbMBwdJ7amJvoRRrXTtp642FQHAe8pFPlW14x1hShXAO4YfYkmmAhLqlujammuQ6bRg7XjBvh0zE88i5UBRiRsH+MV35u3c0ciugUmbrAZ0u9Yfv19MNSMVNMsREDuxQ== \ No newline at end of file diff --git a/src/fixtures/maFiles/manifest-v1/1-account-encrypted/manifest.json.bak b/src/fixtures/maFiles/manifest-v1/1-account-encrypted/manifest.json.bak deleted file mode 100644 index 6919322..0000000 --- a/src/fixtures/maFiles/manifest-v1/1-account-encrypted/manifest.json.bak +++ /dev/null @@ -1,17 +0,0 @@ -{ - "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 -} \ No newline at end of file