1use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
6use anyhow::{anyhow, Context};
7use base64::{engine::general_purpose, Engine};
8use blowfish::{cipher::KeyInit, BlowfishLE};
9use cbc;
10use chrono::Datelike;
11use ecb;
12use log::*;
13use md5::{Digest, Md5};
14use std::{
15 clone::Clone,
16 collections::HashMap,
17 fmt::Debug,
18 fs::remove_file,
19 fs::File,
20 io::prelude::*,
21 marker::Copy,
22 path::Path,
23 str,
24 sync::mpsc::{channel, Receiver},
25 thread,
26};
27
28const OTR_URL: &str = "http://onlinetvrecorder.com/quelle_neu1.php";
30const DECODER_VERSION: &str = "0.4.1133";
32const FILETYPE_LENGTH: usize = 10;
34const PREAMBLE_LENGTH: usize = 512;
35const HEADER_LENGTH: usize = FILETYPE_LENGTH + PREAMBLE_LENGTH;
36const PREAMBLE_KEY: &str = "EF3AB29CD19F0CAC5759C7ABD12CC92BA3FE0AFEBF960D63FEBD0F45";
38const IK: &str = "aFzW1tL7nP9vXd8yUfB5kLoSyATQ";
39const OTRKEY_FILETYPE: &str = "OTRKEYFILE";
41const OTR_ERROR_INDICATOR: &str = "MessageToBePrintedInDecoder";
43const PARAM_FILENAME: &str = "FN";
45const PARAM_FILESIZE: &str = "SZ";
46const PARAM_ENCODED_HASH: &str = "OH";
47const PARAM_DECODED_HASH: &str = "FH";
48const PARAM_DECODING_KEY: &str = "HP";
50const BLOCK_SIZE: usize = 8;
52const MAX_CHUNK_SIZE: usize = 10 * 1024 * 1024;
53
54const HEX_CHARS: &str = "0123456789abcdef";
55
56type OTRParams = HashMap<String, String>;
58
59type Chunk = Vec<u8>;
61
62pub fn decode<P, Q>(in_video: P, out_video: Q, user: &str, password: &str) -> anyhow::Result<()>
65where
66 P: AsRef<Path>,
67 Q: AsRef<Path> + Debug + Copy,
68{
69 if MAX_CHUNK_SIZE % BLOCK_SIZE != 0 {
71 return Err(anyhow!(
72 "Chunk size [{}] is not a multiple of block size [{}]",
73 MAX_CHUNK_SIZE,
74 BLOCK_SIZE
75 ));
76 }
77
78 let mut in_file = File::open(&in_video)?;
80 let header_params =
81 header_params(&mut in_file).with_context(|| "Could not extract video header from")?;
82
83 if (in_file.metadata()?.len() as usize) < file_size_from_params(&header_params) {
85 return Err(anyhow!("Video file seems to be corrupt: it is too small"));
86 }
87
88 let now = current_date();
90 let cbc_key = cbc_key(user, password, &now).with_context(|| {
92 "Could not determine CBC key for encryption of decoding key request payload"
93 })?;
94 let decoding_params = decoding_params(
96 &cbc_key,
97 &decoding_params_request(&cbc_key, &header_params, user, password, &now)
98 .with_context(|| "Could not assemble request for decoding key")?,
99 )
100 .with_context(|| "Could not retrieve decoding key")?;
101
102 if let Err(err) = decode_in_parallel(
104 &mut in_file,
105 out_video,
106 &header_params,
107 decoding_params.get(PARAM_DECODING_KEY).unwrap(),
108 ) {
109 remove_file(out_video).unwrap_or_else(|_| {
110 panic!(
111 "Could not delete file \"{}\" after error when decoding video",
112 out_video.as_ref().display()
113 )
114 });
115 return Err(err);
116 }
117
118 remove_file(&in_video).with_context(|| "Could not delete video after successful decoding")?;
120
121 Ok(())
122}
123
124fn cbc_key(user: &str, password: &str, now: &str) -> anyhow::Result<String> {
126 let user_hash = format!("{:02x}", Md5::digest(user.as_bytes()));
127 let password_hash = format!("{:02x}", Md5::digest(password.as_bytes()));
128 let cbc_key: String = user_hash[0..13].to_string()
129 + &now[..4]
130 + &password_hash[0..11]
131 + &now[4..6]
132 + &user_hash[21..32]
133 + &now[6..]
134 + &password_hash[19..32];
135
136 debug!(cbc_key:serde = cbc_key; "CBC_KEY");
137
138 Ok(cbc_key)
139}
140
141fn chunk_sizes(file_size: usize) -> Vec<usize> {
146 let (full_chunks, remainder) = (file_size / MAX_CHUNK_SIZE, file_size % MAX_CHUNK_SIZE);
147 let mut sizes: Vec<usize> = vec![MAX_CHUNK_SIZE; full_chunks];
148 if remainder / BLOCK_SIZE > 0 {
149 sizes.push(remainder / BLOCK_SIZE * BLOCK_SIZE);
150 }
151 if remainder % BLOCK_SIZE > 0 {
152 sizes.push(remainder % BLOCK_SIZE);
153 }
154 sizes
155}
156
157fn current_date() -> String {
159 let now = chrono::Local::now().date_naive();
160 format!("{:04}{:02}{:02}", now.year(), now.month(), now.day())
161}
162
163fn decode_chunk(key: &str, mut chunk: Chunk) -> Chunk {
167 if chunk.capacity() >= BLOCK_SIZE {
170 ecb::Decryptor::<BlowfishLE>::new_from_slice(
171 &hex::decode(key).expect("Could not turn decoding key into hex string"),
172 )
173 .unwrap_or_else(|_| panic!("Could not create cipher object for decoding of chunk"))
174 .decrypt_padded_mut::<NoPadding>(&mut chunk)
175 .unwrap_or_else(|_| panic!("Could not decode chunk"));
176 }
177 chunk
178}
179
180fn decode_in_parallel<P>(
183 in_file: &mut File,
184 out_video: P,
185 header_params: &OTRParams,
186 key: &str,
187) -> anyhow::Result<()>
188where
189 P: AsRef<Path> + Debug,
190{
191 let mut out_file = File::create(&out_video).with_context(|| {
193 format!(
194 "Could not create result file \"{}\"",
195 out_video.as_ref().display()
196 )
197 })?;
198
199 let mut thread_handles = vec![];
201
202 let (enc_hash_sender, enc_hash_receiver) = channel();
205 let (dec_hash_sender, dec_hash_receiver) = channel();
206 let (enc_hash_handle, dec_hash_handle) = (
207 thread::spawn(move || -> [u8; 16] { hashing_queue(enc_hash_receiver) }),
208 thread::spawn(move || -> [u8; 16] { hashing_queue(dec_hash_receiver) }),
209 );
210
211 for chunk_size in chunk_sizes(file_size_from_params(header_params) - HEADER_LENGTH) {
214 let mut chunk = vec![0u8; chunk_size];
216
217 if in_file
219 .read(&mut chunk[..chunk_size])
220 .with_context(|| "Could not read chunk")?
221 < chunk_size
222 {
223 return Err(anyhow!("Chunk is too short"));
224 }
225
226 enc_hash_sender.send(chunk.clone()).unwrap();
228
229 let dec_key = key.to_string();
231 thread_handles.push(thread::spawn(move || -> Chunk {
232 decode_chunk(&dec_key, chunk)
233 }));
234 }
235
236 drop(enc_hash_sender);
238
239 for handle in thread_handles {
243 match handle.join() {
244 Ok(chunk) => {
245 dec_hash_sender.send(chunk.clone()).unwrap();
247 out_file.write_all(&chunk).with_context(|| {
249 format!(
250 "Could not write to decoded video file \"{}\"",
251 out_video.as_ref().display(),
252 )
253 })?;
254 }
255 Err(_) => {
256 return Err(anyhow!(
257 "Could not create decoded video file \"{}\"",
258 out_video.as_ref().display()
259 ));
260 }
261 }
262 }
263
264 drop(dec_hash_sender);
266
267 if !verify_checksum(
269 &enc_hash_handle.join().unwrap(),
270 &header_params[PARAM_ENCODED_HASH],
271 )
272 .context("Could not verify checksum of encoded video file")?
273 {
274 return Err(anyhow!("MD5 checksum of encoded video file is not correct"));
275 }
276 if !verify_checksum(
277 &dec_hash_handle.join().unwrap(),
278 &header_params[PARAM_DECODED_HASH],
279 )
280 .context("Could not verify checksum of decoded video file")?
281 {
282 return Err(anyhow!("MD5 checksum of decoded video file is not correct"));
283 }
284
285 Ok(())
286}
287
288fn decoding_params(cbc_key: &str, request: &str) -> anyhow::Result<OTRParams> {
291 let response = reqwest::blocking::Client::builder()
293 .user_agent("Windows-OTR-Decoder/".to_string() + DECODER_VERSION)
294 .build()
295 .with_context(|| "Could not create HTTP client to request decoding key")?
296 .get(request)
297 .send()
298 .with_context(|| "Did not get a response for decoding key request")?
299 .text()
300 .with_context(|| {
301 "Response to decoding key request is corrupted: could not turn into text"
302 })?;
303
304 if response.len() < OTR_ERROR_INDICATOR.len() {
307 return Err(anyhow!(
308 "Unidentifiable error while requesting decoding key"
309 ));
310 }
311 if &response[..OTR_ERROR_INDICATOR.len()] == OTR_ERROR_INDICATOR {
312 return Err(anyhow!(
313 "Error while requesting decoding key: \"{}\"",
314 response[OTR_ERROR_INDICATOR.len()..].to_string()
315 ));
316 }
317
318 let mut response = general_purpose::STANDARD
320 .decode(&response)
321 .with_context(|| "Could not decode response to decoding key request from base64")?;
322
323 if response.len() < 2 * BLOCK_SIZE || response.len() % BLOCK_SIZE != 0 {
325 return Err(anyhow!(
326 "Response to decoding key request is corrupted: must be a multiple of {}",
327 BLOCK_SIZE
328 ));
329 }
330
331 let init_vector = &response[..BLOCK_SIZE];
333 let response_decrypted = cbc::Decryptor::<BlowfishLE>::new_from_slices(
334 &hex::decode(cbc_key).with_context(|| "Could not turn CBC key into byte array")?,
335 init_vector,
336 )
337 .with_context(|| "Could not create cipher object for decryption of decoding key response")?
338 .decrypt_padded_mut::<NoPadding>(&mut response[BLOCK_SIZE..])
339 .unwrap_or_else(|_| panic!("Could not decrypt decryption key response"));
340
341 let decoding_params = params_from_str(
343 str::from_utf8(response_decrypted)
344 .with_context(|| "Reponse to decoding key request is corrupt")?,
345 vec![PARAM_DECODING_KEY],
346 )
347 .with_context(|| "Could not extract decoding parameters")?;
348
349 Ok(decoding_params)
350}
351
352fn decoding_params_request(
354 cbc_key: &str,
355 header: &OTRParams,
356 user: &str,
357 password: &str,
358 now: &str,
359) -> anyhow::Result<String> {
360 let mut payload: String = "&A=".to_string()
362 + user
363 + "&P="
364 + password
365 + "&FN="
366 + header.get(PARAM_FILENAME).unwrap()
367 + "&OH="
368 + header.get(PARAM_ENCODED_HASH).unwrap()
369 + "&M="
370 + &format!("{:02x}", Md5::digest(b"something"))
371 + "&OS="
372 + &format!("{:02x}", Md5::digest(b"Windows"))
373 + "&LN=DE"
374 + "&VN="
375 + DECODER_VERSION
376 + "&IR=TRUE"
377 + "&IK="
378 + IK
379 + "&D=";
380 payload += &generate_hex_string(512 - BLOCK_SIZE - payload.len());
381
382 debug!(payload:serde=payload;
383 "Payload for decoding parameters request"
384 );
385
386 let init_vector = generate_byte_vector(BLOCK_SIZE);
388 let payload_as_bytes = unsafe { payload.as_bytes_mut() };
389 let payload_encrypted = cbc::Encryptor::<BlowfishLE>::new_from_slices(
390 &hex::decode(cbc_key).with_context(|| "Could not turn CBC key into byte array")?,
391 &init_vector,
392 )
393 .with_context(|| {
394 "Could not create cipher object for encryption of decryption key request payload"
395 })?
396 .encrypt_padded_mut::<NoPadding>(payload_as_bytes, 512 - BLOCK_SIZE)
397 .unwrap_or_else(|_| panic!("Could not encrypt decryption key request payload"));
398
399 let mut code = init_vector;
401 code.extend_from_slice(payload_encrypted);
402
403 let request: String = OTR_URL.to_string()
405 + "?code="
406 + &general_purpose::STANDARD.encode(code)
407 + "&AA="
408 + user
409 + "&ZZ="
410 + now;
411
412 Ok(request)
413}
414
415fn file_size_from_params(header_params: &OTRParams) -> usize {
418 header_params
419 .get(PARAM_FILESIZE)
420 .unwrap()
421 .parse::<usize>()
422 .unwrap()
423}
424
425fn hashing_queue(queue: Receiver<Chunk>) -> [u8; 16] {
428 let mut hasher = Md5::new();
429
430 for data in queue {
431 hasher.update(data);
432 }
433
434 let mut checksum = [0u8; 16];
436 checksum.clone_from_slice(&hasher.finalize()[..]);
437 checksum
438}
439
440fn header_params(in_file: &mut File) -> anyhow::Result<OTRParams> {
443 let mut buffer = [0; HEADER_LENGTH];
444
445 if in_file
447 .read(&mut buffer)
448 .with_context(|| "Could not read file")?
449 < HEADER_LENGTH
450 {
451 return Err(anyhow!("File is too short"));
452 }
453
454 if str::from_utf8(&buffer[0..FILETYPE_LENGTH])? != OTRKEY_FILETYPE {
456 debug!(
457 "OTRKEY file header is: \"{}\"",
458 str::from_utf8(&buffer[0..FILETYPE_LENGTH]).unwrap()
459 );
460
461 return Err(anyhow!("File does not start with \"{}\"", OTRKEY_FILETYPE));
462 }
463
464 ecb::Decryptor::<BlowfishLE>::new_from_slice(
466 &hex::decode(PREAMBLE_KEY).with_context(|| "Could not decrypt preamble key")?,
467 )
468 .with_context(|| "Could not create cipher object for header decryption")?
469 .decrypt_padded_mut::<NoPadding>(&mut buffer[FILETYPE_LENGTH..])
470 .unwrap_or_else(|_| panic!("Could not decode file header"));
471
472 let header_params = params_from_str(
474 str::from_utf8(&buffer[FILETYPE_LENGTH..])
475 .with_context(|| "Decrypted file header is corrupt")?,
476 vec![
477 PARAM_FILENAME,
478 PARAM_FILESIZE,
479 PARAM_ENCODED_HASH,
480 PARAM_DECODED_HASH,
481 ],
482 )
483 .with_context(|| "Could not extract parameters from file header")?;
484
485 Ok(header_params)
486}
487
488fn generate_byte_vector(len: usize) -> Vec<u8> {
490 let mut bytes = Vec::<u8>::new();
491 for i in 0..len {
492 bytes.push((i % 256).try_into().unwrap());
493 }
494 bytes
495}
496
497fn generate_hex_string(len: usize) -> String {
499 let mut result = String::with_capacity(len);
500 for i in 0..len {
501 result.push(HEX_CHARS.chars().nth(i % HEX_CHARS.len()).unwrap());
502 }
503 result
504}
505
506fn params_from_str(params_str: &str, must_have: Vec<&str>) -> anyhow::Result<OTRParams> {
510 let mut params: OTRParams = HashMap::new();
511
512 for param in params_str.split('&') {
513 if param.is_empty() {
514 continue;
515 }
516 let a: Vec<&str> = param.split('=').collect();
518 params.insert(a[0].to_string(), a[1].to_string());
519 }
520
521 debug!(params:serde = params; "OTRKEY file parameters");
522
523 for key in must_have {
525 if !params.contains_key(key) {
526 return Err(anyhow!("Parameter \"{}\" could not be extracted", key));
527 }
528 }
529
530 Ok(params)
531}
532
533fn verify_checksum(checksum: &[u8], hash: &str) -> anyhow::Result<bool> {
535 if hash.len() != 48 {
536 return Err(anyhow!("MD5 hash must be 48 characters long"));
537 }
538
539 let reduced_hash = hex::decode(
541 hash.chars()
542 .enumerate()
543 .filter_map(|(i, c)| if (i + 1) % 3 != 0 { Some(c) } else { None })
544 .collect::<String>(),
545 )
546 .context("Could not turn hash {} into bytes")?;
547
548 Ok(checksum == reduced_hash)
549}