Closed aynakeya closed 9 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)
如果加密没变过的话
如果加密没变过的话