448 lines
17 KiB
Python
448 lines
17 KiB
Python
#!/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)} |