This commit is contained in:
uh wot 2022-04-04 22:46:13 +02:00
commit 96eff29088
Signed by: uhwot
GPG Key ID: CB2454984587B781
8 changed files with 1674 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1132
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "trk_dec"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "3.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["rustls-tls", "json"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_urlencoded = "0.7"
serde_json = "1.0"
tokio = { version = "1.17", features = ["rt-multi-thread", "macros"] }
md-5 = "0.10"
hex = "0.4"
byteorder = "1.4"
base64 = "0.13"
hkdf = "0.12"
sha2 = "0.10"
aes = "0.8"
cbc = "0.1"
ctr = "0.9"

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# qobuz decryptor crap
build: it's `cargo build` u ass
usage: `./trk_dec <auth token> <track id> <format id>`

202
src/api.rs Normal file
View File

@ -0,0 +1,202 @@
use std::{time::{SystemTime, UNIX_EPOCH}, io::Write};
use reqwest::Client;
use serde::Deserialize;
use serde::{Serialize, de::DeserializeOwned};
use serde_json::json;
use clap::ArgEnum;
use crate::crypto::{gen_signed_req, get_session_key, get_track_key, decrypt_frame};
use crate::mp4::{parse_segment, BoxType, ExtBoxType};
const API_URL: &str = "https://www.qobuz.com/api.json/0.2/";
const APP_ID: u32 = 950096963;
const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36";
#[derive(Clone, ArgEnum, Debug)]
pub enum Method {
GET,
POST
}
pub struct APIClient {
client: Client,
auth_token: Option<String>,
session: Session
}
impl APIClient {
pub fn new() -> Self {
let client = Client::builder()
.user_agent(USER_AGENT)
.build()
.unwrap();
Self {
client,
auth_token: None,
session: Session {
id: String::new(),
key: [0u8; 16],
expiry: 0
}
}
}
pub fn set_auth_token(&mut self, auth_token: &str) {
self.auth_token = Some(auth_token.to_string());
}
pub async fn api_call<T, R>(&self, object: &str, method: &str, params: &T, req_method: Method, signed: bool) -> Result<R, reqwest::Error>
where T: Serialize + ?Sized,
R: DeserializeOwned {
let url = format!("{API_URL}{object}/{method}");
let reqwest_method = match req_method {
Method::GET => reqwest::Method::GET,
Method::POST => reqwest::Method::POST
};
let mut req = self.client.request(reqwest_method, url)
.header("x-app-id", APP_ID);
if let Some(token) = &self.auth_token {
req = req.header("x-user-auth-token", token);
}
match req_method {
Method::GET => {
if signed {
req = req.query(&gen_signed_req(object, method, params));
}
req = req.query(params);
},
Method::POST => {
let mut form_data = String::new();
if signed {
form_data.push_str(&serde_urlencoded::to_string(&gen_signed_req(object, method, params)).unwrap());
}
let params_form = serde_urlencoded::to_string(params).unwrap();
if !params_form.is_empty() {
form_data.push('&');
form_data.push_str(&params_form);
}
req = req.body(form_data)
.header("content-type", "application/x-www-form-urlencoded");
}
}
if object == "file" && method == "url" {
req = req.header("x-session-id", &self.session.id);
}
let resp = req.send().await?
.error_for_status()?;
resp.json().await
}
pub async fn renew_session(&mut self) {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if timestamp >= self.session.expiry {
let resp: SessionResp = self.api_call("session", "start", &json!({"profile":"qbz-1"}), Method::POST, true).await.unwrap();
self.session.id = resp.session_id;
get_session_key(&resp.infos, &mut self.session.key);
self.session.expiry = resp.expires_at;
}
}
pub async fn get_track_url(&mut self, track_id: u32, format_id: u32) -> Result<TrackUrl, reqwest::Error> {
self.renew_session().await;
let params = json!(
{
"track_id": track_id,
"format_id": format_id,
"intent": "stream"
}
);
let resp: TrackUrlResp = self.api_call("file", "url", &params, Method::GET, true).await?;
let mut key = [0u8; 16];
get_track_key(&resp.key, &self.session.key, &mut key);
Ok(TrackUrl {
url_template: resp.url_template,
key,
n_segments: resp.n_segments
})
}
pub async fn dl_track<T>(&self, url_template: &str, key: &[u8; 16], n_segments: u64, out: &mut T) -> Result<(), reqwest::Error>
where T: Write {
for seg in 0..n_segments + 1 {
let resp = self.client.get(url_template.replace("$SEGMENT$", &seg.to_string()))
.send().await?
.error_for_status()?;
let block = resp.bytes().await?;
let boxes = parse_segment(&block);
println!("segment {}", seg);
//println!("{:#?}", boxes);
for atom in &boxes {
println!("box size {}", atom.box_data.len());
if let BoxType::ExtendedBox {ext_box_type, ..} = &atom.box_type {
match ext_box_type {
ExtBoxType::Init { initial_data, .. } => {
out.write_all(initial_data).unwrap();
},
ExtBoxType::Segment {frames, ..} => {
for frame in frames {
if frame.flags != 0 {
let frame_data = decrypt_frame(&frame.frame_data, &key, &frame.iv_raw);
out.write_all(&frame_data).unwrap();
} else {
out.write_all(frame.frame_data).unwrap();
}
}
}
}
}
}
/*for b in boxes {
out.write_all(b.box_data).unwrap();
}*/
};
Ok(())
}
}
#[derive(Deserialize)]
struct TrackUrlResp {
url_template: String,
key: String,
n_segments: u64
}
pub struct TrackUrl {
pub url_template: String,
pub key: [u8; 16],
pub n_segments: u64
}
#[derive(Deserialize)]
struct SessionResp {
session_id: String,
infos: String,
expires_at: u64
}
struct Session {
id: String,
key: [u8; 16],
expiry: u64
}

