use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use anyhow::{anyhow, Context};
use base64::{engine::general_purpose, Engine};
use blowfish::{cipher::KeyInit, BlowfishLE};
use cbc;
use chrono::Datelike;
use ecb;
use log::*;
use md5::{Digest, Md5};
use std::{
clone::Clone,
collections::HashMap,
fmt::Debug,
fs::remove_file,
fs::File,
io::prelude::*,
marker::Copy,
path::Path,
str,
sync::mpsc::{channel, Receiver},
thread,
};
const OTR_URL: &str = "http://onlinetvrecorder.com/quelle_neu1.php";
const DECODER_VERSION: &str = "0.4.1133";
const FILETYPE_LENGTH: usize = 10;
const PREAMBLE_LENGTH: usize = 512;
const HEADER_LENGTH: usize = FILETYPE_LENGTH + PREAMBLE_LENGTH;
const PREAMBLE_KEY: &str = "EF3AB29CD19F0CAC5759C7ABD12CC92BA3FE0AFEBF960D63FEBD0F45";
const IK: &str = "aFzW1tL7nP9vXd8yUfB5kLoSyATQ";
const OTRKEY_FILETYPE: &str = "OTRKEYFILE";
const OTR_ERROR_INDICATOR: &str = "MessageToBePrintedInDecoder";
const PARAM_FILENAME: &str = "FN";
const PARAM_FILESIZE: &str = "SZ";
const PARAM_ENCODED_HASH: &str = "OH";
const PARAM_DECODED_HASH: &str = "FH";
const PARAM_DECODING_KEY: &str = "HP";
const BLOCK_SIZE: usize = 8;
const MAX_CHUNK_SIZE: usize = 10 * 1024 * 1024;
const HEX_CHARS: &str = "0123456789abcdef";
type OTRParams = HashMap<String, String>;
type Chunk = Vec<u8>;
pub fn decode<P, Q>(in_video: P, out_video: Q, user: &str, password: &str) -> anyhow::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path> + Debug + Copy,
{
if MAX_CHUNK_SIZE % BLOCK_SIZE != 0 {
return Err(anyhow!(
"Chunk size [{}] is not a multiple of block size [{}]",
MAX_CHUNK_SIZE,
BLOCK_SIZE
));
}
let mut in_file = File::open(&in_video)?;
let header_params =
header_params(&mut in_file).with_context(|| "Could not extract video header from")?;
if (in_file.metadata()?.len() as usize) < file_size_from_params(&header_params) {
return Err(anyhow!("Video file seems to be corrupt: it is too small"));
}
let now = current_date();
let cbc_key = cbc_key(user, password, &now).with_context(|| {
"Could not determine CBC key for encryption of decoding key request payload"
})?;
let decoding_params = decoding_params(
&cbc_key,
&decoding_params_request(&cbc_key, &header_params, user, password, &now)
.with_context(|| "Could not assemble request for decoding key")?,
)
.with_context(|| "Could not retrieve decoding key")?;
if let Err(err) = decode_in_parallel(
&mut in_file,
out_video,
&header_params,
decoding_params.get(PARAM_DECODING_KEY).unwrap(),
) {
remove_file(out_video).unwrap_or_else(|_| {
panic!(
"Could not delete file \"{}\" after error when decoding video",
out_video.as_ref().display()
)
});
return Err(err);
}
remove_file(&in_video).with_context(|| "Could not delete video after successful decoding")?;
Ok(())
}
fn cbc_key(user: &str, password: &str, now: &str) -> anyhow::Result<String> {
let user_hash = format!("{:02x}", Md5::digest(user.as_bytes()));
let password_hash = format!("{:02x}", Md5::digest(password.as_bytes()));
let cbc_key: String = user_hash[0..13].to_string()
+ &now[..4]
+ &password_hash[0..11]
+ &now[4..6]
+ &user_hash[21..32]
+ &now[6..]
+ &password_hash[19..32];
debug!(cbc_key:serde = cbc_key; "CBC_KEY");
Ok(cbc_key)
}
fn chunk_sizes(file_size: usize) -> Vec<usize> {
let (full_chunks, remainder) = (file_size / MAX_CHUNK_SIZE, file_size % MAX_CHUNK_SIZE);
let mut sizes: Vec<usize> = vec![MAX_CHUNK_SIZE; full_chunks];
if remainder / BLOCK_SIZE > 0 {
sizes.push(remainder / BLOCK_SIZE * BLOCK_SIZE);
}
if remainder % BLOCK_SIZE > 0 {
sizes.push(remainder % BLOCK_SIZE);
}
sizes
}
fn current_date() -> String {
let now = chrono::Local::now().date_naive();
format!("{:04}{:02}{:02}", now.year(), now.month(), now.day())
}
fn decode_chunk(key: &str, mut chunk: Chunk) -> Chunk {
if chunk.capacity() >= BLOCK_SIZE {
ecb::Decryptor::<BlowfishLE>::new_from_slice(
&hex::decode(key).expect("Could not turn decoding key into hex string"),
)
.unwrap_or_else(|_| panic!("Could not create cipher object for decoding of chunk"))
.decrypt_padded_mut::<NoPadding>(&mut chunk)
.unwrap_or_else(|_| panic!("Could not decode chunk"));
}
chunk
}
fn decode_in_parallel<P>(
in_file: &mut File,
out_video: P,
header_params: &OTRParams,
key: &str,
) -> anyhow::Result<()>
where
P: AsRef<Path> + Debug,
{
let mut out_file = File::create(&out_video).with_context(|| {
format!(
"Could not create result file \"{}\"",
out_video.as_ref().display()
)
})?;
let mut thread_handles = vec![];
let (enc_hash_sender, enc_hash_receiver) = channel();
let (dec_hash_sender, dec_hash_receiver) = channel();
let (enc_hash_handle, dec_hash_handle) = (
thread::spawn(move || -> [u8; 16] { hashing_queue(enc_hash_receiver) }),
thread::spawn(move || -> [u8; 16] { hashing_queue(dec_hash_receiver) }),
);
for chunk_size in chunk_sizes(file_size_from_params(header_params) - HEADER_LENGTH) {
let mut chunk = vec![0u8; chunk_size];
if in_file
.read(&mut chunk[..chunk_size])
.with_context(|| "Could not read chunk")?
< chunk_size
{
return Err(anyhow!("Chunk is too short"));
}
enc_hash_sender.send(chunk.clone()).unwrap();
let dec_key = key.to_string();
thread_handles.push(thread::spawn(move || -> Chunk {
decode_chunk(&dec_key, chunk)
}));
}
drop(enc_hash_sender);
for handle in thread_handles {
match handle.join() {
Ok(chunk) => {
dec_hash_sender.send(chunk.clone()).unwrap();
out_file.write_all(&chunk).with_context(|| {
format!(
"Could not write to decoded video file \"{}\"",
out_video.as_ref().display(),
)
})?;
}
Err(_) => {
return Err(anyhow!(
"Could not create decoded video file \"{}\"",
out_video.as_ref().display()
));
}
}
}
drop(dec_hash_sender);
if !verify_checksum(
&enc_hash_handle.join().unwrap(),
&header_params[PARAM_ENCODED_HASH],
)
.context("Could not verify checksum of encoded video file")?
{
return Err(anyhow!("MD5 checksum of encoded video file is not correct"));
}
if !verify_checksum(
&dec_hash_handle.join().unwrap(),
&header_params[PARAM_DECODED_HASH],
)
.context("Could not verify checksum of decoded video file")?
{
return Err(anyhow!("MD5 checksum of decoded video file is not correct"));
}
Ok(())
}
fn decoding_params(cbc_key: &str, request: &str) -> anyhow::Result<OTRParams> {
let response = reqwest::blocking::Client::builder()
.user_agent("Windows-OTR-Decoder/".to_string() + DECODER_VERSION)
.build()
.with_context(|| "Could not create HTTP client to request decoding key")?
.get(request)
.send()
.with_context(|| "Did not get a response for decoding key request")?
.text()
.with_context(|| {
"Response to decoding key request is corrupted: could not turn into text"
})?;
if response.len() < OTR_ERROR_INDICATOR.len() {
return Err(anyhow!(
"Unidentifiable error while requesting decoding key"
));
}
if &response[..OTR_ERROR_INDICATOR.len()] == OTR_ERROR_INDICATOR {
return Err(anyhow!(
"Error while requesting decoding key: \"{}\"",
response[OTR_ERROR_INDICATOR.len()..].to_string()
));
}
let mut response = general_purpose::STANDARD
.decode(&response)
.with_context(|| "Could not decode response to decoding key request from base64")?;
if response.len() < 2 * BLOCK_SIZE || response.len() % BLOCK_SIZE != 0 {
return Err(anyhow!(
"Response to decoding key request is corrupted: must be a multiple of {}",
BLOCK_SIZE
));
}
let init_vector = &response[..BLOCK_SIZE];
let response_decrypted = cbc::Decryptor::<BlowfishLE>::new_from_slices(
&hex::decode(cbc_key).with_context(|| "Could not turn CBC key into byte array")?,
init_vector,
)
.with_context(|| "Could not create cipher object for decryption of decoding key response")?
.decrypt_padded_mut::<NoPadding>(&mut response[BLOCK_SIZE..])
.unwrap_or_else(|_| panic!("Could not decrypt decryption key response"));
let decoding_params = params_from_str(
str::from_utf8(response_decrypted)
.with_context(|| "Reponse to decoding key request is corrupt")?,
vec![PARAM_DECODING_KEY],
)
.with_context(|| "Could not extract decoding parameters")?;
Ok(decoding_params)
}
fn decoding_params_request(
cbc_key: &str,
header: &OTRParams,
user: &str,
password: &str,
now: &str,
) -> anyhow::Result<String> {
let mut payload: String = "&A=".to_string()
+ user
+ "&P="
+ password
+ "&FN="
+ header.get(PARAM_FILENAME).unwrap()
+ "&OH="
+ header.get(PARAM_ENCODED_HASH).unwrap()
+ "&M="
+ &format!("{:02x}", Md5::digest(b"something"))
+ "&OS="
+ &format!("{:02x}", Md5::digest(b"Windows"))
+ "&LN=DE"
+ "&VN="
+ DECODER_VERSION
+ "&IR=TRUE"
+ "&IK="
+ IK
+ "&D=";
payload += &generate_hex_string(512 - BLOCK_SIZE - payload.len());
debug!(payload:serde=payload;
"Payload for decoding parameters request"
);
let init_vector = generate_byte_vector(BLOCK_SIZE);
let payload_as_bytes = unsafe { payload.as_bytes_mut() };
let payload_encrypted = cbc::Encryptor::<BlowfishLE>::new_from_slices(
&hex::decode(cbc_key).with_context(|| "Could not turn CBC key into byte array")?,
&init_vector,
)
.with_context(|| {
"Could not create cipher object for encryption of decryption key request payload"
})?
.encrypt_padded_mut::<NoPadding>(payload_as_bytes, 512 - BLOCK_SIZE)
.unwrap_or_else(|_| panic!("Could not encrypt decryption key request payload"));
let mut code = init_vector;
code.extend_from_slice(payload_encrypted);
let request: String = OTR_URL.to_string()
+ "?code="
+ &general_purpose::STANDARD.encode(code)
+ "&AA="
+ user
+ "&ZZ="
+ now;
Ok(request)
}
fn file_size_from_params(header_params: &OTRParams) -> usize {
header_params
.get(PARAM_FILESIZE)
.unwrap()
.parse::<usize>()
.unwrap()
}
fn hashing_queue(queue: Receiver<Chunk>) -> [u8; 16] {
let mut hasher = Md5::new();
for data in queue {
hasher.update(data);
}
let mut checksum = [0u8; 16];
checksum.clone_from_slice(&hasher.finalize()[..]);
checksum
}
fn header_params(in_file: &mut File) -> anyhow::Result<OTRParams> {
let mut buffer = [0; HEADER_LENGTH];
if in_file
.read(&mut buffer)
.with_context(|| "Could not read file")?
< HEADER_LENGTH
{
return Err(anyhow!("File is too short"));
}
if str::from_utf8(&buffer[0..FILETYPE_LENGTH])? != OTRKEY_FILETYPE {
debug!(
"OTRKEY file header is: \"{}\"",
str::from_utf8(&buffer[0..FILETYPE_LENGTH]).unwrap()
);
return Err(anyhow!("File does not start with \"{}\"", OTRKEY_FILETYPE));
}
ecb::Decryptor::<BlowfishLE>::new_from_slice(
&hex::decode(PREAMBLE_KEY).with_context(|| "Could not decrypt preamble key")?,
)
.with_context(|| "Could not create cipher object for header decryption")?
.decrypt_padded_mut::<NoPadding>(&mut buffer[FILETYPE_LENGTH..])
.unwrap_or_else(|_| panic!("Could not decode file header"));
let header_params = params_from_str(
str::from_utf8(&buffer[FILETYPE_LENGTH..])
.with_context(|| "Decrypted file header is corrupt")?,
vec![
PARAM_FILENAME,
PARAM_FILESIZE,
PARAM_ENCODED_HASH,
PARAM_DECODED_HASH,
],
)
.with_context(|| "Could not extract parameters from file header")?;
Ok(header_params)
}
fn generate_byte_vector(len: usize) -> Vec<u8> {
let mut bytes = Vec::<u8>::new();
for i in 0..len {
bytes.push((i % 256).try_into().unwrap());
}
bytes
}
fn generate_hex_string(len: usize) -> String {
let mut result = String::with_capacity(len);
for i in 0..len {
result.push(HEX_CHARS.chars().nth(i % HEX_CHARS.len()).unwrap());
}
result
}
fn params_from_str(params_str: &str, must_have: Vec<&str>) -> anyhow::Result<OTRParams> {
let mut params: OTRParams = HashMap::new();
for param in params_str.split('&') {
if param.is_empty() {
continue;
}
let a: Vec<&str> = param.split('=').collect();
params.insert(a[0].to_string(), a[1].to_string());
}
debug!(params:serde = params; "OTRKEY file parameters");
for key in must_have {
if !params.contains_key(key) {
return Err(anyhow!("Parameter \"{}\" could not be extracted", key));
}
}
Ok(params)
}
fn verify_checksum(checksum: &[u8], hash: &str) -> anyhow::Result<bool> {
if hash.len() != 48 {
return Err(anyhow!("MD5 hash must be 48 characters long"));
}
let reduced_hash = hex::decode(
hash.chars()
.enumerate()
.filter_map(|(i, c)| if (i + 1) % 3 != 0 { Some(c) } else { None })
.collect::<String>(),
)
.context("Could not turn hash {} into bytes")?;
Ok(checksum == reduced_hash)
}