fix SDA encryption compatibility (#236)

fixes #233
This commit is contained in:
Carson McManus 2023-06-26 19:02:48 -04:00 committed by GitHub
parent da44f49c56
commit f8ae7d4e0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 117 deletions

67
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -412,21 +412,11 @@ pub enum ManifestAccountLoadError {
Unknown(#[from] anyhow::Error),
}
impl From<block_modes::BlockModeError> for ManifestAccountLoadError {
fn from(error: block_modes::BlockModeError) -> Self {
Self::Unknown(anyhow::Error::from(error))
}
}
impl From<base64::DecodeError> for ManifestAccountLoadError {
fn from(error: base64::DecodeError) -> Self {
Self::Unknown(anyhow::Error::from(error))
}
}
impl From<block_modes::InvalidKeyIvLength> for ManifestAccountLoadError {
fn from(error: block_modes::InvalidKeyIvLength) -> Self {
Self::Unknown(anyhow::Error::from(error))
}
}
impl From<std::io::Error> 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,

View file

@ -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)?;
}

View file

@ -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<Aes256, NoPadding>;
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<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let key = Self::get_encryption_key(passkey, &params.salt)?;
let iv = base64::decode(&params.iv)?;
let cipher = Aes256Cbc::new_from_slices(&key, &iv)?;
let mut iv = [0u8; IV_LENGTH];
base64::decode_config_slice(&params.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::<Aes256>::new_from_slices(&key, &iv)?;
let ciphertext = cipher.encrypt_padded_vec_mut::<Pkcs7>(&plaintext);
let encoded = base64::encode(ciphertext);
Ok(encoded.as_bytes().to_vec())
}
fn decrypt(
@ -118,46 +102,51 @@ impl EntryEncryptor for LegacySdaCompatible {
ciphertext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let key = Self::get_encryption_key(passkey, &params.salt)?;
let iv = base64::decode(&params.iv)?;
let cipher = Aes256Cbc::new_from_slices(&key, &iv)?;
let mut iv = [0u8; IV_LENGTH];
base64::decode_config_slice(&params.iv, base64::STANDARD, &mut iv)?;
let cipher = cbc::Decryptor::<Aes256>::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::<Pkcs7>(&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<block_modes::BlockModeError> for EntryEncryptionError {
fn from(error: block_modes::BlockModeError) -> Self {
impl From<InvalidLength> for EntryEncryptionError {
fn from(error: InvalidLength) -> Self {
Self::Unknown(anyhow::Error::from(error))
}
}
impl From<block_modes::InvalidKeyIvLength> for EntryEncryptionError {
fn from(error: block_modes::InvalidKeyIvLength) -> Self {
impl From<inout::NotEqualError> for EntryEncryptionError {
fn from(error: inout::NotEqualError) -> Self {
Self::Unknown(anyhow::Error::from(error))
}
}
impl From<block_modes::block_padding::PadError> for EntryEncryptionError {
fn from(_error: block_modes::block_padding::PadError) -> Self {
Self::Unknown(anyhow!("PadError"))
impl From<inout::PadError> for EntryEncryptionError {
fn from(error: inout::PadError) -> Self {
Self::Unknown(anyhow::Error::from(error))
}
}
impl From<block_modes::block_padding::UnpadError> for EntryEncryptionError {
fn from(_error: block_modes::block_padding::UnpadError) -> Self {
Self::Unknown(anyhow!("UnpadError"))
impl From<inout::block_padding::UnpadError> for EntryEncryptionError {
fn from(error: inout::block_padding::UnpadError) -> Self {
Self::Unknown(anyhow::Error::from(error))
}
}
impl From<base64::DecodeError> 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();
for case in cases {
eprintln!("testing case: {} (len {})", case, case.len());
let orig = case.as_bytes().to_vec();
let encrypted = LegacySdaCompatible::encrypt(passkey, &params, orig.clone()).unwrap();
let result = LegacySdaCompatible::decrypt(passkey, &params, encrypted).unwrap();
assert_eq!(orig, result.to_vec());
}
Ok(())
}

View file

@ -1 +1 @@
Z0HJDSN9EuFOpKEeBzftCWxTsh0sV6QQriLTVrn37FyGNaXhGgzeHlvPfHgkXKCYbALTgx/B2fLh1CEojKO1/eqEgN+982CadR3EXk+vH1k5AMuGhMXPpsEeIh27ltxrdAEzWdlPlAyentBgOKlTCoN6iF+EZVORvp2pPaMrebyHi8/5Y+XC3HrMgfgmP7lFGpUgZK8f0mKB/pGaW+0/3oVikggBK3MIWlh4s9bC9LlMy5H+oU0n/Iu3P9dpbko1bDMKIbUKEPzS3wHXyQRg32zPIfONR0bswb7QTfAhoKixZrAenQluX3lXRL0JFafNEPzUY4r/DJ1pIMLK9cEvbzqwsPth6jrIZRd+zvgnshfNnGLCblYkPo4fGwePuhX2W2w6qgFMpo69rkSp1Zz6JKC/gH9YyL4a8N768ml9H1so5XBm7eB+fMRIL7bHof+V1CxGXX3z1RvjGRHPwKcrKLvffxTTs/dBHb9UDFtTprk=
Z0HJDSN9EuFOpKEeBzftCWxTsh0sV6QQriLTVrn37FyGNaXhGgzeHlvPfHgkXKCYbALTgx/B2fLh1CEojKO1/eqEgN+982CadR3EXk+vH1k5AMuGhMXPpsEeIh27ltxrdAEzWdlPlAyentBgOKlTCoN6iF+EZVORvp2pPaMrebyHi8/5Y+XC3HrMgfgmP7lFGpUgZK8f0mKB/pGaW+0/3oVikggBK3MIWlh4s9bC9LlMy5H+oU0n/Iu3P9dpbko1bDMKIbUKEPzS3wHXyQRg32zPIfONR0bswb7QTfAhoKixZrAenQluX3lXRL0JFafNEPzUY4r/DJ1pIMLK9cEvbzqwsPth6jrIZRd+zvgnshfNnGLCblYkPo4fGwePuhX2W2w6qgFMpo69rkSp1Zz6JKC/gH9YyL4a8N768ml9H1so5XBm7eB+fMRIL7bHof+V1CxGXX3z1RvjGRHPwKcrKLvffxTTs/dBHb9UDFtTprlymLZf6C53c5vMBQ/hk4fm

View file

@ -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==

View file

@ -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
}