84
src/crypto.rs Normal file
View File

@ -0,0 +1,84 @@
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{ser, Serialize};
use md5::{Md5, Digest};
use aes::cipher::{KeyIvInit, BlockDecryptMut, block_padding::NoPadding, StreamCipher};
use sha2::Sha256;
use hkdf::Hkdf;
const APP_SECRET: &str = "979549437fcc4a3faad4867b5cd25dcb";
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
type Aes128Ctr64BE = ctr::Ctr64BE<aes::Aes128>;
#[derive(Serialize)]
pub struct SignedReq {
pub request_ts: u64,
pub request_sig: String,
}
pub fn gen_signed_req<T>(object: &str, method: &str, params: &T) -> SignedReq
where T: ser::Serialize + ?Sized {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut signature = format!("{object}{method}");
let params = serde_urlencoded::to_string(params).unwrap();
let params: BTreeMap<String, String> = serde_urlencoded::from_str(&params).unwrap();
for (key, val) in params {
signature.push_str(&key);
signature.push_str(&val);
};
signature.push_str(&timestamp.to_string());
signature.push_str(APP_SECRET);
let mut hasher = Md5::new();
hasher.update(signature);
let signature = hex::encode(hasher.finalize());
SignedReq {request_ts: timestamp, request_sig: signature}
}
pub fn get_session_key(infos: &str, out: &mut [u8; 16]) {
let mut infos = infos.split('.');
let salt = infos.next().unwrap();
let info = infos.next().unwrap();
let salt = base64::decode_config(salt, base64::URL_SAFE_NO_PAD).unwrap();
let info = base64::decode_config(info, base64::URL_SAFE_NO_PAD).unwrap();
let hk = Hkdf::<Sha256>::new(Some(&salt), &hex::decode(APP_SECRET).unwrap());
hk.expand(&info, out).unwrap();
}
pub fn get_track_key(key_str: &str, session_key: &[u8; 16], out: &mut [u8; 16]) {
let mut key = key_str.split('.');
key.next();
let mut trk_key = [0u8; 32];
let mut iv = [0u8; 16];
base64::decode_config_slice(key.next().unwrap(), base64::URL_SAFE_NO_PAD, &mut trk_key).unwrap();
base64::decode_config_slice(key.next().unwrap(), base64::URL_SAFE_NO_PAD, &mut iv).unwrap();
Aes128CbcDec::new(session_key.into(), &iv.into())
.decrypt_padded_b2b_mut::<NoPadding>(&trk_key[..16], out)
.unwrap();
}
pub fn decrypt_frame(frame: &[u8], key: &[u8; 16], iv_raw: &[u8]) -> Vec<u8> {
let mut out = vec![0u8; frame.len()];
let mut iv = [0u8; 16];
iv[..iv_raw.len()].copy_from_slice(iv_raw);
Aes128Ctr64BE::new(key.into(), &iv.into())
.apply_keystream_b2b(frame, &mut out)
.unwrap();
out
}

