piss
This commit is contained in:
commit
96eff29088
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
|
@ -0,0 +1,5 @@
|
|||
# qobuz decryptor crap
|
||||
|
||||
build: it's `cargo build` u ass
|
||||
|
||||
usage: `./trk_dec <auth token> <track id> <format id>`
|
|
@ -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(¶ms_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", ¶ms, 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
|
||||
}
|
|
@ -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(¶ms).unwrap();
|
||||
for (key, val) in params {
|
||||
signature.push_str(&key);
|
||||
signature.push_str(&val);
|
||||
};
|
||||
|
||||
signature.push_str(×tamp.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
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue