feat: hyper decryption device

with_status
WorldObservationLog 4 months ago
parent 356a5b2478
commit c84c3818e8
  1. 4
      config.example.toml
  2. 37
      src/adb.py
  3. 3
      src/cmd.py
  4. 2
      src/config.py
  5. 17
      src/decrypt.py
  6. 12
      src/rip.py

@ -18,6 +18,10 @@ agentPort = 10020
# For Magisk user, the recommend value is "su -c". For other Root solutions, the recommend value is "su 0" # For Magisk user, the recommend value is "su -c". For other Root solutions, the recommend value is "su 0"
# If not sure which method to use, execute “su 0 ls /” and “su -c ls /” respectively in adb shell and choose the output method # If not sure which method to use, execute “su 0 ls /” and “su -c ls /” respectively in adb shell and choose the output method
suMethod = "su -c" suMethod = "su -c"
# Inject multiple scripts into devices to simulate multi-device decryption, which can speed up decryption
# Experimental feature
hyperDecrypt = false
hyperDecryptNum = 2
[m3u8Api] [m3u8Api]
# Use zhaarey's m3u8 api to get higher song m3u8. # Use zhaarey's m3u8 api to get higher song m3u8.

@ -14,8 +14,24 @@ from src.exceptions import ADBConnectException, FailedGetAuthParamException, \
from src.types import AuthParams from src.types import AuthParams
class HyperDecryptDevice:
host: str
fridaPort: int
decryptLock: asyncio.Lock
serial: str
_father_device = None
def __init__(self, host: str, port: int, father_device):
self.host = host
self.fridaPort = port
self.decryptLock = asyncio.Lock()
self.serial = f"{host}:{port}"
self._father_device = father_device
class Device: class Device:
host: str host: str
serial: str
client: AdbClient client: AdbClient
device: AdbDevice device: AdbDevice
fridaPort: int fridaPort: int
@ -25,6 +41,7 @@ class Device:
authParams: AuthParams = None authParams: AuthParams = None
suMethod: str suMethod: str
decryptLock: asyncio.Lock decryptLock: asyncio.Lock
hyperDecryptDevices: list[HyperDecryptDevice] = []
def __init__(self, host="127.0.0.1", port=5037, su_method: str = "su -c"): def __init__(self, host="127.0.0.1", port=5037, su_method: str = "su -c"):
self.client = AdbClient(host, port) self.client = AdbClient(host, port)
@ -41,6 +58,7 @@ class Device:
if not status: if not status:
raise ADBConnectException raise ADBConnectException
self.device = self.client.device(f"{host}:{port}") self.device = self.client.device(f"{host}:{port}")
self.serial = self.device.serial
def _execute_command(self, cmd: str, su: bool = False, sh: bool = False) -> Optional[str]: def _execute_command(self, cmd: str, su: bool = False, sh: bool = False) -> Optional[str]:
whoami = self.device.shell("whoami") whoami = self.device.shell("whoami")
@ -143,3 +161,22 @@ class Device:
self.authParams = AuthParams(dsid=dsid, accountToken=token, self.authParams = AuthParams(dsid=dsid, accountToken=token,
accountAccessToken=access_token, storefront=storefront) accountAccessToken=access_token, storefront=storefront)
return self.authParams return self.authParams
def hyper_decrypt(self, ports: list[int]):
if not self._if_frida_running():
raise FridaNotRunningException
logger.debug("injecting agent script with hyper decrypt")
self.fridaPort = ports[0]
if not self.fridaDevice:
frida.get_device_manager().add_remote_device(self.device.serial)
self.fridaDevice = frida.get_device_manager().get_device(self.device.serial)
self.pid = self.fridaDevice.spawn("com.apple.android.music")
self.fridaSession = self.fridaDevice.attach(self.pid)
for port in ports:
self._start_forward(port, port)
with open("agent.js", "r") as f:
agent = f.read().replace("2147483647", str(port))
script: frida.core.Script = self.fridaSession.create_script(agent)
script.load()
self.hyperDecryptDevices.append(HyperDecryptDevice(host=self.host, port=port, father_device=self))
self.fridaDevice.resume(self.pid)

@ -67,6 +67,9 @@ class NewInteractiveShell:
if not self.storefront_device_mapping.get(auth_params.storefront.lower()): if not self.storefront_device_mapping.get(auth_params.storefront.lower()):
self.storefront_device_mapping.update({auth_params.storefront.lower(): []}) self.storefront_device_mapping.update({auth_params.storefront.lower(): []})
self.storefront_device_mapping[auth_params.storefront.lower()].append(device) self.storefront_device_mapping[auth_params.storefront.lower()].append(device)
if device_info.hyperDecrypt:
device.hyper_decrypt(list(range(device_info.agentPort, device_info.agentPort + device_info.hyperDecryptNum)))
else:
device.start_inject_frida(device_info.agentPort) device.start_inject_frida(device_info.agentPort)
async def command_parser(self, cmd: str): async def command_parser(self, cmd: str):