38
src/main.rs Normal file
View File

@ -0,0 +1,38 @@
use std::fs::File;
use clap::Parser;
mod api;
use api::APIClient;
mod crypto;
mod mp4;
#[derive(Parser, Debug)]
struct Args {
auth_token: String,
track_id: u32,
format_id: u32
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let mut client = APIClient::new();
client.set_auth_token(&args.auth_token);
let url_resp = client.get_track_url(args.track_id, args.format_id).await.unwrap();
println!("{}", url_resp.url_template);
println!("{}", hex::encode(url_resp.key));
println!("{}", url_resp.n_segments);
let track_id = args.track_id;
let format_id = args.format_id;
let path = format!("{track_id}-{format_id}");
let mut file = File::create(path).unwrap();
client.dl_track(&url_resp.url_template, &url_resp.key, url_resp.n_segments, &mut file).await.unwrap();
}

188
src/mp4.rs Normal file
View File

@ -0,0 +1,188 @@
use std::io::{Cursor, Read};
use byteorder::{BigEndian, ReadBytesExt};
#[derive(Debug)]
pub struct Box<'a> {
pub box_data: &'a [u8],
pub box_type: BoxType<'a>,
}
#[derive(Debug)]
pub enum BoxType<'a> {
Box { box_type: String },
ExtendedBox { ext_box_type: ExtBoxType<'a>, version: u8, flags: u32 },
}
#[derive(Debug)]
pub enum ExtBoxType<'a> {
Init {
track_id: u32,
file_id: u32,
sample_rate: u32,
bits_per_sample: u8,
channels_count: u8,
samples_count: u64,
initial_data_size: u16,
initial_data: &'a [u8],
key_id_size: u8,
key_id: &'a [u8],
segments_count: u16,
segment_infos: Vec<SegmentInfo>
},
Segment {
base_offset: u32,
iv_size: u8,
n_frames: u32,
frames: Vec<Frame<'a>>
}
}
#[derive(Debug)]
pub struct SegmentInfo {
pub length: u32,
pub samples_count: u32
}
#[derive(Debug)]
pub struct Frame<'a> {
pub length: u32,
pub samples_count: u16,
pub flags: u16,
pub iv_raw: &'a [u8],
pub frame_data: &'a [u8]
}
pub fn parse_segment(buf: &[u8]) -> Vec<Box> {
let mut reader = Cursor::new(buf);
let mut boxes: Vec<Box> = Vec::new();
while reader.position() < buf.len() as u64 {
let size = reader.read_u32::<BigEndian>().unwrap();
let box_start = reader.position() - 4;
let box_end = box_start + size as u64;
let box_data = &buf[reader.position() as usize..box_end as usize];
let mut box_reader = Cursor::new(box_data);
let mut box_type = [0u8; 4];
box_reader.read_exact(&mut box_type).unwrap();
let box_type = String::from_utf8(box_type.to_vec()).unwrap();
let box_type_enum;
if box_type == "uuid" {
let uuid = box_reader.read_u128::<BigEndian>().unwrap();
let uuid = format!("{:016x}", uuid);
let version = box_reader.read_u8().unwrap();
let flags = box_reader.read_u24::<BigEndian>().unwrap();
let ext_box_data = &box_data[box_reader.position() as usize..];
let ext_box_type = match uuid.as_str() {
"c7c75df0fdd951e98fc22971e4acf8d2" => parse_init_box(ext_box_data),
"3b42129256f35f75923663b69a1f52b2" => parse_segment_box(ext_box_data, box_start, buf),
_ => panic!("Unknown extended box type: {}", uuid)
};
box_type_enum = BoxType::ExtendedBox {
ext_box_type,
version,
flags
};
} else {
box_type_enum = BoxType::Box {
box_type
};
}
boxes.push(Box {
box_data,
box_type: box_type_enum
});
reader.set_position(box_end);
}
boxes
}
fn parse_init_box<'a>(buf: &'a [u8]) -> ExtBoxType<'a> {
let mut reader = Cursor::new(buf);
let track_id = reader.read_u32::<BigEndian>().unwrap();
let file_id = reader.read_u32::<BigEndian>().unwrap();
let sample_rate = reader.read_u32::<BigEndian>().unwrap();
let bits_per_sample = reader.read_u8().unwrap();
let channels_count = reader.read_u8().unwrap();
let samples_count = reader.read_u64::<BigEndian>().unwrap();
let initial_data_size = reader.read_u16::<BigEndian>().unwrap();
let initial_data = &buf[reader.position() as usize..reader.position() as usize + initial_data_size as usize];
reader.set_position(reader.position() + initial_data_size as u64);
let key_id_size = reader.read_u8().unwrap();
let key_id = &buf[reader.position() as usize..reader.position() as usize + key_id_size as usize];
reader.set_position(reader.position() + key_id_size as u64);
let segments_count = reader.read_u16::<BigEndian>().unwrap();
let mut segment_infos: Vec<SegmentInfo> = Vec::new();
for _ in 0..segments_count {
segment_infos.push(SegmentInfo {
length: reader.read_u32::<BigEndian>().unwrap(),
samples_count: reader.read_u32::<BigEndian>().unwrap()
});
}
ExtBoxType::Init {
track_id,
file_id,
sample_rate,
bits_per_sample,
channels_count,
samples_count,
initial_data_size,
initial_data,
key_id_size,
key_id,
segments_count,
segment_infos
}
}
fn parse_segment_box<'a>(buf: &'a [u8], box_start: u64, seg_buf: &'a [u8]) -> ExtBoxType<'a> {
let mut reader = Cursor::new(buf);
let base_offset = reader.read_u32::<BigEndian>().unwrap();
let iv_size = reader.read_u8().unwrap();
let n_frames = reader.read_u24::<BigEndian>().unwrap();
let mut frames: Vec<Frame<'a>> = Vec::new();
let mut frame_start = box_start as usize + base_offset as usize;
for _ in 0..n_frames {
let length = reader.read_u32::<BigEndian>().unwrap();
let samples_count = reader.read_u16::<BigEndian>().unwrap();
let flags = reader.read_u16::<BigEndian>().unwrap();
let iv_raw = &buf[reader.position() as usize..reader.position() as usize + iv_size as usize];
reader.set_position(reader.position() + iv_size as u64);
let frame_end = frame_start + length as usize;
let frame_data = &seg_buf[frame_start..frame_end];
frames.push(Frame {
length,
samples_count,
flags,
iv_raw,
frame_data
});
frame_start = frame_end;
}
ExtBoxType::Segment {
base_offset,
iv_size,
n_frames,
frames
}
}