use std::{array, iter::repeat, sync::LazyLock};
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use md5::{Digest, Md5};
use rand::random;
use regex::Regex;
use crate::{
errors::{DecryptionError, EncryptionError, Error, FormatError},
fritz_base32,
};
#[must_use]
pub fn generate_boostrap_key(password: &str) -> [u8; 32] {
let hash = Md5::digest(password.as_bytes());
array::from_fn(|i| hash.get(i).copied().unwrap_or(0))
}
pub fn decrypt_encryption_key(
encrypted_key: &str,
bootstrap_key: [u8; 32],
) -> Result<[u8; 32], Error> {
let key = decode_secret(encrypted_key, bootstrap_key, IsStringValue::No)?;
let mut key: [u8; 32] = key
.as_slice()
.try_into()
.map_err(|_| FormatError::InvalidEncryptionKeyLength { length: key.len() })?;
key[16..].fill(0);
Ok(key)
}
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
#[derive(Clone, Copy, Debug)]
pub enum IsStringValue {
No,
Yes,
}
pub fn decode_secret(
secret: &str,
key: [u8; 32],
is_string_value: IsStringValue,
) -> Result<Vec<u8>, Error> {
let secret = secret
.strip_prefix("$$$$")
.ok_or(FormatError::InvalidSecretPrefix {
secret: secret.to_string(),
})?;
let secret = fritz_base32::ENCODING.decode(secret.as_bytes())?;
let extra_len = secret.len() % 16;
if extra_len > 4 {
do yeet DecryptionError::BadCiphertextLength { excess: extra_len };
}
let (secret, _extra) = &secret.split_at(secret.len() - extra_len);
let (iv, ciphertext) = secret.split_array_ref();
let decrypted = Aes256CbcDec::new(&key.into(), iv.into())
.decrypt_padded_vec_mut::<NoPadding>(ciphertext)
.expect("unpadding should not fail with NoPadding");
let (hash, decrypted) = decrypted.split_array_ref::<4>();
let calculated_hash: [u8; 16] = Md5::digest(decrypted).into();
if *hash != calculated_hash[..4] {
do yeet DecryptionError::HashMismatch {
expected: *hash,
found: calculated_hash,
};
}
let (length, data) = decrypted.split_array_ref();
let length: usize = u32::from_be_bytes(*length)
.try_into()
.expect("u32 should fit in usize");
let mut data = &data[..length];
if let IsStringValue::Yes = is_string_value {
data = data
.strip_suffix(b"\0")
.ok_or(DecryptionError::MissingNullTerminator {
data: data.to_vec(),
})?;
}
Ok(data.to_vec())
}
pub(crate) fn decode_secrets_in_text(
line: &str,
encryption_key: [u8; 32],
) -> Result<Option<String>, Error> {
static SECRET_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\${4}[A-Z1-6]+").unwrap());
let mut it = SECRET_REGEX.captures_iter(line).peekable();
if it.peek().is_none() {
return Ok(None);
}
let mut new = String::with_capacity(line.len());
let mut last_match = 0;
for cap in it {
let m = cap.get(0).unwrap();
new.push_str(&line[last_match..m.start()]);
let decoded = decode_secret(m.into(), encryption_key, IsStringValue::Yes)?;
new.push_str(std::str::from_utf8(&decoded).map_err(FormatError::SecretNotUtf8)?);
last_match = m.end();
}
new.push_str(&line[last_match..]);
Ok(Some(new))
}
pub fn encode_secret(
mut data: Vec<u8>,
key: [u8; 32],
is_string_value: IsStringValue,
) -> Result<String, Error> {
if let IsStringValue::Yes = is_string_value {
data.push(b'\0');
}
let length = data.len();
let length = u32::try_from(length)
.map_err(|_| EncryptionError::DataTooLarge { length })?
.to_be_bytes();
let total_len = data.len() + 4 + 4;
data.extend(repeat(0).take(16 - total_len % 16));
let mut hash = Md5::new();
hash.update(length);
hash.update(&data);
let hash: [u8; 16] = hash.finalize().into();
let iv: [u8; 16] = random();
let plaintext = [&hash[..4], &length, &data].concat();
let encrypted =
Aes256CbcEnc::new(&key.into(), &iv.into()).encrypt_padded_vec_mut::<NoPadding>(&plaintext);
let mut result = "$$$$".to_string();
fritz_base32::ENCODING
.encode_append(&[iv.as_slice(), encrypted.as_slice()].concat(), &mut result);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decrypt_known() {
let cases: &[(&[u8], &str, &str)] = &[
(b"test", "$$$$5AEWFTDNEIXGOGVTKJYBXJ2JK1B1LDDUXDLOAQPSQTPGI6LCQRLP1E5DEPNJIF45F4ADNFTXZF31541T", "7695206a0422786fc612643ed2f337d900000000000000000000000000000000"),
(b"testus", "$$$$4ZNKDABPVOF32CDHBPIGXRGUOPWFS3KYOUXXZK61GQ6TT1E4JYYUDOE6YMLBML6ZOJSO53NGG3HOS4ZY", "3cfb14830234eb5abf8de7570d47d79600000000000000000000000000000000"),
(b"testuse", "$$$$UEUST1NGXTC1Y52R2CSML4TNJPT2HV54MGFUXRCYHH3ROBT3OMBLV66ZQ2XOGZURJTRTP3123QQB34Y5", "4b50c9683664a466c117d89986ef5c9300000000000000000000000000000000"),
(b"testuser", "$$$$XLSJ6KVR2KNNBUVJHBA1NPMDEZCHZI4GAMY3BMNUJ5CN4PAGLKWD5K5M5GJYBXKBF1BJE5JX5GEFA4W5", "19d1c0f994c4a7157a99363d8d60097500000000000000000000000000000000"),
];
for &(plain, secret, key) in cases {
let key = hex::decode(key).unwrap().try_into().unwrap();
assert_eq!(
decode_secret(secret, key, IsStringValue::Yes,).unwrap(),
plain
);
}
}
#[test]
fn encrypt() {
let key = random();
let encrypted = encode_secret(b"testuse".to_vec(), key, IsStringValue::Yes).unwrap();
assert_eq!(
decode_secret(&encrypted, key, IsStringValue::Yes,).unwrap(),
b"testuse"
);
}
}