add a new, faster encryption scheme (Argon2idAes256) and make it the default (#270)

- move legacy scheme to new module
- add argon2 crate
- mvoe test
- add argon2id aes encryption scheme
- refactor encryption to be less shit
- fix all the errors
- fix lints
This commit is contained in:
Carson McManus 2023-07-03 10:23:56 -04:00 committed by GitHub
parent 26c6916e79
commit d5218d770e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 410 additions and 203 deletions

44
Cargo.lock generated
View file

@ -46,6 +46,17 @@ version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "argon2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
dependencies = [
"base64ct",
"blake2",
"password-hash",
]
[[package]]
name = "async-broadcast"
version = "0.5.1"
@ -261,6 +272,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -587,7 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03"
dependencies = [
"generic-array",
"rand_core 0.6.3",
"rand_core 0.6.4",
"subtle",
]
@ -1697,6 +1717,17 @@ dependencies = [
"windows-sys 0.36.1",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "pem-rfc7468"
version = "0.2.4"
@ -2093,7 +2124,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"rand_core 0.6.4",
]
[[package]]
@ -2113,7 +2144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.3",
"rand_core 0.6.4",
]
[[package]]
@ -2142,9 +2173,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.6",
]
@ -2173,7 +2204,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core 0.6.3",
"rand_core 0.6.4",
]
[[package]]
@ -2852,6 +2883,7 @@ version = "0.9.7"
dependencies = [
"aes 0.8.3",
"anyhow",
"argon2",
"base64",
"cbc",
"clap",

View file

@ -65,6 +65,7 @@ phonenumber = "0.3"
cbc = { version = "0.1.2", features = ["std"] }
inout = { version = "0.1.3", features = ["std"] }
keyring = { version = "2.0.4", optional = true }
argon2 = { version = "0.5.0", features = ["std"] }
[dev-dependencies]
tempdir = "0.3"

View file

@ -1,5 +1,5 @@
use crate::accountmanager::legacy::SdaManifest;
pub use crate::encryption::EntryEncryptionParams;
pub use crate::encryption::EncryptionScheme;
use crate::encryption::EntryEncryptor;
use log::*;
use secrecy::{ExposeSecret, SecretString};
@ -212,11 +212,9 @@ impl AccountManager {
);
let final_buffer: Vec<u8> = match (&self.passkey, entry.encryption.as_ref()) {
(Some(passkey), Some(params)) => crate::encryption::LegacySdaCompatible::encrypt(
passkey.expose_secret(),
params,
serialized,
)?,
(Some(passkey), Some(scheme)) => {
scheme.encrypt(passkey.expose_secret(), serialized)?
}
(None, Some(_)) => {
bail!("maFiles are encrypted, but no passkey was provided.");
}
@ -354,7 +352,7 @@ trait EntryLoader<T> {
&self,
path: &Path,
passkey: Option<&SecretString>,
encryption_params: Option<&EntryEncryptionParams>,
encryption_params: Option<&EncryptionScheme>,
) -> anyhow::Result<T, ManifestAccountLoadError>;
}
@ -363,20 +361,16 @@ impl EntryLoader<SteamGuardAccount> for ManifestEntry {
&self,
path: &Path,
passkey: Option<&SecretString>,
encryption_params: Option<&EntryEncryptionParams>,
encryption_params: Option<&EncryptionScheme>,
) -> anyhow::Result<SteamGuardAccount, ManifestAccountLoadError> {
debug!("loading entry: {:?}", path);
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let account: SteamGuardAccount = match (&passkey, encryption_params.as_ref()) {
(Some(passkey), Some(params)) => {
(Some(passkey), Some(scheme)) => {
let mut ciphertext: Vec<u8> = vec![];
reader.read_to_end(&mut ciphertext)?;
let plaintext = crate::encryption::LegacySdaCompatible::decrypt(
passkey.expose_secret(),
params,
ciphertext,
)?;
let plaintext = scheme.decrypt(passkey.expose_secret(), ciphertext)?;
if plaintext[0] != b'{' && plaintext[plaintext.len() - 1] != b'}' {
return Err(ManifestAccountLoadError::IncorrectPasskey);
}
@ -497,7 +491,7 @@ mod tests {
"zvIayp3JPvtvX/QGHqsqKBk/44s=".into(),
)?;
manager.add_account(account);
manager.manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
manager.manifest.entries[0].encryption = Some(EncryptionScheme::generate());
manager.submit_passkey(passkey.clone());
assert!(matches!(manager.save(), Ok(_)));
@ -550,7 +544,7 @@ mod tests {
account.token_gid = "asdf1234".into();
manager.add_account(account);
manager.submit_passkey(passkey.clone());
manager.manifest.entries[0].encryption = Some(EntryEncryptionParams::generate());
manager.manifest.entries[0].encryption = Some(EncryptionScheme::generate());
manager.save()?;
let mut loaded_manager = AccountManager::load(manifest_path.as_path())?;

View file

@ -12,11 +12,9 @@ use serde::Deserialize;
use steamguard::{token::TwoFactorSecret, SecretString, SteamGuardAccount};
use zeroize::Zeroize;
use crate::encryption::{EncryptionScheme, EntryEncryptor};
use crate::encryption::{EntryEncryptor, LegacySdaCompatible};
use super::{
EntryEncryptionParams, EntryLoader, ManifestAccountLoadError, ManifestEntry, ManifestV1,
};
use super::{EncryptionScheme, EntryLoader, ManifestAccountLoadError, ManifestEntry, ManifestV1};
#[derive(Debug, Deserialize)]
pub struct SdaManifest {
@ -74,20 +72,16 @@ impl EntryLoader<SdaAccount> for SdaManifestEntry {
&self,
path: &Path,
passkey: Option<&SecretString>,
encryption_params: Option<&EntryEncryptionParams>,
encryption_params: Option<&EncryptionScheme>,
) -> anyhow::Result<SdaAccount, ManifestAccountLoadError> {
debug!("loading entry: {:?}", path);
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let account: SdaAccount = match (&passkey, encryption_params.as_ref()) {
(Some(passkey), Some(params)) => {
(Some(passkey), Some(scheme)) => {
let mut ciphertext: Vec<u8> = vec![];
reader.read_to_end(&mut ciphertext)?;
let plaintext = crate::encryption::LegacySdaCompatible::decrypt(
passkey.expose_secret(),
params,
ciphertext,
)?;
let plaintext = scheme.decrypt(passkey.expose_secret(), ciphertext)?;
if plaintext[0] != b'{' && plaintext[plaintext.len() - 1] != b'}' {
return Err(ManifestAccountLoadError::IncorrectPasskey);
}
@ -115,13 +109,12 @@ pub struct SdaEntryEncryptionParams {
pub salt: String,
}
impl From<SdaEntryEncryptionParams> for EntryEncryptionParams {
impl From<SdaEntryEncryptionParams> for EncryptionScheme {
fn from(sda: SdaEntryEncryptionParams) -> Self {
Self {
EncryptionScheme::LegacySdaCompatible(LegacySdaCompatible {
iv: sda.iv,
salt: sda.salt,
scheme: EncryptionScheme::LegacySdaCompatible,
}
})
}
}

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use super::EntryEncryptionParams;
use super::EncryptionScheme;
pub const CURRENT_MANIFEST_VERSION: u32 = 1;
pub type Manifest = ManifestV1;
@ -19,7 +19,7 @@ pub struct ManifestEntryV1 {
pub filename: String,
pub steam_id: u64,
pub account_name: String,
pub encryption: Option<EntryEncryptionParams>,
pub encryption: Option<EncryptionScheme>,
}
impl Default for ManifestV1 {

View file

@ -6,10 +6,12 @@ use serde::{de::Error, Deserialize};
use steamguard::SteamGuardAccount;
use thiserror::Error;
use crate::encryption::EncryptionScheme;
use super::{
legacy::{SdaAccount, SdaManifest},
manifest::ManifestV1,
EntryEncryptionParams, EntryLoader, Manifest,
EntryLoader, Manifest,
};
pub(crate) fn load_and_migrate(
@ -130,7 +132,7 @@ impl MigratingManifest {
.entries
.iter()
.map(|e| {
let params: Option<EntryEncryptionParams> =
let params: Option<EncryptionScheme> =
e.encryption.clone().map(|e| e.into());
e.load(&Path::join(folder, &e.filename), passkey, params.as_ref())
})

View file

@ -1,6 +1,9 @@
use log::*;
use crate::{tui, AccountManager};
use crate::{
encryption::{EncryptionScheme, EntryEncryptor},
tui, AccountManager,
};
use super::*;
@ -60,7 +63,7 @@ where
}
manager.load_accounts()?;
for entry in manager.iter_mut() {
entry.encryption = Some(crate::accountmanager::EntryEncryptionParams::generate());
entry.encryption = Some(EncryptionScheme::generate());
}
manager.save()?;
Ok(())

View file

@ -1,123 +1,68 @@
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, InvalidLength, KeyIvInit};
use aes::Aes256;
use aes::cipher::InvalidLength;
use rand::Rng;
use ring::pbkdf2;
use ring::rand::SecureRandom;
use serde::{Deserialize, Serialize};
use thiserror::Error;
mod argon2id_aes;
#[cfg(feature = "keyring")]
mod keyring;
mod legacy;
pub use argon2id_aes::*;
pub use legacy::*;
#[cfg(feature = "keyring")]
pub use crate::encryption::keyring::*;
const SALT_LENGTH: usize = 8;
const IV_LENGTH: usize = 16;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntryEncryptionParams {
pub iv: String,
pub salt: String,
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)]
#[serde(tag = "scheme")]
pub enum EncryptionScheme {
/// Encryption scheme that is compatible with SteamDesktopAuthenticator.
LegacySdaCompatible = -1,
}
impl Default for EncryptionScheme {
fn default() -> Self {
Self::LegacySdaCompatible
}
Argon2idAes256(Argon2idAes256),
LegacySdaCompatible(LegacySdaCompatible),
}
pub trait EntryEncryptor {
fn generate() -> Self;
fn encrypt(
&self,
passkey: &str,
params: &EntryEncryptionParams,
plaintext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError>;
fn decrypt(
&self,
passkey: &str,
params: &EntryEncryptionParams,
ciphertext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError>;
}
/// Encryption scheme that is compatible with SteamDesktopAuthenticator.
pub struct LegacySdaCompatible;
impl LegacySdaCompatible {
const PBKDF2_ITERATIONS: u32 = 50000; // This is necessary to maintain compatibility with SteamDesktopAuthenticator.
const KEY_SIZE_BYTES: usize = 32;
fn get_encryption_key(passkey: &str, salt: &str) -> 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,
);
Ok(full_key)
impl EntryEncryptor for EncryptionScheme {
fn generate() -> Self {
EncryptionScheme::Argon2idAes256(Argon2idAes256::generate())
}
}
impl EntryEncryptor for LegacySdaCompatible {
fn encrypt(
&self,
passkey: &str,
params: &EntryEncryptionParams,
plaintext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let key = Self::get_encryption_key(passkey, &params.salt)?;
let mut iv = [0u8; IV_LENGTH];
base64::decode_config_slice(&params.iv, base64::STANDARD, &mut iv)?;
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())
match self {
EncryptionScheme::Argon2idAes256(scheme) => scheme.encrypt(passkey, plaintext),
EncryptionScheme::LegacySdaCompatible(scheme) => scheme.encrypt(passkey, plaintext),
}
}
fn decrypt(
&self,
passkey: &str,
params: &EntryEncryptionParams,
ciphertext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let key = Self::get_encryption_key(passkey, &params.salt)?;
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_padded_mut::<Pkcs7>(&mut buffer)?;
Ok(decrypted.to_vec())
match self {
EncryptionScheme::Argon2idAes256(scheme) => scheme.decrypt(passkey, ciphertext),
EncryptionScheme::LegacySdaCompatible(scheme) => scheme.decrypt(passkey, ciphertext),
}
}
}
@ -172,81 +117,3 @@ pub fn generate_keyring_id() -> String {
.map(char::from)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
/// This test ensures compatibility with SteamDesktopAuthenticator and with previous versions of steamguard-cli
#[test]
fn test_encryption_key() {
assert_eq!(
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()
.as_slice(),
base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=")
.unwrap()
.as_slice()
);
}
#[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();
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(())
}
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::<Vec<u8>>(),
// ) {
// 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(), &params, orig.clone()).unwrap();
// let result = LegacySdaCompatible::decrypt(&passkey.into(), &params, encrypted).unwrap();
// prop_assert_eq!(orig, result.to_vec());
// }
// }
}

View file

@ -0,0 +1,148 @@
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use aes::Aes256;
use argon2::Argon2;
use log::*;
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Argon2idAes256 {
pub iv: String,
pub salt: String,
}
impl Argon2idAes256 {
const KEY_SIZE_BYTES: usize = 32;
const IV_LENGTH: usize = 16;
const SALT_LENGTH: usize = 16;
fn get_encryption_key(passkey: &str, salt: &str) -> 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];
let deriver = Argon2::new(
argon2::Algorithm::Argon2id,
argon2::Version::V0x13,
Self::config(),
);
deriver.hash_password_into(password_bytes, &salt_bytes, &mut full_key)?;
Ok(full_key)
}
fn config() -> argon2::Params {
argon2::Params::new(
12 * 1024, // 12MB
3,
12,
Some(Self::KEY_SIZE_BYTES),
)
.expect("Unable to create Argon2 config.")
}
}
impl EntryEncryptor for Argon2idAes256 {
fn generate() -> Self {
let rng = ring::rand::SystemRandom::new();
let mut salt = [0u8; Self::SALT_LENGTH];
let mut iv = [0u8; Self::IV_LENGTH];
rng.fill(&mut salt).expect("Unable to generate salt.");
rng.fill(&mut iv).expect("Unable to generate IV.");
Argon2idAes256 {
iv: base64::encode(iv),
salt: base64::encode(salt),
}
}
fn encrypt(
&self,
passkey: &str,
plaintext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let start = std::time::Instant::now();
let key = Self::get_encryption_key(passkey, &self.salt)?;
debug!("key derivation took: {:?}", start.elapsed());
let start = std::time::Instant::now();
let mut iv = [0u8; Self::IV_LENGTH];
base64::decode_config_slice(&self.iv, base64::STANDARD, &mut iv)?;
let cipher = cbc::Encryptor::<Aes256>::new_from_slices(&key, &iv)?;
let ciphertext = cipher.encrypt_padded_vec_mut::<Pkcs7>(&plaintext);
let encoded = base64::encode(ciphertext);
debug!("encryption took: {:?}", start.elapsed());
Ok(encoded.as_bytes().to_vec())
}
fn decrypt(
&self,
passkey: &str,
ciphertext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let start = std::time::Instant::now();
let key = Self::get_encryption_key(passkey, &self.salt)?;
debug!("key derivation took: {:?}", start.elapsed());
let start = std::time::Instant::now();
let mut iv = [0u8; Self::IV_LENGTH];
base64::decode_config_slice(&self.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_padded_mut::<Pkcs7>(&mut buffer)?;
debug!("decryption took: {:?}", start.elapsed());
Ok(decrypted.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encryption_key() {
assert_eq!(
base64::encode(
Argon2idAes256::get_encryption_key("password", "GMhL0N2hqXg=")
.unwrap()
.as_slice()
),
"DTm3hc95aKyAGmyVMZdLUPfcPjcXN1i1zYObYJg2GzY="
);
}
#[test]
fn test_encryption_key2() {
assert_eq!(
base64::encode(
Argon2idAes256::get_encryption_key("password", "wTzTE9A6aN8=")
.unwrap()
.as_slice()
),
"zwMjXhwggpJWCvkouG/xrSPZRWn2cUUyph3PAViRONA="
);
}
#[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 scheme = Argon2idAes256::generate();
for case in cases {
eprintln!("testing case: {} (len {})", case, case.len());
let orig = case.as_bytes().to_vec();
let encrypted = scheme.encrypt(passkey, orig.clone()).unwrap();
let result = scheme.decrypt(passkey, encrypted).unwrap();
assert_eq!(orig, result.to_vec());
}
Ok(())
}
}

167
src/encryption/legacy.rs Normal file
View file

@ -0,0 +1,167 @@
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use aes::Aes256;
use log::*;
use ring::pbkdf2;
use super::*;
/// Encryption scheme that is compatible with SteamDesktopAuthenticator.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LegacySdaCompatible {
pub iv: String,
pub salt: String,
}
impl LegacySdaCompatible {
const PBKDF2_ITERATIONS: u32 = 50000; // This is necessary to maintain compatibility with SteamDesktopAuthenticator.
const KEY_SIZE_BYTES: usize = 32;
const SALT_LENGTH: usize = 8;
const IV_LENGTH: usize = 16;
fn get_encryption_key(passkey: &str, salt: &str) -> 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,
);
Ok(full_key)
}
}
impl EntryEncryptor for LegacySdaCompatible {
fn generate() -> LegacySdaCompatible {
let rng = ring::rand::SystemRandom::new();
let mut salt = [0u8; Self::SALT_LENGTH];
let mut iv = [0u8; Self::IV_LENGTH];
rng.fill(&mut salt).expect("Unable to generate salt.");
rng.fill(&mut iv).expect("Unable to generate IV.");
LegacySdaCompatible {
iv: base64::encode(iv),
salt: base64::encode(salt),
}
}
fn encrypt(
&self,
passkey: &str,
plaintext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let start = std::time::Instant::now();
let key = Self::get_encryption_key(passkey, &self.salt)?;
debug!("key derivation took: {:?}", start.elapsed());
let start = std::time::Instant::now();
let mut iv = [0u8; Self::IV_LENGTH];
base64::decode_config_slice(&self.iv, base64::STANDARD, &mut iv)?;
let cipher = cbc::Encryptor::<Aes256>::new_from_slices(&key, &iv)?;
let ciphertext = cipher.encrypt_padded_vec_mut::<Pkcs7>(&plaintext);
let encoded = base64::encode(ciphertext);
debug!("encryption took: {:?}", start.elapsed());
Ok(encoded.as_bytes().to_vec())
}
fn decrypt(
&self,
passkey: &str,
ciphertext: Vec<u8>,
) -> anyhow::Result<Vec<u8>, EntryEncryptionError> {
let start = std::time::Instant::now();
let key = Self::get_encryption_key(passkey, &self.salt)?;
debug!("key derivation took: {:?}", start.elapsed());
let start = std::time::Instant::now();
let mut iv = [0u8; Self::IV_LENGTH];
base64::decode_config_slice(&self.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_padded_mut::<Pkcs7>(&mut buffer)?;
debug!("decryption took: {:?}", start.elapsed());
Ok(decrypted.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
/// This test ensures compatibility with SteamDesktopAuthenticator and with previous versions of steamguard-cli
#[test]
fn test_encryption_key() {
assert_eq!(
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()
.as_slice(),
base64::decode("Dqpej/3DqEat0roJaHmu3luYgDzRCUmzX94n4fqvWj8=")
.unwrap()
.as_slice()
);
}
#[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 scheme = LegacySdaCompatible::generate();
for case in cases {
eprintln!("testing case: {} (len {})", case, case.len());
let orig = case.as_bytes().to_vec();
let encrypted = scheme.encrypt(passkey, orig.clone()).unwrap();
let result = scheme.decrypt(passkey, encrypted).unwrap();
assert_eq!(orig, result.to_vec());
}
Ok(())
}
prop_compose! {
/// An insecure but reproducible strategy for generating encryption params.
fn encryption_params()(salt in any::<[u8; LegacySdaCompatible::SALT_LENGTH]>(), iv in any::<[u8; LegacySdaCompatible::IV_LENGTH]>()) -> LegacySdaCompatible {
LegacySdaCompatible {
salt: base64::encode(salt),
iv: base64::encode(iv),
}
}
}
// proptest! {
// #[test]
// fn ensure_encryption_symmetric(
// passkey in ".{1,}",
// params in encryption_params(),
// data in any::<Vec<u8>>(),
// ) {
// 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(), &params, orig.clone()).unwrap();
// let result = LegacySdaCompatible::decrypt(&passkey.into(), &params, encrypted).unwrap();
// prop_assert_eq!(orig, result.to_vec());
// }
// }
}