Skip to content

Commit

Permalink
feat: decrypt and play tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
roderickvd committed Nov 5, 2024
1 parent e670d2e commit eaa2642
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 108 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ tokio = { version = "1", features = [
"time",
] }
tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] }
tokio-util = "0.7"
toml = "0.8"
url = "2.3"
uuid = { version = "1.2", features = ["fast-rng", "serde", "v4", "v5"] }
Expand Down
6 changes: 5 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::arl::Arl;
use uuid::Uuid;

use crate::{arl::Arl, decrypt::Key};

/// Methods that can be used to authenticate with Deezer.
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Credentials {
Expand Down Expand Up @@ -68,4 +69,7 @@ pub struct Config {

/// The credentials used to authenticate with Deezer.
pub credentials: Credentials,

/// Secret for computing the track decryption key.
pub bf_secret: Key,
}
94 changes: 56 additions & 38 deletions src/decrypt.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::{
fs,
io::{self, Cursor, Read, Seek, SeekFrom},
num::NonZeroU64,
ops::Deref,
str::FromStr,
};

use blowfish::{cipher::BlockDecryptMut, cipher::KeyIvInit, Blowfish};
use cbc::cipher::block_padding::NoPadding;
use md5::{Digest, Md5};
use stream_download::storage::temp::TempStorageReader;

use crate::{
error::{Error, Result},
Expand Down Expand Up @@ -39,7 +41,7 @@ use crate::{
/// `Read` and `Seek` implementations passed through.
pub struct Decrypt {
/// The file to decrypt.
reader: TempStorageReader,
file: fs::File,

/// The size of the file.
file_size: Option<u64>,
Expand All @@ -62,8 +64,38 @@ pub struct Decrypt {
/// The fixed length of a decryption key.
pub const KEY_LENGTH: usize = 16;

/// A decryption key fixed length.
pub type Key = [u8; KEY_LENGTH];
pub type RawKey = [u8; KEY_LENGTH];

/// A decryption key with fixed length.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct Key(RawKey);

impl FromStr for Key {
type Err = Error;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let len = s.len();
if len != KEY_LENGTH {
return Err(Error::out_of_range(format!(
"key length is {len} but should be {KEY_LENGTH}",
)));
}

let bytes = s.as_bytes();
let mut key = [0; KEY_LENGTH];
key.copy_from_slice(bytes);

Ok(Self(key))
}
}

impl Deref for Key {
type Target = RawKey;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Decrypt {
/// The initialization vector to use for CBC decryption. Deezer uses a fixed
Expand Down Expand Up @@ -92,14 +124,14 @@ impl Decrypt {
return Err(Error::unimplemented("unsupported encryption algorithm"));
}

let mut reader = track.try_reader()?;
reader.rewind()?;
let mut file = track.try_file()?;
file.rewind()?;

// Calculate decryption key.
let key = Self::key_for_track_id(track.id(), salt);

Ok(Self {
reader,
file,
file_size: track.file_size(),
cipher: track.cipher(),
key,
Expand All @@ -115,16 +147,17 @@ impl Decrypt {
let track_hash = format!("{:x}", Md5::digest(track_id.to_string()));
let track_hash = track_hash.as_bytes();

let mut key = [0; KEY_LENGTH];
let mut key = RawKey::default();
for i in 0..KEY_LENGTH {
key[i] = track_hash[i] ^ track_hash[i + KEY_LENGTH] ^ salt[i];
}
key
Key(key)
}

/// Calculates number of bytes in the buffer that have not been read yet.
#[must_use]
fn bytes_on_buffer(&self) -> u64 {
// TODO : prevent panic
self.buffer.get_ref().len() as u64 - self.buffer.position()
}
}
Expand All @@ -135,7 +168,7 @@ impl Seek for Decrypt {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
// If the track is not encrypted, we can seek directly.
if self.cipher == Cipher::NONE {
return self.reader.seek(pos);
return self.file.seek(pos);
}

// Calculate the target position in the encrypted file.
Expand All @@ -148,22 +181,19 @@ impl Seek for Decrypt {
"cannot seek from the end of a stream with unknown size",
))?;

let f = (file_size as i64)
.checked_sub(pos + 1)
file_size
.checked_add_signed(pos)
.and_then(|pos| pos.checked_sub(1))
.ok_or(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid seek to a negative or overflowing position",
))?;

// TODO
f as u64
))?
}

SeekFrom::Current(pos) => {
let current = self
.reader
.stream_position()?
.wrapping_add(self.buffer.position());
let current = self.block.map_or(0, |block| {
block * Self::CBC_BLOCK_SIZE as u64 + self.buffer.position()
});

current.checked_add_signed(pos).ok_or(io::Error::new(
io::ErrorKind::InvalidInput,
Expand All @@ -179,8 +209,6 @@ impl Seek for Decrypt {
));
}

debug!(" 1 {pos:?} ({:?})", self.file_size);

// The encrypted file is striped into blocks of STRIPE_SIZE bytes,
// alternating between encrypted and non-encrypted blocks. Calculate
// the block number within the encrypted file and the offset within the
Expand All @@ -200,23 +228,17 @@ impl Seek for Decrypt {

// If the buffer is empty, or the target block is different from the
// current block, read the block from the encrypted file.
if !self.block.is_some_and(|current| current == block) {
if self.block.is_none_or(|current| current != block) {
self.block = Some(block);

debug!(" 2 {pos:?} ({:?})", self.file_size);

// Seek to the start of the block in the encrypted file.
self.reader
self.file
.seek(SeekFrom::Start(block * Self::CBC_BLOCK_SIZE as u64))?;

debug!(" 3 seeked to pos {})", block * Self::CBC_BLOCK_SIZE as u64);

// TODO : when this is the first block of two unencrypted blocks,
// read ahead 2 * CBC_BLOCK_SIZE.
let mut buffer = [0; Self::CBC_BLOCK_SIZE];
let length = self.reader.read(&mut buffer)?;

debug!(" 4 {} {length}", Self::CBC_BLOCK_SIZE);
let length = self.file.read(&mut buffer)?;

// Decrypt the block if it is encrypted. Every third block is
// encrypted, and only if the block is of a full stripe size.
Expand All @@ -226,7 +248,7 @@ impl Seek for Decrypt {
if is_encrypted && is_full_block {
// The state of the cipher is reset on each block.
let cipher =
cbc::Decryptor::<Blowfish>::new_from_slices(&self.key, Self::CBC_BF_IV)
cbc::Decryptor::<Blowfish>::new_from_slices(&*self.key, Self::CBC_BF_IV)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;

// Decrypt the block in-place. The buffer is guaranteed to be
Expand All @@ -236,15 +258,11 @@ impl Seek for Decrypt {
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
}

debug!(" 5 {pos:?} ({:?})", self.file_size);

// Truncate the buffer to the actual length of the block.
let mut buffer = buffer.to_vec();
buffer.truncate(length);

self.buffer = Cursor::new(buffer);

debug!(" 6 {})", self.bytes_on_buffer());
}

// Set the offset position within the current block, and return the
Expand All @@ -260,7 +278,7 @@ impl Read for Decrypt {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// If the track is not encrypted, we can read directly.
if self.cipher == Cipher::NONE {
return self.reader.read(buf);
return self.file.read(buf);
}

let mut bytes_on_buffer = self.bytes_on_buffer();
Expand All @@ -285,7 +303,7 @@ impl Read for Decrypt {
// which should be equal to or larger than `bytes_wanted`.
let bytes_to_read = usize::min(
bytes_on_buffer.try_into().unwrap_or(usize::MAX),
bytes_wanted,
bytes_wanted.saturating_sub(bytes_read),
);
let bytes_read_from_buffer = self
.buffer
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ impl Error {
}
}

/// Create a not implemented error with the specified error.
/// Create an out of range error with the specified error.
pub fn out_of_range<E>(error: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
Expand Down
10 changes: 9 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ async fn run(args: Args) -> Result<()> {
}
};

let bf_secret = match secrets.get("bf_secret").and_then(|value| value.as_str()) {
Some(bf_secret) => bf_secret.parse()?,
None => {
todo!("bf_secret not found in secrets file");
}
};

let app_name = env!("CARGO_PKG_NAME").to_owned();
let app_version = env!("CARGO_PKG_VERSION").to_owned();
let app_lang = "en".to_owned();
Expand Down Expand Up @@ -260,11 +267,12 @@ async fn run(args: Args) -> Result<()> {
user_agent,

credentials,
bf_secret,
}
};

let player = Player::new(&config, &args.device)?;
let mut client = remote::Client::new(&config, player, true)?;
let mut client = remote::Client::new(&config, player)?;

// Restart after sleeping some duration to prevent accidental denial of
// service attacks on the Deezer infrastructure. Initially set the timer to
Expand Down
12 changes: 9 additions & 3 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use cpal::traits::{DeviceTrait, HostTrait};

use crate::{
config::Config,
decrypt::Decrypt,
decrypt::{Decrypt, Key},
error::{Error, Result},
events::Event,
http,
Expand All @@ -23,6 +23,9 @@ pub struct Player {
/// The license token to use for downloading tracks.
pub license_token: String,

/// The decryption key to use for decrypting tracks.
pub bf_secret: Key,

/// The list of tracks to play, a.k.a. the playlist.
tracks: Vec<Track>,

Expand Down Expand Up @@ -69,6 +72,7 @@ impl Player {
audio_quality: AudioQuality::default(),
client: http::Client::without_cookies(config)?,
license_token: String::new(),
bf_secret: config.bf_secret,
repeat_mode: RepeatMode::default(),
shuffle: false,
event_tx: None,
Expand Down Expand Up @@ -259,9 +263,10 @@ impl Player {
if let Some(position) = self.position {
if let Some(target_track) = self.tracks.get_mut(position) {
if target_track.is_complete() {
let decryptor = Decrypt::new(&target_track, b"0123456789123456")?;
let decoder = rodio::Decoder::new(decryptor)?;
let decryptor = Decrypt::new(target_track, &self.bf_secret)?;
let decoder = rodio::Decoder::new_flac(decryptor)?;
self.sink.append(decoder);
self.track_in_sink = self.position;
}

// Start downloading the track if it is pending.
Expand Down Expand Up @@ -335,6 +340,7 @@ impl Player {

if let Some(track) = self.track() {
// TODO - notify when moving to next track
// TODO : send stream_play on every pause/play?
if let Some(event_tx) = &self.event_tx {
if let Err(e) = event_tx.send(Event::TrackChanged(track.id())) {
error!("failed to send track changed event: {e}");
Expand Down
11 changes: 3 additions & 8 deletions src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ pub struct Client {
// TODO : merge with gateway
user_token: Option<UserToken>,

scheme: String,
version: String,
websocket_tx:
Option<SplitSink<WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>, WebsocketMessage>>,
Expand Down Expand Up @@ -97,7 +96,7 @@ impl Client {
/// - the `app_version` in `config` is not in [`SemVer`] format
///
/// [SemVer]: https://semver.org/
pub fn new(config: &Config, player: Player, secure: bool) -> Result<Self> {
pub fn new(config: &Config, player: Player) -> Result<Self> {
// Construct version in the form of `Mmmppp` where:
// - `M` is the major version
// - `mm` is the minor version
Expand All @@ -117,9 +116,6 @@ impl Client {
};
debug!("remote version: {version}");

let scheme = if secure { "wss" } else { "ws" };
debug!("remote scheme: {scheme}");

// Controllers send discovery requests every two seconds.
let time_to_live = Duration::from_secs(5);
let connection_offers = LruCache::with_expiry_duration(time_to_live);
Expand All @@ -139,7 +135,6 @@ impl Client {
gateway: Gateway::new(config)?,
user_token: None,

scheme: scheme.to_owned(),
version,
websocket_tx: None,

Expand Down Expand Up @@ -229,8 +224,8 @@ impl Client {
tokio::pin!(expiry);

let uri = format!(
"{}://live.deezer.com/ws/{}?version={}",
self.scheme, user_token, self.version
"wss://live.deezer.com/ws/{}?version={}",
user_token, self.version
)
.parse::<http::Uri>()?;
let mut request = ClientRequestBuilder::new(uri);
Expand Down
Loading

0 comments on commit eaa2642

Please sign in to comment.