Files

303 lines
11 KiB
Go
Raw Permalink Normal View History

package provider
import (
"encoding/binary"
)
// Edge DRM: Sec-MS-GEC v2. Since the protocol change (edge-tts reference,
// as of 2026) Microsoft requires a token of the form
// "<16 hex CS64>-<64 hex SHA256 variant>" over the 5-minute-rounded
// Windows filetime ticks + trusted client token. Both digests are
// deliberately obfuscated variants of well-known algorithms; the constants
// and the control flow below are a literal port of the reference (edge-tts
// src/edge_tts/drm.py, MIT) and are table-checked against its outputs
// (TestEdgeSecMSGEC). If Microsoft changes the scheme again, the Edge
// provider returns clear errors ("protocol changed?") instead of failing
// silently.
const (
drmWinEpoch = 11644473600 // seconds between 1601-01-01 and 1970-01-01
)
// drmIV is the standard SHA-256 initial vector.
var drmIV = [8]uint32{
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
}
// drmMidState is the reference's working-register state for the mid-block
// entry (round 14 of the first block).
var drmMidState = [8]uint32{
0xFD9FBEA2, 0x91243CE7, 0xBE499094, 0xCD2E1E0A,
0x6C006F49, 0x04BD35AB, 0xFEB0A7E4, 0x03EFFBD2,
}
// drmSchedule holds precomputed schedule parts of the first block.
var drmSchedule = [14]uint32{
0xEB24BF9E, 0x1558691F, 0xA0C09EE2, 0x0CFA32BA,
0xA622EE8C, 0xD1DD9491, 0xC0DD25A0, 0xF9E70AB6,
0xF0C31573, 0x7058CE4E, 0x7188E606, 0x00E5AB33,
0xD258B060, 0x72766564,
}
// drmAdjustment replaces the round constant K[14] on the mid-block path.
const (
drmAdjustment uint32 = 0xC9DC06A7
drmSigma0Adj uint32 = 0x059C0B80
drmSigma1Adj uint32 = 0x000B92C0
drmCS64Factor uint32 = 0x82893933
drmCS64Adj0 uint32 = 0xF2F90B1A
drmCS64Adj1 uint32 = 0x0B1A0000
)
// drmK are the standard SHA-256 round constants.
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 coefficient, offset coefficient) per row.
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 is the (obfuscated-notation) SHA-256 σ0 mixing function.
func drmSigma0(v uint32) uint32 {
return v>>7 ^ v<<25 ^ v>>18 ^ v<<14 ^ v>>3
}
// drmSigma1 is the SHA-256 σ1 mixing function.
func drmSigma1(v uint32) uint32 {
return v>>17 ^ v<<15 ^ v>>19 ^ v<<13 ^ v>>10
}
// drmRol16 rotates 32 bits by 16.
func drmRol16(v uint32) uint32 { return v<<16 | v>>16 }
// drmRotR rotates 32 bits right by n.
func drmRotR(v uint32, n uint) uint32 { return v>>n | v<<(32-n) }
// drmExpand extends the message schedule from index from to 64 words.
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 executes the SHA-256 rounds, literally following the
// reference, including the mid-block entry (firstRound 14 with the replaced
// round constant) and the 8-way role rotation of the registers.
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 {
// round 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)))
// round i+1 (roles rotated)
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)))
// round 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)))
// round 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)))
// round 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)))
// round 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)))
// round 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)))
// round 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 computes the state after the (obfuscated-notation) first
// block: 14 zero words + the first 7 bytes of data as 24-bit/32-bit
// big-endian words, schedule with corrections, rounds from 14 onwards.
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 part of W[i-2] (index 16 reads W[14], plus the correction).
s1 := drmSigma1(words[i-2])
if i == 16 {
s1 ^= drmSigma1Adj
}
v += s1
if i-7 >= 14 {
v += words[i-7]
}
if i-15 >= 14 { // only i == 29, reads W[14], plus the correction
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 is the reference's obfuscated SHA-256 variant: block 1
// lives in drmSHA256Init (first 7 bytes), followed by data[7:] with the
// reference length field (57+len(data)) and standard rounds.
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 splits data into little-endian u32 words (zero-padded to
// 4 bytes, even word count).
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 computes the two CS64 checksum words (literal port; keys from
// the first 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
}