Files
talk/internal/provider/edgedrm.go
T
m3ta-chironandClaude Fable 5.1 85fa64cfe6 refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.

Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.

Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 11:15:35 +02:00

304 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}