mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41:02 +00:00
Now implements streaming download+decryption entirely in Rust: - Added reqwest/tokio/futures-util dependencies - Created StreamingDecryptor for chunk-by-chunk decryption - New download_and_decrypt_track command streams to disk directly - Frontend simplified to single invoke() call
176 lines
5.6 KiB
Rust
176 lines
5.6 KiB
Rust
use blowfish::cipher::{BlockDecrypt, KeyInit};
|
|
use blowfish::Blowfish;
|
|
use byteorder::BigEndian;
|
|
|
|
/// Generate Blowfish key for Deezer track decryption
|
|
pub fn generate_blowfish_key(track_id: &str) -> Vec<u8> {
|
|
const SECRET: &[u8] = b"g4el58wc0zvf9na1";
|
|
|
|
// MD5 hash of track ID
|
|
let id_md5 = format!("{:x}", md5::compute(track_id.as_bytes()));
|
|
|
|
// Generate 16-byte key by XORing MD5 parts with secret
|
|
let mut bf_key = Vec::with_capacity(16);
|
|
for i in 0..16 {
|
|
let md5_byte1 = id_md5.as_bytes()[i];
|
|
let md5_byte2 = id_md5.as_bytes()[i + 16];
|
|
let secret_byte = SECRET[i];
|
|
bf_key.push(md5_byte1 ^ md5_byte2 ^ secret_byte);
|
|
}
|
|
|
|
bf_key
|
|
}
|
|
|
|
/// Decrypt a single 2048-byte chunk using Blowfish CBC
|
|
pub fn decrypt_chunk(chunk: &[u8], blowfish_key: &[u8]) -> Vec<u8> {
|
|
let cipher = Blowfish::<BigEndian>::new_from_slice(blowfish_key)
|
|
.expect("Invalid key length");
|
|
|
|
let mut result = chunk.to_vec();
|
|
let iv = [0u8, 1, 2, 3, 4, 5, 6, 7];
|
|
|
|
// Decrypt in CBC mode
|
|
let mut prev_block = iv;
|
|
|
|
for chunk_idx in (0..result.len()).step_by(8) {
|
|
if chunk_idx + 8 <= result.len() {
|
|
let mut block = [0u8; 8];
|
|
block.copy_from_slice(&result[chunk_idx..chunk_idx + 8]);
|
|
|
|
let encrypted_block = block;
|
|
cipher.decrypt_block((&mut block).into());
|
|
|
|
// XOR with previous ciphertext (CBC mode)
|
|
for i in 0..8 {
|
|
block[i] ^= prev_block[i];
|
|
}
|
|
|
|
result[chunk_idx..chunk_idx + 8].copy_from_slice(&block);
|
|
prev_block = encrypted_block;
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Decrypt track data using Deezer's encryption scheme
|
|
/// Every 3 chunks (6144 bytes), only the first 2048 bytes are encrypted
|
|
pub fn decrypt_track(data: &[u8], track_id: &str) -> Vec<u8> {
|
|
const CHUNK_SIZE: usize = 2048;
|
|
const WINDOW_SIZE: usize = CHUNK_SIZE * 3; // 6144
|
|
|
|
let blowfish_key = generate_blowfish_key(track_id);
|
|
let mut result = Vec::with_capacity(data.len());
|
|
|
|
let mut offset = 0;
|
|
|
|
while offset < data.len() {
|
|
let remaining = data.len() - offset;
|
|
|
|
if remaining >= WINDOW_SIZE {
|
|
// Full window: decrypt first 2048 bytes, keep next 4096 as-is
|
|
let encrypted_chunk = &data[offset..offset + CHUNK_SIZE];
|
|
let plain_part = &data[offset + CHUNK_SIZE..offset + WINDOW_SIZE];
|
|
|
|
let decrypted = decrypt_chunk(encrypted_chunk, &blowfish_key);
|
|
result.extend_from_slice(&decrypted);
|
|
result.extend_from_slice(plain_part);
|
|
|
|
offset += WINDOW_SIZE;
|
|
} else if remaining >= CHUNK_SIZE {
|
|
// Partial window: decrypt first 2048 bytes, keep rest as-is
|
|
let encrypted_chunk = &data[offset..offset + CHUNK_SIZE];
|
|
let plain_part = &data[offset + CHUNK_SIZE..];
|
|
|
|
let decrypted = decrypt_chunk(encrypted_chunk, &blowfish_key);
|
|
result.extend_from_slice(&decrypted);
|
|
result.extend_from_slice(plain_part);
|
|
|
|
offset = data.len();
|
|
} else {
|
|
// Less than 2048 bytes: keep as-is
|
|
result.extend_from_slice(&data[offset..]);
|
|
offset = data.len();
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Streaming decryption state for processing data chunk-by-chunk
|
|
pub struct StreamingDecryptor {
|
|
blowfish_key: Vec<u8>,
|
|
buffer: Vec<u8>,
|
|
}
|
|
|
|
impl StreamingDecryptor {
|
|
const CHUNK_SIZE: usize = 2048;
|
|
const WINDOW_SIZE: usize = Self::CHUNK_SIZE * 3; // 6144
|
|
|
|
pub fn new(track_id: &str) -> Self {
|
|
Self {
|
|
blowfish_key: generate_blowfish_key(track_id),
|
|
buffer: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Process incoming data and return decrypted output
|
|
/// May return less data than input as it buffers to maintain 6144-byte windows
|
|
pub fn process(&mut self, data: &[u8]) -> Vec<u8> {
|
|
self.buffer.extend_from_slice(data);
|
|
let mut output = Vec::new();
|
|
|
|
// Process complete windows
|
|
while self.buffer.len() >= Self::WINDOW_SIZE {
|
|
let encrypted_chunk = &self.buffer[0..Self::CHUNK_SIZE];
|
|
let plain_part = &self.buffer[Self::CHUNK_SIZE..Self::WINDOW_SIZE];
|
|
|
|
let decrypted = decrypt_chunk(encrypted_chunk, &self.blowfish_key);
|
|
output.extend_from_slice(&decrypted);
|
|
output.extend_from_slice(plain_part);
|
|
|
|
self.buffer.drain(0..Self::WINDOW_SIZE);
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
/// Finalize decryption and return any remaining buffered data
|
|
pub fn finalize(self) -> Vec<u8> {
|
|
if self.buffer.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let remaining = self.buffer.len();
|
|
|
|
if remaining >= Self::CHUNK_SIZE {
|
|
// Partial window: decrypt first 2048 bytes, keep rest as-is
|
|
let encrypted_chunk = &self.buffer[0..Self::CHUNK_SIZE];
|
|
let plain_part = &self.buffer[Self::CHUNK_SIZE..];
|
|
|
|
let decrypted = decrypt_chunk(encrypted_chunk, &self.blowfish_key);
|
|
let mut output = Vec::with_capacity(remaining);
|
|
output.extend_from_slice(&decrypted);
|
|
output.extend_from_slice(plain_part);
|
|
output
|
|
} else {
|
|
// Less than 2048 bytes: keep as-is
|
|
self.buffer
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_key_generation() {
|
|
let key = generate_blowfish_key("99756342");
|
|
assert_eq!(key.len(), 16);
|
|
// Key should be: 3333676c346d7c62372b7c3e6d32626c (in hex)
|
|
assert_eq!(format!("{:02x}", key[0]), "33");
|
|
assert_eq!(format!("{:02x}", key[1]), "33");
|
|
}
|
|
}
|