Files
talk/internal/provider/edgedrm.go
T

303 lines
11 KiB
Go
Raw Normal View History

package provider
import (
"encoding/binary"
)
// Edge-DRM: Sec-MS-GEC v2. Microsoft verlangt seit der Protokoll-Änderung
// (edge-tts-Referenz, Stand 2026) einen Token der Form
// „<16 Hex CS64>-<64 Hex SHA256-Variante>" über den 5-Minuten-gerundeten
// Windows-Filetime-Ticks + Trusted-Client-Token. Beide Digests sind
// absichtlich verschleierte Varianten bekannter Algorithmen — die
// Konstanten und der Kontrollfluss unten sind eine wörtliche Portierung
// der Referenz (edge-tts src/edge_tts/drm.py, MIT) und gegen deren
// Ausgaben tabellengeprüft (TestEdgeDRMReferenzvektoren). Ändert Microsoft
// das Schema erneut, liefert der Edge-Provider klare Fehler („Protokoll
// geändert?"), statt still zu versagen.
const (
drmWinEpoch = 11644473600 // Sekunden zwischen 1601-01-01 und 1970-01-01
)
// drmIV ist der Standard-SHA-256-Initialvektor.
var drmIV = [8]uint32{
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
}
// drmMidState ist der Arbeitsregister-Stand der Referenz für den
// Mid-Block-Einstieg (Runde 14 des ersten Blocks).
var drmMidState = [8]uint32{
0xFD9FBEA2, 0x91243CE7, 0xBE499094, 0xCD2E1E0A,
0x6C006F49, 0x04BD35AB, 0xFEB0A7E4, 0x03EFFBD2,
}
// drmSchedule sind vorberechnete Schedule-Anteile des ersten Blocks.
var drmSchedule = [14]uint32{
0xEB24BF9E, 0x1558691F, 0xA0C09EE2, 0x0CFA32BA,
0xA622EE8C, 0xD1DD9491, 0xC0DD25A0, 0xF9E70AB6,
0xF0C31573, 0x7058CE4E, 0x7188E606, 0x00E5AB33,
0xD258B060, 0x72766564,
}
// drmAdjustment ersetzt die Rundungskonstante K[14] im Mid-Block-Pfad.
const (
drmAdjustment uint32 = 0xC9DC06A7
drmSigma0Adj uint32 = 0x059C0B80
drmSigma1Adj uint32 = 0x000B92C0
drmCS64Factor uint32 = 0x82893933
drmCS64Adj0 uint32 = 0xF2F90B1A
drmCS64Adj1 uint32 = 0x0B1A0000
)
// drmK sind die Standard-SHA-256-Rundungskonstanten.
var drmK = [64]uint32{
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
}
// drmCS64Coefficients: (Term-, Offset-Koeffizient) je Zeile.
var drmCS64Coefficients = [14][2]uint32{
{0xE82E91B1, 0xB32700D0}, {0x44DE24C6, 0x88DFF51D},
{0x3BEB65BC, 0x29514038}, {0xC89C5933, 0xB328CBDF},
{0x8D1EC211, 0xD405B4D5}, {0x314EF5D5, 0x26E55F3C},
{0xE92491B1, 0x607801C6}, {0xF0A5B9D5, 0x82B2C1FB},
{0x06064CF3, 0xFA51A3DA}, {0xDA79FA60, 0x207CC464},
{0xBED20560, 0x3C3C74F3}, {0x89F9C9F8, 0xD2D14B61},
{0x35FA38C6, 0xBB04A2C1}, {0xCB49E3C6, 0xC5626167},
}
// drmSigma0 ist die (verschleiert notierte) SHA-256-σ0-Mischfunktion.
func drmSigma0(v uint32) uint32 {
return v>>7 ^ v<<25 ^ v>>18 ^ v<<14 ^ v>>3
}
// drmSigma1 ist die SHA-256-σ1-Mischfunktion.
func drmSigma1(v uint32) uint32 {
return v>>17 ^ v<<15 ^ v>>19 ^ v<<13 ^ v>>10
}
// drmRol16 rotiert 32 Bit um 16.
func drmRol16(v uint32) uint32 { return v<<16 | v>>16 }
// drmRotR rotiert 32 Bit rechts um n.
func drmRotR(v uint32, n uint) uint32 { return v>>n | v<<(32-n) }
// drmExpand erweitert die Message-Schedule ab from auf 64 Wörter.
func drmExpand(words []uint32, from int) []uint32 {
for i := from; i < 64; i++ {
x, y := words[i-2], words[i-15]
words = append(words, drmSigma1(x)+words[i-7]+drmSigma0(y)+words[i-16])
}
return words
}
// drmRounds führt die SHA-256-Runden aus — wörtlich nach Referenz,
// inklusive des Mid-Block-Einstiegs (firstRound 14 mit ersetzter
// Rundungskonstante) und der 8er-Rollen-Rotation der Register.
func drmRounds(state [8]uint32, words []uint32, firstRound int) [8]uint32 {
a, b, c, d, e, f, g, h := state[0], state[1], state[2], state[3], state[4], state[5], state[6], state[7]
midBlock := firstRound == 14
k := func(i int) uint32 {
if midBlock && i == 14 {
return drmAdjustment
}
return drmK[i]
}
aligned := ((firstRound + 7) / 8) * 8
if aligned > 64 {
aligned = 64
}
for i := firstRound; i < aligned; i++ {
t1 := h + (drmRotR(e, 6) ^ drmRotR(e, 11) ^ drmRotR(e, 25)) + (g ^ (e & (f ^ g))) + k(i) + words[i]
t2 := (drmRotR(a, 2) ^ drmRotR(a, 13) ^ drmRotR(a, 22)) + ((a & b) | (c & (a | b)))
a, b, c, d, e, f, g, h = t1+t2, a, b, c, d+t1, e, f, g
}
for i := aligned; i < 64; i += 8 {
// Runde i
t1 := h + (drmRotR(e, 6) ^ drmRotR(e, 11) ^ drmRotR(e, 25)) + (g ^ (e & (f ^ g))) + k(i) + words[i]
d += t1
h = t1 + (drmRotR(a, 2) ^ drmRotR(a, 13) ^ drmRotR(a, 22)) + ((a & b) | (c & (a | b)))
// Runde i+1 (Rollen rotiert)
t1 = g + (drmRotR(d, 6) ^ drmRotR(d, 11) ^ drmRotR(d, 25)) + (f ^ (d & (e ^ f))) + k(i+1) + words[i+1]
c += t1
g = t1 + (drmRotR(h, 2) ^ drmRotR(h, 13) ^ drmRotR(h, 22)) + ((h & a) | (b & (h | a)))
// Runde i+2
t1 = f + (drmRotR(c, 6) ^ drmRotR(c, 11) ^ drmRotR(c, 25)) + (e ^ (c & (d ^ e))) + k(i+2) + words[i+2]
b += t1
f = t1 + (drmRotR(g, 2) ^ drmRotR(g, 13) ^ drmRotR(g, 22)) + ((g & h) | (a & (g | h)))
// Runde i+3
t1 = e + (drmRotR(b, 6) ^ drmRotR(b, 11) ^ drmRotR(b, 25)) + (d ^ (b & (c ^ d))) + k(i+3) + words[i+3]
a += t1
e = t1 + (drmRotR(f, 2) ^ drmRotR(f, 13) ^ drmRotR(f, 22)) + ((f & g) | (h & (f | g)))
// Runde i+4
t1 = d + (drmRotR(a, 6) ^ drmRotR(a, 11) ^ drmRotR(a, 25)) + (c ^ (a & (b ^ c))) + k(i+4) + words[i+4]
h += t1
d = t1 + (drmRotR(e, 2) ^ drmRotR(e, 13) ^ drmRotR(e, 22)) + ((e & f) | (g & (e | f)))
// Runde i+5
t1 = c + (drmRotR(h, 6) ^ drmRotR(h, 11) ^ drmRotR(h, 25)) + (b ^ (h & (a ^ b))) + k(i+5) + words[i+5]
g += t1
c = t1 + (drmRotR(d, 2) ^ drmRotR(d, 13) ^ drmRotR(d, 22)) + ((d & e) | (f & (d | e)))
// Runde i+6
t1 = b + (drmRotR(g, 6) ^ drmRotR(g, 11) ^ drmRotR(g, 25)) + (a ^ (g & (h ^ a))) + k(i+6) + words[i+6]
f += t1
b = t1 + (drmRotR(c, 2) ^ drmRotR(c, 13) ^ drmRotR(c, 22)) + ((c & d) | (e & (c | d)))
// Runde i+7
t1 = a + (drmRotR(f, 6) ^ drmRotR(f, 11) ^ drmRotR(f, 25)) + (h ^ (f & (g ^ h))) + k(i+7) + words[i+7]
e += t1
a = t1 + (drmRotR(b, 2) ^ drmRotR(b, 13) ^ drmRotR(b, 22)) + ((b & c) | (d & (b | c)))
}
return [8]uint32{a, b, c, d, e, f, g, h}
}
// drmSHA256Init berechnet den Zustand nach dem (verschleiert notierten)
// ersten Block: 14 Nullwörter + die ersten 7 Bytes von data als
// 24-Bit-/32-Bit-Big-Endian-Wörter, Schedule mit Korrekturen, Runden ab 14.
func drmSHA256Init(data []byte) [8]uint32 {
words := make([]uint32, 30, 64)
words[14] = uint32(data[0])<<16 | uint32(data[1])<<8 | uint32(data[2])
words[15] = binary.BigEndian.Uint32(data[3:7])
for i := 16; i < 30; i++ {
v := drmSchedule[i-16]
// σ1-Anteil von W[i-2] (Index 16 liest W[14], dazu die Korrektur).
s1 := drmSigma1(words[i-2])
if i == 16 {
s1 ^= drmSigma1Adj
}
v += s1
if i-7 >= 14 {
v += words[i-7]
}
if i-15 >= 14 { // nur i == 29, liest W[14], dazu die Korrektur
s0 := drmSigma0(words[i-15])
if i == 29 {
s0 ^= drmSigma0Adj
}
v += s0
}
words[i] = v
}
words = append(words, drmSigma1(words[28])+words[23]+drmSigma0(words[15])+words[14]+drmAdjustment-drmK[14])
words = drmExpand(words, 31)
working := drmRounds(drmMidState, words, 14)
var out [8]uint32
for i := range out {
out[i] = drmIV[i] + working[i]
}
return out
}
// drmSHA256Digest ist die verschleierte SHA-256-Variante der Referenz:
// Block 1 steckt in drmSHA256Init (erste 7 Bytes), danach data[7:] mit
// Referenz-Längenfeld (57+len(data)) und Standard-Runden.
func drmSHA256Digest(data []byte) [8]uint32 {
length := 57 + len(data)
padded := append([]byte(nil), data[7:]...)
padded = append(padded, 0x80)
zeros := (55 - length) % 64
if zeros < 0 {
zeros += 64
}
padded = append(padded, make([]byte, zeros)...)
var lenBuf [8]byte
binary.BigEndian.PutUint64(lenBuf[:], uint64(length)*8)
padded = append(padded, lenBuf[:]...)
state := drmSHA256Init(data[:7])
for off := 0; off < len(padded); off += 64 {
words := make([]uint32, 16, 64)
for i := range words {
words[i] = binary.BigEndian.Uint32(padded[off+i*4:])
}
words = drmExpand(words, 16)
working := drmRounds(state, words, 0)
for i := range state {
state[i] += working[i]
}
}
return state
}
// drmWordsLE4Even zerlegt data in little-endian u32-Wörter (auf 4 Bytes
// null-aufgefüllt, Wortzahl gerade).
func drmWordsLE4Even(data []byte) []uint32 {
padded := make([]byte, len(data))
copy(padded, data)
if r := len(padded) % 4; r != 0 {
padded = append(padded, make([]byte, 4-r)...)
}
words := make([]uint32, len(padded)/4)
for i := range words {
words[i] = binary.LittleEndian.Uint32(padded[i*4:])
}
if len(words)%2 == 1 {
words = append(words, 0)
}
return words
}
// drmCS64 berechnet die beiden CS64-Prüfsummen-Wörter (wörtliche
// Portierung; Schlüssel aus den ersten 8 Digest-Bytes little-endian).
func drmCS64(data []byte, digest [8]uint32) (a, b uint32) {
var digestBytes [32]byte
for i, w := range digest {
binary.BigEndian.PutUint32(digestBytes[i*4:], w)
}
keyA := binary.LittleEndian.Uint32(digestBytes[0:4]) | 1
keyB := binary.LittleEndian.Uint32(digestBytes[4:8]) | 1
factors := [2]uint32{keyA * drmCS64Factor, keyB * drmCS64Factor}
terms := make([]uint32, 0, 14+8)
offsets := make([]uint32, 0, 14+8)
for i := range drmCS64Coefficients {
terms = append(terms, drmCS64Coefficients[i][0]*factors[i%2])
offsets = append(offsets, drmCS64Coefficients[i][1])
}
for i, word := range drmWordsLE4Even(append([]byte{0}, data...)) {
var term, offset uint32
if i%2 == 0 {
term = word * keyA
offset = drmRol16(word) * 0xEF0569FB
} else {
term = word * keyB
offset = drmRol16(word) * 0xC31713DB
}
if i == 0 {
term += drmCS64Adj0 * factors[0]
offset += drmCS64Adj1
}
terms = append(terms, term)
offsets = append(offsets, offset)
}
var swapAcc, swapLast, st, acc uint32
for i := 0; i < len(terms); i += 2 {
x, y := terms[i], terms[i+1]
t := drmRol16(offsets[i] + x)
q := t * 0x76F50F34
swapAcc += drmRol16(q)*0xC3EFEA97 + q
t = drmRol16(offsets[i+1] + y)
q = t * 0x1881CE3C
swapLast = drmRol16(q)*0x35BD1EC9 + q
swapAcc += swapLast
st = st*keyA + x
st = drmRol16(st) * 0xCF98B111
st = drmRol16(st) * 0x87085B9F
st = drmRol16(st) * 0x12CEB96D
st = drmRol16(st) * 0x257E1D83
acc += st
t = st*keyB + y
t = drmRol16(t) * 0xA27416F5
t = drmRol16(t) * 0xD38396FF
st = drmRol16(drmRol16(t)*0x7C932B89) * 0xBFA49F69
acc += st
}
return swapLast ^ st, swapAcc ^ acc
}