#!/usr/bin/env python3 # QQMusic Mac MMKV Decryptor by LSR@Unlock Music import hashlib import re import sys from argparse import ArgumentParser from dataclasses import dataclass from os import PathLike from os.path import dirname from pathlib import Path from struct import pack, unpack @dataclass class MMKVDecryptionData: udid: str mmkv_path: Path mmkv_key: str data: bytes @property def mmkv_name(self) -> str: return self.mmkv_path.name def _aes_128_cfb_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes: """Decrypt using `Crypto.Cipher.AES` _or_ fallback to `OpenSSL` otherwise""" try: from Crypto.Cipher import AES # pyright: ignore[reportMissingImports] aes = AES.new(key[:16], AES.MODE_CFB, iv=iv, segment_size=128) return aes.decrypt(ciphertext) except ImportError: from subprocess import PIPE, Popen process = Popen( ["openssl", "enc", "-aes-128-cfb", "-d", "-K", key.hex(), "-iv", iv.hex()], stdin=PIPE, stdout=PIPE, stderr=PIPE, text=False, ) stdout, stderr = process.communicate(input=ciphertext) if process.returncode != 0: raise RuntimeError( f"OpenSSL error (install PyCryptodome instead): {stderr.decode()}" ) return stdout def _caesar(text: str, shift: int) -> str: """A simple Caesar cipher implementation for alphanumeric characters""" result = "" for char in text: if char.isalpha(): base = ord("A") if char.isupper() else ord("a") result += chr((ord(char) - base + shift) % 26 + base) elif char.isdigit(): result += chr((ord(char) - ord("0") + shift) % 10 + ord("0")) else: result += char return result __MMKV_TYPE_STREAM_KEY = 1 def _derive_mmkv_config(udid: str, mmkv_type: int): """Derive MMKV name and key from UDID, return (name, key)""" str1 = _caesar(udid, mmkv_type + 3) int1 = int(udid[5:7], 16) int2 = 5 + (int1 + mmkv_type) % 4 mmkv_name = str1[0:int2] int3 = mmkv_type + 0xA546 str3 = f"{udid}{int3:04x}" mmkv_key = hashlib.md5(str3.encode()).hexdigest() return mmkv_name, mmkv_key def _decrypt_mmkv(path: PathLike, key: bytes): """Decrypt MMKV file using the given key, return decrypted data""" with open(path, "rb") as mmkv, open(str(path) + ".crc", "rb") as crc: crc.seek(12) iv = crc.read(16) (real_size,) = unpack("