@ -13,6 +13,8 @@ class Device(BaseModel):
port: int port: int
agentPort: int agentPort: int
suMethod: str suMethod: str
hyperDecrypt: bool
hyperDecryptNum: int
class M3U8Api(BaseModel): class M3U8Api(BaseModel):

@ -2,26 +2,31 @@ import asyncio
import logging import logging
from loguru import logger from loguru import logger
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log, RetryCallState from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
from src.adb import Device from src.adb import Device, HyperDecryptDevice
from src.exceptions import DecryptException, RetryableDecryptException from src.exceptions import DecryptException, RetryableDecryptException
from src.models.song_data import Datum from src.models.song_data import Datum
from src.mp4 import SongInfo, SampleInfo from src.mp4 import SongInfo, SampleInfo
from src.types import defaultId, prefetchKey from src.types import defaultId, prefetchKey
from src.utils import timeit
retry_count = {} retry_count = {}
@retry(retry=retry_if_exception_type(RetryableDecryptException), stop=stop_after_attempt(3), @retry(retry=retry_if_exception_type(RetryableDecryptException), stop=stop_after_attempt(3),
before_sleep=before_sleep_log(logger, logging.WARN)) before_sleep=before_sleep_log(logger, logging.WARN))
async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Device) -> bytes: @timeit
async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Device | HyperDecryptDevice) -> bytes:
async with device.decryptLock: async with device.decryptLock:
logger.info(f"Decrypting song: {manifest.attributes.artistName} - {manifest.attributes.name}") if isinstance(device, HyperDecryptDevice):
logger.info(f"Using hyperDecryptDevice {device.serial} to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}")
else:
logger.info(f"Using device {device.serial} to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}")
try: try:
reader, writer = await asyncio.open_connection(device.host, device.fridaPort) reader, writer = await asyncio.open_connection(device.host, device.fridaPort)
except ConnectionRefusedError: except ConnectionRefusedError:
logger.warning(f"Failed to connect to device {device.device.serial}, re-injecting") logger.warning(f"Failed to connect to device {device.serial}, re-injecting")
device.restart_inject_frida() device.restart_inject_frida()
raise RetryableDecryptException raise RetryableDecryptException
decrypted = bytes() decrypted = bytes()
@ -42,7 +47,7 @@ async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Devi
try: try:
result = await decrypt_sample(writer, reader, sample) result = await decrypt_sample(writer, reader, sample)
except RetryableDecryptException as e: except RetryableDecryptException as e:
if 0 <= retry_count.get(device.device.serial, 0) < 3 or 4 <= retry_count.get(device.device.serial, 0) < 6: if 0 <= retry_count.get(device.serial, 0) < 3 or 4 <= retry_count.get(device.serial, 0) < 6:
logger.warning(f"Failed to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}, retrying") logger.warning(f"Failed to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}, retrying")
writer.write(bytes([0, 0, 0, 0])) writer.write(bytes([0, 0, 0, 0]))
writer.close() writer.close()

@ -1,4 +1,5 @@
import asyncio import asyncio
import random
import subprocess import subprocess
from loguru import logger from loguru import logger
@ -15,7 +16,7 @@ from src.mp4 import extract_media, extract_song, encapsulate, write_metadata, fi
from src.save import save from src.save import save
from src.types import GlobalAuthParams, Codec from src.types import GlobalAuthParams, Codec
from src.url import Song, Album, URLType, Artist, Playlist from src.url import Song, Album, URLType, Artist, Playlist
from src.utils import check_song_exists, if_raw_atmos, playlist_write_song_index, get_codec_from_codec_id from src.utils import check_song_exists, if_raw_atmos, playlist_write_song_index, get_codec_from_codec_id, timeit
task_lock = asyncio.Semaphore(16) task_lock = asyncio.Semaphore(16)
@ -80,6 +81,15 @@ async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config
codec = get_codec_from_codec_id(codec_id) codec = get_codec_from_codec_id(codec_id)
raw_song = await download_song(song_uri) raw_song = await download_song(song_uri)
song_info = await extract_song(raw_song, codec) song_info = await extract_song(raw_song, codec)
if device.hyperDecryptDevices:
if all([hyper_device.decryptLock.locked() for hyper_device in device.hyperDecryptDevices]):
decrypted_song = await decrypt(song_info, keys, song_data, random.choice(device.hyperDecryptDevices))
else:
for hyperDecryptDevice in device.hyperDecryptDevices:
if not hyperDecryptDevice.decryptLock.locked():
decrypted_song = await decrypt(song_info, keys, song_data, hyperDecryptDevice)
break
else:
decrypted_song = await decrypt(song_info, keys, song_data, device) decrypted_song = await decrypt(song_info, keys, song_data, device)
song = await encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a) song = await encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a)
if not if_raw_atmos(codec, config.download.atmosConventToM4a): if not if_raw_atmos(codec, config.download.atmosConventToM4a):

Loading…
Cancel
Save