237 lines
8.7 KiB
Go
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},
|
|
}
|