package main import ( "bytes" "compress/zlib" "encoding/binary" "encoding/hex" "encoding/xml" "errors" "fmt" "io" "regexp" "strconv" "strings" ) // The QRC transform and keys below follow the community-provided // qq_lyric_tables.py implementation supplied with the original script. var ( qrcKey1 = [8]byte{'!', '@', '#', ')', '(', '*', '$', '%'} qrcKey2 = [8]byte{'1', '2', '3', 'Z', 'X', 'C', '!', '@'} qrcKey3 = [8]byte{'!', '@', '#', ')', '(', 'N', 'H', 'L'} ) const maxQRCBytes = 4 << 20 var ( qrcWordTimingPattern = regexp.MustCompile(`\(\d+,\d+\)`) qrcLinePattern = regexp.MustCompile(`^\[(\d+),(\d+)\](.*)$`) qrcMetadataPattern = regexp.MustCompile(`^\[(ti|ar|al|by|offset|kana|language|total):`) ) func decryptQRCLyric(encoded string) (string, error) { encrypted, err := hex.DecodeString(strings.TrimSpace(encoded)) if err != nil { return "", errors.New("QQ Music returned invalid phonetic lyrics") } if len(encrypted) == 0 || len(encrypted)%8 != 0 || len(encrypted) > maxUpstreamBytes { return "", errors.New("QQ Music returned invalid phonetic lyric length") } decrypted := append([]byte(nil), encrypted...) transformQRCBlocks(decrypted, newQRCDes(qrcKey3, true)) transformQRCBlocks(decrypted, newQRCDes(qrcKey2, false)) transformQRCBlocks(decrypted, newQRCDes(qrcKey1, true)) reader, err := zlib.NewReader(bytes.NewReader(decrypted)) if err != nil { return "", errors.New("QQ Music returned invalid compressed phonetic lyrics") } decompressed, readErr := io.ReadAll(io.LimitReader(reader, maxQRCBytes+1)) closeErr := reader.Close() if readErr != nil || closeErr != nil { return "", errors.New("could not decompress phonetic lyrics") } if len(decompressed) > maxQRCBytes { return "", errors.New("phonetic lyrics exceeded the safety limit") } return qrcXMLToLRC(decompressed) } func qrcXMLToLRC(document []byte) (string, error) { decoder := xml.NewDecoder(bytes.NewReader(document)) content := "" for { token, err := decoder.Token() if errors.Is(err, io.EOF) { break } if err != nil { return "", errors.New("QQ Music returned invalid phonetic lyric XML") } start, ok := token.(xml.StartElement) if !ok { continue } for _, attribute := range start.Attr { if attribute.Name.Local == "LyricContent" { content = attribute.Value break } } if content != "" { break } } if content == "" { return "", errors.New("phonetic lyrics did not contain lyric content") } lines := make([]string, 0, strings.Count(content, "\n")+1) for _, rawLine := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") { line := strings.TrimSpace(rawLine) if line == "" { continue } if qrcMetadataPattern.MatchString(line) { lines = append(lines, line) continue } match := qrcLinePattern.FindStringSubmatch(line) if match == nil { lines = append(lines, line) continue } start, err := strconv.Atoi(match[1]) if err != nil || start < 0 { continue } lyric := strings.TrimSpace(qrcWordTimingPattern.ReplaceAllString(match[3], "")) if lyric == "" { continue } lines = append(lines, fmt.Sprintf("[%02d:%02d.%03d]%s", start/60000, start%60000/1000, start%1000, lyric)) } if len(lines) == 0 { return "", errors.New("phonetic lyrics were empty") } return strings.Join(lines, "\n"), nil } type qrcDes struct { subkeys [16]uint64 } func newQRCDes(key [8]byte, decrypt bool) qrcDes { parameter := qrcMap64(binary.LittleEndian.Uint64(key[:]), qrcKeyPermutation[:]) c, d := uint32(parameter), uint32(parameter>>32) var result qrcDes for round, shift := range qrcRoundShifts { c = c<>(28-shift))&0xfffffff0 d = d<>(28-shift))&0xfffffff0 subkey := qrcMap64(uint64(d)<<32|uint64(c), qrcKeyCompression[:]) if decrypt { result.subkeys[15-round] = subkey } else { result.subkeys[round] = subkey } } return result } func (d qrcDes) transform(value uint64) uint64 { state := qrcMap64(value, qrcInitialPermutation[:]) for _, key := range d.subkeys { right := uint32(state >> 32) left := uint32(state) expanded := qrcMap64(uint64(right)<<32|uint64(right), qrcKeyExpansion[:]) ^ key sbox := uint32(0) for index, shift := range qrcSBoxShifts { sbox = sbox<<4 | uint32(qrcSBoxes[index][expanded>>shift&0x3f]) } next := qrcMap32(sbox, qrcPBox[:]) ^ left state = uint64(next)<<32 | uint64(right) } state = state>>32 | state<<32 return qrcMap64(state, qrcFinalPermutation[:]) } func transformQRCBlocks(data []byte, cipher qrcDes) { for offset := 0; offset < len(data); offset += 8 { binary.LittleEndian.PutUint64(data[offset:offset+8], cipher.transform(binary.LittleEndian.Uint64(data[offset:offset+8]))) } } func qrcBitMask(index uint8) uint64 { if index < 32 { return uint64(1) << (31 - index) } return uint64(1) << (63 - index + 32) } func qrcMapBit(result, source uint64, check, set uint8) uint64 { if source&qrcBitMask(check) != 0 { result |= qrcBitMask(set) } return result } func qrcMap64(source uint64, table []uint8) uint64 { half := len(table) / 2 var low, high uint64 for index := 0; index < half; index++ { low = qrcMapBit(low, source, table[index], uint8(index)) high = qrcMapBit(high, source, table[index+half], uint8(index)) } return uint64(uint32(high))<<32 | uint64(uint32(low)) } func qrcMap32(source uint32, table []uint8) uint32 { var result uint64 for index, check := range table { result = qrcMapBit(result, uint64(source), check, uint8(index)) } return uint32(result) } var qrcRoundShifts = [16]uint8{1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1} var qrcSBoxShifts = [8]uint8{26, 20, 14, 8, 58, 52, 46, 40} var qrcInitialPermutation = [64]uint8{ 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6, } var qrcFinalPermutation = [64]uint8{ 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24, } var qrcKeyPermutation = [56]uint8{ 56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3, } var qrcKeyCompression = [48]uint8{ 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 45, 56, 35, 41, 51, 59, 34, 44, 55, 49, 37, 52, 48, 53, 43, 60, 38, 57, 50, 46, 54, 40, 33, 36, } var qrcKeyExpansion = [48]uint8{ 31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 11, 12, 11, 12, 13, 14, 15, 16, 15, 16, 17, 18, 19, 20, 19, 20, 21, 22, 23, 24, 23, 24, 25, 26, 27, 28, 27, 28, 29, 30, 31, 0, } var qrcPBox = [32]uint8{15, 6, 19, 20, 28, 11, 27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7, 23, 13, 31, 26, 2, 8, 18, 12, 29, 5, 21, 10, 3, 24} var qrcSBoxes = [8][64]uint8{ {14, 0, 4, 15, 13, 7, 1, 4, 2, 14, 15, 2, 11, 13, 8, 1, 3, 10, 10, 6, 6, 12, 12, 11, 5, 9, 9, 5, 0, 3, 7, 8, 4, 15, 1, 12, 14, 8, 8, 2, 13, 4, 6, 9, 2, 1, 11, 7, 15, 5, 12, 11, 9, 3, 7, 14, 3, 10, 10, 0, 5, 6, 0, 13}, {15, 3, 1, 13, 8, 4, 14, 7, 6, 15, 11, 2, 3, 8, 4, 15, 9, 12, 7, 0, 2, 1, 13, 10, 12, 6, 0, 9, 5, 11, 10, 5, 0, 13, 14, 8, 7, 10, 11, 1, 10, 3, 4, 15, 13, 4, 1, 2, 5, 11, 8, 6, 12, 7, 6, 12, 9, 0, 3, 5, 2, 14, 15, 9}, {10, 13, 0, 7, 9, 0, 14, 9, 6, 3, 3, 4, 15, 6, 5, 10, 1, 2, 13, 8, 12, 5, 7, 14, 11, 12, 4, 11, 2, 15, 8, 1, 13, 1, 6, 10, 4, 13, 9, 0, 8, 6, 15, 9, 3, 8, 0, 7, 11, 4, 1, 15, 2, 14, 12, 3, 5, 11, 10, 5, 14, 2, 7, 12}, {7, 13, 13, 8, 14, 11, 3, 5, 0, 6, 6, 15, 9, 0, 10, 3, 1, 4, 2, 7, 8, 2, 5, 12, 11, 1, 12, 10, 4, 14, 15, 9, 10, 3, 6, 15, 9, 0, 0, 6, 12, 10, 11, 10, 7, 13, 13, 8, 15, 9, 1, 4, 3, 5, 14, 11, 5, 12, 2, 7, 8, 2, 4, 14}, {2, 14, 12, 11, 4, 2, 1, 12, 7, 4, 10, 7, 11, 13, 6, 1, 8, 5, 5, 0, 3, 15, 15, 10, 13, 3, 0, 9, 14, 8, 9, 6, 4, 11, 2, 8, 1, 12, 11, 7, 10, 1, 13, 14, 7, 2, 8, 13, 15, 6, 9, 15, 12, 0, 5, 9, 6, 10, 3, 4, 0, 5, 14, 3}, {12, 10, 1, 15, 10, 4, 15, 2, 9, 7, 2, 12, 6, 9, 8, 5, 0, 6, 13, 1, 3, 13, 4, 14, 14, 0, 7, 11, 5, 3, 11, 8, 9, 4, 14, 3, 15, 2, 5, 12, 2, 9, 8, 5, 12, 15, 3, 10, 7, 11, 0, 14, 4, 1, 10, 7, 1, 6, 13, 0, 11, 8, 6, 13}, {4, 13, 11, 0, 2, 11, 14, 7, 15, 4, 0, 9, 8, 1, 13, 10, 3, 14, 12, 3, 9, 5, 7, 12, 5, 2, 10, 15, 6, 8, 1, 6, 1, 6, 4, 11, 11, 13, 13, 8, 12, 1, 3, 4, 7, 10, 14, 7, 10, 9, 15, 5, 6, 0, 8, 15, 0, 14, 5, 2, 9, 3, 2, 12}, {13, 1, 2, 15, 8, 13, 4, 8, 6, 10, 15, 3, 11, 7, 1, 4, 10, 12, 9, 5, 3, 6, 14, 11, 5, 0, 0, 14, 12, 9, 7, 2, 7, 2, 11, 1, 4, 14, 1, 7, 9, 4, 12, 10, 14, 8, 2, 13, 0, 15, 6, 12, 10, 9, 13, 0, 15, 3, 3, 5, 5, 6, 8, 11}, }