Diaoxiaozhang / Ximalaya-XM-Decrypt

喜马拉雅xm文件解密工具
309 stars 87 forks source link

decryption #8

Closed aynakeya closed 9 months ago

aynakeya commented 10 months ago
from mutagen.easyid3 import ID3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
import io,sys,pathlib
import re,base64,magic
import mutagen

class XMInfo:
    '''
    const {
        title: s,
        artist: l,
        subtitle: c,
        length: d,
        comment: {
            language: u,
            text: p
        },
        album: h,
        trackNumber: b,
        size: g,
        encodingTechnology: v,
        ISRC: _,
        fileType: y,
        encodedBy: w,
        publisher: k,
        composer: x,
        mediaType: S
    }
    '''
    def __init__(self):
        self.title = ""
        self.artist = ""
        self.album = ""
        self.tracknumber = 0
        self.size = 0
        self.header_size = 0
        self.ISRC = ""
        self.encodedby = ""
        self.encoding_technology = ""

    def iv(self):
        if (self.ISRC != ""):
            return bytes.fromhex(self.ISRC)
        return bytes.fromhex(self.encodedby)

def get_str(x):
    if x is None:
        return ""
    return x

def read_file(x):
    with open(x,"rb") as f:
        return f.read()

# return number of id3 bytes
def get_xm_info(data:bytes):
    # print(EasyID3(io.BytesIO(data)))
    id3 = ID3(io.BytesIO(data),v2_version=3)
    id3value = XMInfo()
    id3value.title = str(id3["TIT2"])
    id3value.album = str(id3["TALB"])
    id3value.artist = str(id3["TPE1"])
    id3value.tracknumber = int(str(id3["TRCK"]))
    id3value.ISRC = "" if id3.get("TSRC") is None else str(id3["TSRC"])
    id3value.encodedby = "" if id3.get("TENC") is None else str(id3["TENC"])
    id3value.size = int(str(id3["TSIZ"]))
    id3value.header_size = id3.size
    id3value.encoding_technology = str(id3["TSSE"])
    return id3value

def get_printable_count(x:bytes):
    i = 0
    for i,c in enumerate(x):
        # all pritable
        if c < 0x20 or c > 0x7e:
            return i
    return i

def get_printable_bytes(x:bytes):
    return x[:get_printable_count(x)]

def xm_decrypt(raw_data):
    # decode id3
    xm_info = get_xm_info(raw_data)
    print("id3 header size: ",hex(xm_info.header_size))
    encrypted_data = raw_data[xm_info.header_size:xm_info.header_size+xm_info.size:]

    # Stage 1 aes-256-cbc
    xm_key = b"ximalayaximalayaximalayaximalaya"
    print(f"decrypt stage 1 (aes-256-cbc):\n"
          f"    data length = {len(encrypted_data)},\n"
          f"    key = {xm_key},\n"
          f"    iv = {xm_info.iv().hex()}")
    cipher = AES.new(xm_key, AES.MODE_CBC, xm_info.iv())
    de_data = cipher.decrypt(pad(encrypted_data, 16))
    # Stage 2 xmDecrypt = (base64 decode => aes-192-cbc => base64 encode)
    print(f"decrypt stage 2 (xmDecrypt):\n"
          f"    data length = {len(de_data)},\n"
          f"    key = {str(xm_info.tracknumber)}")
    stage_2_data = base64.b64decode(get_printable_bytes(de_data))
    assert len(stage_2_data) % 16 == 0
    key = str(xm_info.tracknumber).encode()
    key = (b'12345678'*3)[:0x18-len(key)] + key
    cipher = AES.new(key, AES.MODE_CBC, key[:16])
    stage_2_data = unpad(cipher.decrypt(stage_2_data),16).decode() # idk but workround
    # Stage 3 combine
    print(f"Stage 3 (base64 combination):\n"
          f"    technology = {xm_info.encoding_technology}")
    decrypted_data = base64.b64decode(xm_info.encoding_technology+stage_2_data)
    final_data = decrypted_data + raw_data[xm_info.header_size+xm_info.size::]
    return xm_info,final_data

def xm_decrypt_v12():
    pass

def find_ext(data):
    exts = ["m4a","mp3","flac","wav"]
    value = magic.from_buffer(data).lower()
    for ext in exts:
        if ext in value:
            return ext
    raise Exception(f"unexpected format {value}")

def decrypt_xm_file(from_file,output=''):
    print(f"decrypting {from_file}")
    data = read_file(from_file)
    info, audio_data = xm_decrypt(data)
    if output == "":
        output = re.sub(r'[^\w\-_\. ]', '_', info.title)+"."+find_ext(audio_data[:0xff])
    buffer = io.BytesIO(audio_data)
    tags = mutagen.File(buffer,easy=True)
    tags["title"] = info.title
    tags["album"] = info.album
    tags["artist"] = info.artist
    print(tags.pprint())
    tags.save(buffer)
    with open(output,"wb") as f:
        buffer.seek(0)
        f.write(buffer.read())
    print(f"decrypt succeed, file write to {output}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python decrypt_xm.py [<filename> ...]")
    for filename in sys.argv[1::]:
        decrypt_xm_file(filename)

如果加密没变过的话