first commit
This commit is contained in:
448
deezspot/deezloader/__download_utils__.py
Normal file
448
deezspot/deezloader/__download_utils__.py
Normal file
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from hashlib import md5 as __md5
|
||||
|
||||
from binascii import (
|
||||
a2b_hex as __a2b_hex,
|
||||
b2a_hex as __b2a_hex
|
||||
)
|
||||
|
||||
from Crypto.Cipher.Blowfish import (
|
||||
new as __newBlowfish,
|
||||
MODE_CBC as __MODE_CBC
|
||||
)
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util import Counter
|
||||
import os
|
||||
from deezspot.libutils.logging_utils import logger
|
||||
|
||||
__secret_key = "g4el58wc0zvf9na1"
|
||||
__secret_key2 = b"jo6aey6haid2Teih"
|
||||
__idk = __a2b_hex("0001020304050607")
|
||||
|
||||
def md5hex(data: str):
|
||||
hashed = __md5(
|
||||
data.encode()
|
||||
).hexdigest()
|
||||
|
||||
return hashed
|
||||
|
||||
def gen_song_hash(song_id, song_md5, media_version):
|
||||
"""
|
||||
Generate a hash for the song using its ID, MD5 and media version.
|
||||
|
||||
Args:
|
||||
song_id: The song's ID
|
||||
song_md5: The song's MD5 hash
|
||||
media_version: The media version
|
||||
|
||||
Returns:
|
||||
str: The generated hash
|
||||
"""
|
||||
try:
|
||||
# Combine the song data
|
||||
data = f"{song_md5}{media_version}{song_id}"
|
||||
|
||||
# Generate hash using SHA1
|
||||
import hashlib
|
||||
hash_obj = hashlib.sha1()
|
||||
hash_obj.update(data.encode('utf-8'))
|
||||
return hash_obj.hexdigest()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate song hash: {str(e)}")
|
||||
raise
|
||||
|
||||
def __calcbfkey(songid):
|
||||
"""
|
||||
Calculate the Blowfish decrypt key for a given song ID.
|
||||
|
||||
Args:
|
||||
songid: String song ID
|
||||
|
||||
Returns:
|
||||
The Blowfish decryption key
|
||||
"""
|
||||
try:
|
||||
h = md5hex(songid)
|
||||
logger.debug(f"MD5 hash of song ID '{songid}': {h}")
|
||||
|
||||
# Build the key through XOR operations as per Deezer's algorithm
|
||||
bfkey = "".join(
|
||||
chr(
|
||||
ord(h[i]) ^ ord(h[i + 16]) ^ ord(__secret_key[i])
|
||||
)
|
||||
for i in range(16)
|
||||
)
|
||||
|
||||
# Log the generated key in hex format for debugging
|
||||
logger.debug(f"Generated Blowfish key: {bfkey.encode().hex()}")
|
||||
return bfkey
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating Blowfish key: {str(e)}")
|
||||
raise
|
||||
|
||||
def __blowfishDecrypt(data, key):
|
||||
"""
|
||||
Decrypt a single block of data using Blowfish in CBC mode.
|
||||
|
||||
Args:
|
||||
data: The encrypted data block (must be a multiple of 8 bytes)
|
||||
key: The Blowfish key as a string
|
||||
|
||||
Returns:
|
||||
The decrypted data
|
||||
"""
|
||||
try:
|
||||
# Ensure data is a multiple of Blowfish block size (8 bytes)
|
||||
if len(data) % 8 != 0:
|
||||
logger.warning(f"Data length {len(data)} is not a multiple of 8 bytes - Blowfish requires 8-byte blocks")
|
||||
# Pad data to a multiple of 8 if needed (though this should be avoided)
|
||||
padding = 8 - (len(data) % 8)
|
||||
data += b'\x00' * padding
|
||||
logger.warning(f"Padded data with {padding} null bytes")
|
||||
|
||||
# Create Blowfish cipher in CBC mode with initialization vector
|
||||
c = __newBlowfish(
|
||||
key.encode(), __MODE_CBC, __idk
|
||||
)
|
||||
|
||||
# Decrypt the data
|
||||
decrypted = c.decrypt(data)
|
||||
logger.debug(f"Decrypted {len(data)} bytes of data")
|
||||
|
||||
return decrypted
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Blowfish decryption: {str(e)}")
|
||||
raise
|
||||
|
||||
def decrypt_blowfish_track(crypted_audio, song_id, md5_origin, song_path):
|
||||
"""
|
||||
Decrypt the audio file using Blowfish encryption.
|
||||
|
||||
Args:
|
||||
crypted_audio: The encrypted audio data
|
||||
song_id: The song ID for generating the key
|
||||
md5_origin: The MD5 hash of the track
|
||||
song_path: Path where to save the decrypted file
|
||||
"""
|
||||
try:
|
||||
# Calculate the Blowfish key
|
||||
bf_key = __calcbfkey(song_id)
|
||||
|
||||
# For debugging - log the key being used
|
||||
logger.debug(f"Using Blowfish key for decryption: {bf_key.encode().hex()}")
|
||||
|
||||
# Prepare to process the file
|
||||
block_size = 2048 # Size of each block to process
|
||||
|
||||
# We need to reconstruct the data from potentially variable-sized chunks into
|
||||
# fixed-size blocks for proper decryption
|
||||
buffer = bytearray()
|
||||
block_count = 0 # Count of completed blocks
|
||||
|
||||
# Open the output file
|
||||
with open(song_path, 'wb') as output_file:
|
||||
# Process each incoming chunk of data
|
||||
for chunk in crypted_audio:
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
# Add current chunk to our buffer
|
||||
buffer.extend(chunk)
|
||||
|
||||
# Process as many complete blocks as we can
|
||||
while len(buffer) >= block_size:
|
||||
# Extract a block from buffer
|
||||
block = buffer[:block_size]
|
||||
buffer = buffer[block_size:]
|
||||
|
||||
# Only decrypt every third block
|
||||
is_encrypted = (block_count % 3 == 0)
|
||||
|
||||
if is_encrypted:
|
||||
# Ensure the block is a multiple of 8 bytes (Blowfish block size)
|
||||
if len(block) == block_size and len(block) % 8 == 0:
|
||||
try:
|
||||
# Create a fresh cipher with the initialization vector for each block
|
||||
# This is crucial - we need to reset the IV for each encrypted block
|
||||
cipher = __newBlowfish(bf_key.encode(), __MODE_CBC, __idk)
|
||||
|
||||
# Decrypt the block
|
||||
block = cipher.decrypt(block)
|
||||
logger.debug(f"Decrypted block {block_count} (size: {len(block)})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt block {block_count}: {str(e)}")
|
||||
# Continue with the encrypted block rather than failing completely
|
||||
|
||||
# Write the block (decrypted or not) to the output file
|
||||
output_file.write(block)
|
||||
block_count += 1
|
||||
|
||||
# Write any remaining data in the buffer (this won't be decrypted as it's a partial block)
|
||||
if buffer:
|
||||
logger.debug(f"Writing final partial block of size {len(buffer)}")
|
||||
output_file.write(buffer)
|
||||
|
||||
logger.debug(f"Successfully decrypted and saved Blowfish-encrypted file to {song_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt Blowfish file: {str(e)}")
|
||||
raise
|
||||
|
||||
def decryptfile(crypted_audio, ids, song_path):
|
||||
"""
|
||||
Decrypt the audio file using either AES or Blowfish encryption.
|
||||
|
||||
Args:
|
||||
crypted_audio: The encrypted audio data
|
||||
ids: The track IDs containing encryption info
|
||||
song_path: Path where to save the decrypted file
|
||||
"""
|
||||
try:
|
||||
# Check encryption type
|
||||
encryption_type = ids.get('encryption_type', 'aes')
|
||||
# Check if this is a FLAC file based on file extension
|
||||
is_flac = song_path.lower().endswith('.flac')
|
||||
|
||||
if encryption_type == 'aes':
|
||||
# Get the AES encryption key and nonce
|
||||
key = bytes.fromhex(ids['key'])
|
||||
nonce = bytes.fromhex(ids['nonce'])
|
||||
|
||||
# For AES-CTR, we can decrypt chunk by chunk
|
||||
counter = Counter.new(128, initial_value=int.from_bytes(nonce, byteorder='big'))
|
||||
cipher = AES.new(key, AES.MODE_CTR, counter=counter)
|
||||
|
||||
# Open the output file
|
||||
with open(song_path, 'wb') as f:
|
||||
# Process the data in chunks
|
||||
for chunk in crypted_audio:
|
||||
if chunk:
|
||||
# Decrypt the chunk and write to file
|
||||
decrypted_chunk = cipher.decrypt(chunk)
|
||||
f.write(decrypted_chunk)
|
||||
|
||||
logger.debug(f"Successfully decrypted and saved AES-encrypted file to {song_path}")
|
||||
|
||||
elif encryption_type == 'blowfish':
|
||||
# Customize Blowfish decryption based on file type
|
||||
if is_flac:
|
||||
logger.debug("Detected FLAC file - using special FLAC decryption handling")
|
||||
decrypt_blowfish_flac(
|
||||
crypted_audio,
|
||||
str(ids['track_id']),
|
||||
ids['md5_origin'],
|
||||
song_path
|
||||
)
|
||||
else:
|
||||
# Use standard Blowfish decryption for MP3
|
||||
decrypt_blowfish_track(
|
||||
crypted_audio,
|
||||
str(ids['track_id']),
|
||||
ids['md5_origin'],
|
||||
song_path
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown encryption type: {encryption_type}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt file: {str(e)}")
|
||||
raise
|
||||
|
||||
def decrypt_blowfish_flac(crypted_audio, song_id, md5_origin, song_path):
|
||||
"""
|
||||
Special decryption function for FLAC files using Blowfish encryption.
|
||||
This implementation follows Deezer's encryption scheme exactly.
|
||||
|
||||
In Deezer's encryption scheme:
|
||||
- Data is processed in 2048-byte blocks
|
||||
- Only every third block is encrypted (blocks 0, 3, 6, etc.)
|
||||
- Partial blocks at the end of the file are not encrypted
|
||||
- FLAC file structure must be preserved exactly
|
||||
- The initialization vector is reset for each encrypted block
|
||||
|
||||
Args:
|
||||
crypted_audio: Iterator of the encrypted audio data chunks
|
||||
song_id: The song ID for generating the key
|
||||
md5_origin: The MD5 hash of the track
|
||||
song_path: Path where to save the decrypted file
|
||||
"""
|
||||
try:
|
||||
# Calculate the Blowfish key
|
||||
bf_key = __calcbfkey(song_id)
|
||||
|
||||
# For debugging - log the key being used
|
||||
logger.debug(f"Using Blowfish key for decryption: {bf_key.encode().hex()}")
|
||||
|
||||
# Prepare to process the file
|
||||
block_size = 2048 # Size of each block to process
|
||||
|
||||
# We need to reconstruct the data from potentially variable-sized chunks into
|
||||
# fixed-size blocks for proper decryption
|
||||
buffer = bytearray()
|
||||
block_count = 0 # Count of completed blocks
|
||||
|
||||
# Open the output file
|
||||
with open(song_path, 'wb') as output_file:
|
||||
# Process each incoming chunk of data
|
||||
for chunk in crypted_audio:
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
# Add current chunk to our buffer
|
||||
buffer.extend(chunk)
|
||||
|
||||
# Process as many complete blocks as we can
|
||||
while len(buffer) >= block_size:
|
||||
# Extract a block from buffer
|
||||
block = buffer[:block_size]
|
||||
buffer = buffer[block_size:]
|
||||
|
||||
# Determine if this block should be decrypted (every third block)
|
||||
if block_count % 3 == 0:
|
||||
# Ensure we have a complete block for decryption and it's a multiple of 8 bytes
|
||||
if len(block) == block_size and len(block) % 8 == 0:
|
||||
try:
|
||||
# Create a fresh cipher with the initialization vector for each block
|
||||
# This is crucial - we need to reset the IV for each encrypted block
|
||||
cipher = __newBlowfish(bf_key.encode(), __MODE_CBC, __idk)
|
||||
|
||||
# Decrypt the block
|
||||
block = cipher.decrypt(block)
|
||||
logger.debug(f"Decrypted block {block_count} (size: {len(block)})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt block {block_count}: {str(e)}")
|
||||
# Continue with the encrypted block rather than failing completely
|
||||
|
||||
# Write the block (decrypted or not) to the output file
|
||||
output_file.write(block)
|
||||
block_count += 1
|
||||
|
||||
# Write any remaining data in the buffer (this won't be decrypted as it's a partial block)
|
||||
if buffer:
|
||||
logger.debug(f"Writing final partial block of size {len(buffer)}")
|
||||
output_file.write(buffer)
|
||||
|
||||
# Final validation
|
||||
if os.path.getsize(song_path) > 0:
|
||||
with open(song_path, 'rb') as f:
|
||||
if f.read(4) == b'fLaC':
|
||||
logger.info(f"FLAC file header verification passed")
|
||||
else:
|
||||
logger.warning("FLAC file doesn't begin with proper 'fLaC' signature")
|
||||
|
||||
logger.info(f"Successfully decrypted FLAC file to {song_path} ({os.path.getsize(song_path)} bytes)")
|
||||
|
||||
# Run the detailed analysis
|
||||
analysis = analyze_flac_file(song_path)
|
||||
if analysis.get("potential_issues"):
|
||||
logger.warning(f"Decryption completed but analysis found issues: {analysis['potential_issues']}")
|
||||
else:
|
||||
logger.info("FLAC analysis indicates the file structure is valid")
|
||||
|
||||
else:
|
||||
logger.error("Decrypted file is empty - decryption likely failed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt Blowfish FLAC file: {str(e)}")
|
||||
raise
|
||||
|
||||
def analyze_flac_file(file_path, limit=100):
|
||||
"""
|
||||
Analyze a FLAC file at the binary level for debugging purposes.
|
||||
This function helps identify issues with file structure that might cause
|
||||
playback problems.
|
||||
|
||||
Args:
|
||||
file_path: Path to the FLAC file
|
||||
limit: Maximum number of blocks to analyze
|
||||
|
||||
Returns:
|
||||
A dictionary with analysis results
|
||||
"""
|
||||
try:
|
||||
results = {
|
||||
"file_size": 0,
|
||||
"has_flac_signature": False,
|
||||
"block_structure": [],
|
||||
"metadata_blocks": 0,
|
||||
"potential_issues": []
|
||||
}
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
results["potential_issues"].append("File does not exist")
|
||||
return results
|
||||
|
||||
# Get file size
|
||||
file_size = os.path.getsize(file_path)
|
||||
results["file_size"] = file_size
|
||||
|
||||
if file_size < 8:
|
||||
results["potential_issues"].append("File too small to be a valid FLAC")
|
||||
return results
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
# Check FLAC signature (first 4 bytes should be 'fLaC')
|
||||
header = f.read(4)
|
||||
results["has_flac_signature"] = (header == b'fLaC')
|
||||
|
||||
if not results["has_flac_signature"]:
|
||||
results["potential_issues"].append(f"Missing FLAC signature. Found: {header}")
|
||||
|
||||
# Read and analyze metadata blocks
|
||||
# FLAC format: https://xiph.org/flac/format.html
|
||||
try:
|
||||
# Go back to position after signature
|
||||
f.seek(4)
|
||||
|
||||
# Read metadata blocks
|
||||
last_block = False
|
||||
block_count = 0
|
||||
|
||||
while not last_block and block_count < limit:
|
||||
block_header = f.read(4)
|
||||
if len(block_header) < 4:
|
||||
break
|
||||
|
||||
# First bit of first byte indicates if this is the last metadata block
|
||||
last_block = (block_header[0] & 0x80) != 0
|
||||
# Last 7 bits of first byte indicate block type
|
||||
block_type = block_header[0] & 0x7F
|
||||
# Next 3 bytes indicate length of block data
|
||||
block_length = (block_header[1] << 16) | (block_header[2] << 8) | block_header[3]
|
||||
|
||||
# Record block info
|
||||
block_info = {
|
||||
"position": f.tell() - 4,
|
||||
"type": block_type,
|
||||
"length": block_length,
|
||||
"is_last": last_block
|
||||
}
|
||||
|
||||
results["block_structure"].append(block_info)
|
||||
|
||||
# Skip to next block
|
||||
f.seek(block_length, os.SEEK_CUR)
|
||||
block_count += 1
|
||||
|
||||
results["metadata_blocks"] = block_count
|
||||
|
||||
# Check for common issues
|
||||
if block_count == 0:
|
||||
results["potential_issues"].append("No metadata blocks found")
|
||||
|
||||
# Check for STREAMINFO block (type 0) which should be present
|
||||
has_streaminfo = any(block["type"] == 0 for block in results["block_structure"])
|
||||
if not has_streaminfo:
|
||||
results["potential_issues"].append("Missing STREAMINFO block")
|
||||
|
||||
except Exception as e:
|
||||
results["potential_issues"].append(f"Error analyzing metadata: {str(e)}")
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing FLAC file: {str(e)}")
|
||||
return {"error": str(e)}
|
||||
Reference in New Issue
Block a user