Files

237 lines
8.7 KiB
Go

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<<shift | (c>>(28-shift))&0xfffffff0
d = d<<shift | (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},
}