├── README.md ├── .gitignore ├── go.mod ├── cdm ├── consts.go ├── pssh.go └── cdm.go ├── util └── http.go ├── go.sum ├── main.go └── proto └── wv_proto2.proto /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | device_client_id_blob* 2 | device_private_key* 3 | manifest.mpd* 4 | *.exe 5 | headers.json* 6 | test.go 7 | *.txt 8 | downey* -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dengskoloper/downey 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 7 | github.com/jessevdk/go-flags v1.5.0 8 | google.golang.org/protobuf v1.28.0 9 | lukechampine.com/frand v1.4.2 10 | ) 11 | -------------------------------------------------------------------------------- /cdm/consts.go: -------------------------------------------------------------------------------- 1 | package widevine 2 | 3 | import "io/ioutil" 4 | 5 | var DefaultPrivateKey string 6 | var DefaultClientID []byte 7 | 8 | func InitConstants() { 9 | DefaultPrivateKeyBuffer, err := ioutil.ReadFile("device_private_key") 10 | if err != nil { 11 | panic(err) 12 | } 13 | DefaultPrivateKey = string(DefaultPrivateKeyBuffer) 14 | 15 | DefaultClientID, err = ioutil.ReadFile("device_client_id_blob") 16 | if err != nil { 17 | panic(err) 18 | } 19 | } -------------------------------------------------------------------------------- /util/http.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/http" 5 | "fmt" 6 | "io/ioutil" 7 | "encoding/json" 8 | ) 9 | 10 | func ReadHeadersFromJSON(r *http.Header) { 11 | jsonBytesFromFile, err := ioutil.ReadFile("headers.json") 12 | if err != nil { 13 | panic(err) 14 | } 15 | var jsonHeaderMap map[string]string 16 | 17 | err = json.Unmarshal(jsonBytesFromFile, &jsonHeaderMap) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | for k, v := range jsonHeaderMap { 23 | fmt.Printf("Setting HTTP Header \"%s : %s\"\n", k, v) 24 | r.Set(k, v) 25 | } 26 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 2 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 3 | github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY= 4 | github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg= 5 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 6 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 7 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 8 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 9 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 10 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= 12 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 14 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 15 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 16 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 17 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 18 | lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= 19 | lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | import ( 3 | "github.com/dengskoloper/downey/cdm" 4 | "github.com/dengskoloper/downey/util" 5 | "net/http" 6 | "bytes" 7 | "encoding/hex" 8 | "encoding/base64" 9 | "fmt" 10 | "io" 11 | "time" 12 | "os" 13 | "github.com/jessevdk/go-flags" 14 | ) 15 | 16 | var opts struct { 17 | LicenseServerURL string `long:"lic-server" description:"License Server URL"` 18 | AddHeaders bool `long:"add-headers" description:"Read HTTP headers from headers.json"` 19 | InitPSSH string `long:"pssh" description:"Override PSSH data"` 20 | } 21 | 22 | func init() { 23 | widevine.InitConstants() 24 | } 25 | 26 | func main() { 27 | _, err := flags.Parse(&opts) 28 | if err != nil { 29 | panic(err) 30 | } 31 | var initData []byte 32 | if len(opts.InitPSSH) > 0 { 33 | initData, err = base64.StdEncoding.DecodeString(opts.InitPSSH) 34 | if err != nil { 35 | panic(err) 36 | } 37 | } else { 38 | file, err := os.Open("manifest.mpd") 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | initData, err = widevine.InitDataFromMPD(file) 44 | if err != nil { 45 | panic(err) 46 | } 47 | } 48 | cdm, err := widevine.NewDefaultCDM(initData) 49 | if err != nil { 50 | panic(err) 51 | } 52 | licenseRequest, err := cdm.GetLicenseRequest() 53 | if err != nil { 54 | panic(err) 55 | } 56 | client := &http.Client{ 57 | Timeout: 10 * time.Second, 58 | } 59 | request, err := http.NewRequest(http.MethodPost, opts.LicenseServerURL, bytes.NewReader(licenseRequest)) 60 | 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | if opts.AddHeaders { 66 | util.ReadHeadersFromJSON(&request.Header) 67 | } 68 | 69 | request.Close = true 70 | response, err := client.Do(request) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | defer response.Body.Close() 76 | licenseResponse, err := io.ReadAll(response.Body) 77 | if err != nil { 78 | panic(err) 79 | } 80 | keys, err := cdm.GetLicenseKeys(licenseRequest, licenseResponse) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | command := "" 86 | for _, key := range keys { 87 | if key.Type == widevine.License_KeyContainer_CONTENT { 88 | command += "\n" + hex.EncodeToString(key.ID) + ":" + hex.EncodeToString(key.Value) 89 | } 90 | } 91 | fmt.Println("\nDecryption keys: ", command) 92 | } -------------------------------------------------------------------------------- /cdm/pssh.go: -------------------------------------------------------------------------------- 1 | package widevine 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/xml" 7 | "errors" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | // This function retrieves the PSSH/Init Data from a given MPD file reader. 13 | // Example file: https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd 14 | func InitDataFromMPD(r io.Reader) ([]byte, error) { 15 | type mpd struct { 16 | XMLName xml.Name `xml:"MPD"` 17 | Text string `xml:",chardata"` 18 | ID string `xml:"id,attr"` 19 | Profiles string `xml:"profiles,attr"` 20 | Type string `xml:"type,attr"` 21 | AvailabilityStartTime string `xml:"availabilityStartTime,attr"` 22 | PublishTime string `xml:"publishTime,attr"` 23 | MediaPresentationDuration string `xml:"mediaPresentationDuration,attr"` 24 | MinBufferTime string `xml:"minBufferTime,attr"` 25 | Version string `xml:"version,attr"` 26 | Ns2 string `xml:"ns2,attr"` 27 | Xmlns string `xml:"xmlns,attr"` 28 | Bitmovin string `xml:"bitmovin,attr"` 29 | Period struct { 30 | Text string `xml:",chardata"` 31 | AdaptationSet []struct { 32 | Text string `xml:",chardata"` 33 | MimeType string `xml:"mimeType,attr"` 34 | Codecs string `xml:"codecs,attr"` 35 | Lang string `xml:"lang,attr"` 36 | Label string `xml:"label,attr"` 37 | SegmentTemplate struct { 38 | Text string `xml:",chardata"` 39 | Media string `xml:"media,attr"` 40 | Initialization string `xml:"initialization,attr"` 41 | Duration string `xml:"duration,attr"` 42 | StartNumber string `xml:"startNumber,attr"` 43 | Timescale string `xml:"timescale,attr"` 44 | } `xml:"SegmentTemplate"` 45 | ContentProtection []struct { 46 | Text string `xml:",chardata"` 47 | SchemeIdUri string `xml:"schemeIdUri,attr"` 48 | Value string `xml:"value,attr"` 49 | DefaultKID string `xml:"default_KID,attr"` 50 | Pssh string `xml:"pssh"` 51 | } `xml:"ContentProtection"` 52 | Representation []struct { 53 | Text string `xml:",chardata"` 54 | ID string `xml:"id,attr"` 55 | Bandwidth string `xml:"bandwidth,attr"` 56 | Width string `xml:"width,attr"` 57 | Height string `xml:"height,attr"` 58 | FrameRate string `xml:"frameRate,attr"` 59 | AudioSamplingRate string `xml:"audioSamplingRate,attr"` 60 | ContentProtection []struct { 61 | Text string `xml:",chardata"` 62 | SchemeIdUri string `xml:"schemeIdUri,attr"` 63 | Value string `xml:"value,attr"` 64 | DefaultKID string `xml:"default_KID,attr"` 65 | Cenc string `xml:"cenc,attr"` 66 | Pssh struct { 67 | Text string `xml:",chardata"` 68 | Cenc string `xml:"cenc,attr"` 69 | } `xml:"pssh"` 70 | } `xml:"ContentProtection"` 71 | } `xml:"Representation"` 72 | AudioChannelConfiguration struct { 73 | Text string `xml:",chardata"` 74 | SchemeIdUri string `xml:"schemeIdUri,attr"` 75 | Value string `xml:"value,attr"` 76 | } `xml:"AudioChannelConfiguration"` 77 | } `xml:"AdaptationSet"` 78 | } `xml:"Period"` 79 | } 80 | 81 | var mpdPlaylist mpd 82 | if err := xml.NewDecoder(r).Decode(&mpdPlaylist); err != nil { 83 | return nil, err 84 | } 85 | 86 | const widevineSchemeIdURI = "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" 87 | for _, adaptionSet := range mpdPlaylist.Period.AdaptationSet { 88 | for _, protection := range adaptionSet.ContentProtection { 89 | if protection.SchemeIdUri == widevineSchemeIdURI && len(protection.Pssh) > 0 { 90 | return base64.StdEncoding.DecodeString(protection.Pssh) 91 | } 92 | } 93 | } 94 | for _, adaptionSet := range mpdPlaylist.Period.AdaptationSet { 95 | for _, representation := range adaptionSet.Representation { 96 | for _, protection := range representation.ContentProtection { 97 | if protection.SchemeIdUri == widevineSchemeIdURI && len(protection.Pssh.Text) > 0 { 98 | return base64.StdEncoding.DecodeString(protection.Pssh.Text) 99 | } 100 | } 101 | } 102 | } 103 | 104 | return nil, errors.New("no init data found") 105 | } 106 | 107 | // This function retrieves certificate data from a given license server. 108 | func GetCertData(client *http.Client, licenseURL string) ([]byte, error) { 109 | response, err := client.Post(licenseURL, "application/x-www-form-urlencoded", bytes.NewReader([]byte{0x08, 0x04})) 110 | if err != nil { 111 | return nil, err 112 | } 113 | defer response.Body.Close() 114 | return io.ReadAll(response.Body) 115 | } 116 | -------------------------------------------------------------------------------- /cdm/cdm.go: -------------------------------------------------------------------------------- 1 | package widevine 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/aes" 7 | "crypto/cipher" 8 | "crypto/rsa" 9 | "crypto/sha1" 10 | "crypto/x509" 11 | "encoding/pem" 12 | "errors" 13 | "github.com/aead/cmac" 14 | "google.golang.org/protobuf/proto" 15 | "lukechampine.com/frand" 16 | "math" 17 | "time" 18 | ) 19 | 20 | type CDM struct { 21 | privateKey *rsa.PrivateKey 22 | clientID []byte 23 | sessionID [32]byte 24 | 25 | widevineCencHeader WidevineCencHeader 26 | signedDeviceCertificate SignedDeviceCertificate 27 | privacyMode bool 28 | } 29 | 30 | type Key struct { 31 | ID []byte 32 | Type License_KeyContainer_KeyType 33 | Value []byte 34 | } 35 | 36 | // Creates a new CDM object with the specified device information. 37 | func NewCDM(privateKey string, clientID []byte, initData []byte) (CDM, error) { 38 | block, _ := pem.Decode([]byte(privateKey)) 39 | if block == nil || block.Type != "RSA PRIVATE KEY" { 40 | return CDM{}, errors.New("failed to decode device private key") 41 | } 42 | keyParsed, err := x509.ParsePKCS1PrivateKey(block.Bytes) 43 | if err != nil { 44 | return CDM{}, err 45 | } 46 | 47 | var widevineCencHeader WidevineCencHeader 48 | if len(initData) < 32 { 49 | return CDM{}, errors.New("initData not long enough") 50 | } 51 | if err := proto.Unmarshal(initData[32:], &widevineCencHeader); err != nil { 52 | return CDM{}, err 53 | } 54 | 55 | sessionID := func() (s [32]byte) { 56 | c := []byte("ABCDEF0123456789") 57 | for i := 0; i < 16; i++ { 58 | s[i] = c[frand.Intn(len(c))] 59 | } 60 | s[16] = '0' 61 | s[17] = '1' 62 | for i := 18; i < 32; i++ { 63 | s[i] = '0' 64 | } 65 | return s 66 | }() 67 | 68 | return CDM{ 69 | privateKey: keyParsed, 70 | clientID: clientID, 71 | 72 | widevineCencHeader: widevineCencHeader, 73 | 74 | sessionID: sessionID, 75 | }, nil 76 | } 77 | 78 | // Creates a new CDM object using the default device configuration. 79 | func NewDefaultCDM(initData []byte) (CDM, error) { 80 | return NewCDM(DefaultPrivateKey, DefaultClientID, initData) 81 | } 82 | 83 | // Sets a device certificate. This is makes generating the license request 84 | // more complicated but is supported. This is usually not necessary for most 85 | // Widevine applications. 86 | func (c *CDM) SetServiceCertificate(certData []byte) error { 87 | var message SignedMessage 88 | if err := proto.Unmarshal(certData, &message); err != nil { 89 | return err 90 | } 91 | if err := proto.Unmarshal(message.Msg, &c.signedDeviceCertificate); err != nil { 92 | return err 93 | } 94 | c.privacyMode = true 95 | return nil 96 | } 97 | 98 | func (c *CDM) GetServiceCertificate() *SignedDeviceCertificate { 99 | 100 | return &c.signedDeviceCertificate 101 | } 102 | 103 | // Generates the license request data. This is sent to the license server via 104 | // HTTP POST and the server in turn returns the license response. 105 | func (c *CDM) GetLicenseRequest() ([]byte, error) { 106 | var licenseRequest SignedLicenseRequest 107 | licenseRequest.Msg = new(LicenseRequest) 108 | licenseRequest.Msg.ContentId = new(LicenseRequest_ContentIdentification) 109 | licenseRequest.Msg.ContentId.CencId = new(LicenseRequest_ContentIdentification_CENC) 110 | 111 | // this is probably really bad for the GC but protobuf uses pointers for optional 112 | // fields so it is necessary and this is not a long running program 113 | { 114 | v := SignedLicenseRequest_LICENSE_REQUEST 115 | licenseRequest.Type = &v 116 | } 117 | 118 | licenseRequest.Msg.ContentId.CencId.Pssh = &c.widevineCencHeader 119 | 120 | { 121 | v := LicenseType_DEFAULT 122 | licenseRequest.Msg.ContentId.CencId.LicenseType = &v 123 | } 124 | 125 | licenseRequest.Msg.ContentId.CencId.RequestId = c.sessionID[:] 126 | 127 | { 128 | v := LicenseRequest_NEW 129 | licenseRequest.Msg.Type = &v 130 | } 131 | 132 | { 133 | v := uint32(time.Now().Unix()) 134 | licenseRequest.Msg.RequestTime = &v 135 | } 136 | 137 | { 138 | v := ProtocolVersion_CURRENT 139 | licenseRequest.Msg.ProtocolVersion = &v 140 | } 141 | 142 | { 143 | v := uint32(frand.Uint64n(math.MaxUint32)) 144 | licenseRequest.Msg.KeyControlNonce = &v 145 | } 146 | 147 | if c.privacyMode { 148 | pad := func(data []byte, blockSize int) []byte { 149 | padlen := blockSize - (len(data) % blockSize) 150 | if padlen == 0 { 151 | padlen = blockSize 152 | } 153 | return append(data, bytes.Repeat([]byte{byte(padlen)}, padlen)...) 154 | } 155 | const blockSize = 16 156 | 157 | var cidKey, cidIV [blockSize]byte 158 | frand.Read(cidKey[:]) 159 | frand.Read(cidIV[:]) 160 | 161 | block, err := aes.NewCipher(cidKey[:]) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | paddedClientID := pad(c.clientID, blockSize) 167 | encryptedClientID := make([]byte, len(paddedClientID)) 168 | cipher.NewCBCEncrypter(block, cidIV[:]).CryptBlocks(encryptedClientID, paddedClientID) 169 | 170 | servicePublicKey, err := x509.ParsePKCS1PublicKey(c.signedDeviceCertificate.XDeviceCertificate.PublicKey) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | encryptedCIDKey, err := rsa.EncryptOAEP(sha1.New(), frand.Reader, servicePublicKey, cidKey[:], nil) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | licenseRequest.Msg.EncryptedClientId = new(EncryptedClientIdentification) 181 | { 182 | v := string(c.signedDeviceCertificate.XDeviceCertificate.ServiceId) 183 | licenseRequest.Msg.EncryptedClientId.ServiceId = &v 184 | } 185 | licenseRequest.Msg.EncryptedClientId.ServiceCertificateSerialNumber = c.signedDeviceCertificate.XDeviceCertificate.SerialNumber 186 | licenseRequest.Msg.EncryptedClientId.EncryptedClientId = encryptedClientID 187 | licenseRequest.Msg.EncryptedClientId.EncryptedClientIdIv = cidIV[:] 188 | licenseRequest.Msg.EncryptedClientId.EncryptedPrivacyKey = encryptedCIDKey 189 | } else { 190 | licenseRequest.Msg.ClientId = new(ClientIdentification) 191 | if err := proto.Unmarshal(c.clientID, licenseRequest.Msg.ClientId); err != nil { 192 | return nil, err 193 | } 194 | } 195 | 196 | { 197 | data, err := proto.Marshal(licenseRequest.Msg) 198 | if err != nil { 199 | return nil, err 200 | } 201 | hash := sha1.Sum(data) 202 | if licenseRequest.Signature, err = rsa.SignPSS(frand.Reader, c.privateKey, crypto.SHA1, hash[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}); err != nil { 203 | return nil, err 204 | } 205 | } 206 | 207 | return proto.Marshal(&licenseRequest) 208 | } 209 | 210 | // Retrieves the keys from the license response data. These keys can be 211 | // used to decrypt the DASH-MP4. 212 | func (c *CDM) GetLicenseKeys(licenseRequest []byte, licenseResponse []byte) (keys []Key, err error) { 213 | var license SignedLicense 214 | if err = proto.Unmarshal(licenseResponse, &license); err != nil { 215 | return 216 | } 217 | 218 | var licenseRequestParsed SignedLicenseRequest 219 | if err = proto.Unmarshal(licenseRequest, &licenseRequestParsed); err != nil { 220 | return 221 | } 222 | licenseRequestMsg, err := proto.Marshal(licenseRequestParsed.Msg) 223 | if err != nil { 224 | return 225 | } 226 | 227 | sessionKey, err := rsa.DecryptOAEP(sha1.New(), frand.Reader, c.privateKey, license.SessionKey, nil) 228 | if err != nil { 229 | return 230 | } 231 | 232 | sessionKeyBlock, err := aes.NewCipher(sessionKey) 233 | if err != nil { 234 | return 235 | } 236 | 237 | encryptionKey := []byte{1, 'E', 'N', 'C', 'R', 'Y', 'P', 'T', 'I', 'O', 'N', 0} 238 | encryptionKey = append(encryptionKey, licenseRequestMsg...) 239 | encryptionKey = append(encryptionKey, []byte{0, 0, 0, 0x80}...) 240 | encryptionKeyCmac, err := cmac.Sum(encryptionKey, sessionKeyBlock, sessionKeyBlock.BlockSize()) 241 | if err != nil { 242 | return 243 | } 244 | encryptionKeyCipher, err := aes.NewCipher(encryptionKeyCmac) 245 | if err != nil { 246 | return 247 | } 248 | 249 | unpad := func(b []byte) []byte { 250 | if len(b) == 0 { 251 | return b 252 | } 253 | // pks padding is designed so that the value of all the padding bytes is 254 | // the number of padding bytes repeated so to figure out how many 255 | // padding bytes there are we can just look at the value of the last 256 | // byte 257 | // i.e if there are 6 padding bytes then it will look at like 258 | // 0x6 0x6 0x6 0x6 0x6 0x6 259 | count := int(b[len(b)-1]) 260 | return b[0 : len(b)-count] 261 | } 262 | for _, key := range license.Msg.Key { 263 | decrypter := cipher.NewCBCDecrypter(encryptionKeyCipher, key.Iv) 264 | decryptedKey := make([]byte, len(key.Key)) 265 | decrypter.CryptBlocks(decryptedKey, key.Key) 266 | keys = append(keys, Key{ 267 | ID: key.Id, 268 | Type: *key.Type, 269 | Value: unpad(decryptedKey), 270 | }) 271 | } 272 | 273 | return 274 | } 275 | -------------------------------------------------------------------------------- /proto/wv_proto2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | option go_package = ".;widevine"; 4 | 5 | // from x86 (partial), most of it from the ARM version: 6 | message ClientIdentification { 7 | enum TokenType { 8 | KEYBOX = 0; 9 | DEVICE_CERTIFICATE = 1; 10 | REMOTE_ATTESTATION_CERTIFICATE = 2; 11 | } 12 | message NameValue { 13 | required string Name = 1; 14 | required string Value = 2; 15 | } 16 | message ClientCapabilities { 17 | enum HdcpVersion { 18 | HDCP_NONE = 0; 19 | HDCP_V1 = 1; 20 | HDCP_V2 = 2; 21 | HDCP_V2_1 = 3; 22 | HDCP_V2_2 = 4; 23 | } 24 | optional uint32 ClientToken = 1; 25 | optional uint32 SessionToken = 2; 26 | optional uint32 VideoResolutionConstraints = 3; 27 | optional HdcpVersion MaxHdcpVersion = 4; 28 | optional uint32 OemCryptoApiVersion = 5; 29 | } 30 | required TokenType Type = 1; 31 | //optional bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one: 32 | optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob 33 | repeated NameValue ClientInfo = 3; 34 | optional bytes ProviderClientToken = 4; 35 | optional uint32 LicenseCounter = 5; 36 | optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later 37 | optional FileHashes _FileHashes = 7; // vmp blob goes here 38 | } 39 | 40 | message DeviceCertificate { 41 | enum CertificateType { 42 | ROOT = 0; 43 | INTERMEDIATE = 1; 44 | USER_DEVICE = 2; 45 | SERVICE = 3; 46 | } 47 | required CertificateType Type = 1; // the compiled code reused this as ProvisionedDeviceInfo.WvSecurityLevel, however that is incorrect (compiler aliased it as they're both identical as a structure) 48 | optional bytes SerialNumber = 2; 49 | optional uint32 CreationTimeSeconds = 3; 50 | optional bytes PublicKey = 4; 51 | optional uint32 SystemId = 5; 52 | optional uint32 TestDeviceDeprecated = 6; // is it bool or int? 53 | optional bytes ServiceId = 7; // service URL for service certificates 54 | } 55 | 56 | // missing some references, 57 | message DeviceCertificateStatus { 58 | enum CertificateStatus { 59 | VALID = 0; 60 | REVOKED = 1; 61 | } 62 | optional bytes SerialNumber = 1; 63 | optional CertificateStatus Status = 2; 64 | optional ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated? 65 | } 66 | 67 | message DeviceCertificateStatusList { 68 | optional uint32 CreationTimeSeconds = 1; 69 | repeated DeviceCertificateStatus CertificateStatus = 2; 70 | } 71 | 72 | message EncryptedClientIdentification { 73 | required string ServiceId = 1; 74 | optional bytes ServiceCertificateSerialNumber = 2; 75 | required bytes EncryptedClientId = 3; 76 | required bytes EncryptedClientIdIv = 4; 77 | required bytes EncryptedPrivacyKey = 5; 78 | } 79 | 80 | // todo: fill (for this top-level type, it might be impossible/difficult) 81 | enum LicenseType { 82 | ZERO = 0; 83 | DEFAULT = 1; // 1 is STREAMING/temporary license; on recent versions may go up to 3 (latest x86); it might be persist/don't persist type, unconfirmed 84 | OFFLINE = 2; 85 | } 86 | 87 | // todo: fill (for this top-level type, it might be impossible/difficult) 88 | // this is just a guess because these globals got lost, but really, do we need more? 89 | enum ProtocolVersion { 90 | CURRENT = 21; // don't have symbols for this 91 | } 92 | 93 | 94 | message LicenseIdentification { 95 | optional bytes RequestId = 1; 96 | optional bytes SessionId = 2; 97 | optional bytes PurchaseId = 3; 98 | optional LicenseType Type = 4; 99 | optional uint32 Version = 5; 100 | optional bytes ProviderSessionToken = 6; 101 | } 102 | 103 | 104 | message License { 105 | message Policy { 106 | optional bool CanPlay = 1; // changed from uint32 to bool 107 | optional bool CanPersist = 2; 108 | optional bool CanRenew = 3; 109 | optional uint32 RentalDurationSeconds = 4; 110 | optional uint32 PlaybackDurationSeconds = 5; 111 | optional uint32 LicenseDurationSeconds = 6; 112 | optional uint32 RenewalRecoveryDurationSeconds = 7; 113 | optional string RenewalServerUrl = 8; 114 | optional uint32 RenewalDelaySeconds = 9; 115 | optional uint32 RenewalRetryIntervalSeconds = 10; 116 | optional bool RenewWithUsage = 11; // was uint32 117 | } 118 | message KeyContainer { 119 | enum KeyType { 120 | SIGNING = 1; 121 | CONTENT = 2; 122 | KEY_CONTROL = 3; 123 | OPERATOR_SESSION = 4; 124 | } 125 | enum SecurityLevel { 126 | SW_SECURE_CRYPTO = 1; 127 | SW_SECURE_DECODE = 2; 128 | HW_SECURE_CRYPTO = 3; 129 | HW_SECURE_DECODE = 4; 130 | HW_SECURE_ALL = 5; 131 | } 132 | message OutputProtection { 133 | enum CGMS { 134 | COPY_FREE = 0; 135 | COPY_ONCE = 2; 136 | COPY_NEVER = 3; 137 | CGMS_NONE = 0x2A; // PC default! 138 | } 139 | optional ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away 140 | optional CGMS CgmsFlags = 2; 141 | } 142 | message KeyControl { 143 | required bytes KeyControlBlock = 1; // what is this? 144 | required bytes Iv = 2; 145 | } 146 | message OperatorSessionKeyPermissions { 147 | optional uint32 AllowEncrypt = 1; 148 | optional uint32 AllowDecrypt = 2; 149 | optional uint32 AllowSign = 3; 150 | optional uint32 AllowSignatureVerify = 4; 151 | } 152 | message VideoResolutionConstraint { 153 | optional uint32 MinResolutionPixels = 1; 154 | optional uint32 MaxResolutionPixels = 2; 155 | optional OutputProtection RequiredProtection = 3; 156 | } 157 | optional bytes Id = 1; 158 | optional bytes Iv = 2; 159 | optional bytes Key = 3; 160 | optional KeyType Type = 4; 161 | optional SecurityLevel Level = 5; 162 | optional OutputProtection RequiredProtection = 6; 163 | optional OutputProtection RequestedProtection = 7; 164 | optional KeyControl _KeyControl = 8; // duped names, etc 165 | optional OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc 166 | repeated VideoResolutionConstraint VideoResolutionConstraints = 10; 167 | } 168 | optional LicenseIdentification Id = 1; 169 | optional Policy _Policy = 2; // duped names, etc 170 | repeated KeyContainer Key = 3; 171 | optional uint32 LicenseStartTime = 4; 172 | optional uint32 RemoteAttestationVerified = 5; // bool? 173 | optional bytes ProviderClientToken = 6; 174 | // there might be more, check with newer versions (I see field 7-8 in a lic) 175 | // this appeared in latest x86: 176 | optional uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc) 177 | } 178 | 179 | message LicenseError { 180 | enum Error { 181 | INVALID_DEVICE_CERTIFICATE = 1; 182 | REVOKED_DEVICE_CERTIFICATE = 2; 183 | SERVICE_UNAVAILABLE = 3; 184 | } 185 | //LicenseRequest.RequestType ErrorCode; // clang mismatch 186 | optional Error ErrorCode = 1; 187 | } 188 | 189 | message LicenseRequest { 190 | message ContentIdentification { 191 | message CENC { 192 | //optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with: 193 | optional WidevineCencHeader Pssh = 1; 194 | optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!) 195 | optional bytes RequestId = 3; 196 | } 197 | message WebM { 198 | optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used 199 | optional LicenseType LicenseType = 2; 200 | optional bytes RequestId = 3; 201 | } 202 | message ExistingLicense { 203 | optional LicenseIdentification LicenseId = 1; 204 | optional uint32 SecondsSinceStarted = 2; 205 | optional uint32 SecondsSinceLastPlayed = 3; 206 | optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB! 207 | } 208 | optional CENC CencId = 1; 209 | optional WebM WebmId = 2; 210 | optional ExistingLicense License = 3; 211 | } 212 | enum RequestType { 213 | NEW = 1; 214 | RENEWAL = 2; 215 | RELEASE = 3; 216 | } 217 | optional ClientIdentification ClientId = 1; 218 | optional ContentIdentification ContentId = 2; 219 | optional RequestType Type = 3; 220 | optional uint32 RequestTime = 4; 221 | optional bytes KeyControlNonceDeprecated = 5; 222 | optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this 223 | optional uint32 KeyControlNonce = 7; 224 | optional EncryptedClientIdentification EncryptedClientId = 8; 225 | } 226 | 227 | // raw pssh hack 228 | message LicenseRequestRaw { 229 | message ContentIdentification { 230 | message CENC { 231 | optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with: 232 | //optional WidevineCencHeader Pssh = 1; 233 | optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!) 234 | optional bytes RequestId = 3; 235 | } 236 | message WebM { 237 | optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used 238 | optional LicenseType LicenseType = 2; 239 | optional bytes RequestId = 3; 240 | } 241 | message ExistingLicense { 242 | optional LicenseIdentification LicenseId = 1; 243 | optional uint32 SecondsSinceStarted = 2; 244 | optional uint32 SecondsSinceLastPlayed = 3; 245 | optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB! 246 | } 247 | optional CENC CencId = 1; 248 | optional WebM WebmId = 2; 249 | optional ExistingLicense License = 3; 250 | } 251 | enum RequestType { 252 | NEW = 1; 253 | RENEWAL = 2; 254 | RELEASE = 3; 255 | } 256 | optional ClientIdentification ClientId = 1; 257 | optional ContentIdentification ContentId = 2; 258 | optional RequestType Type = 3; 259 | optional uint32 RequestTime = 4; 260 | optional bytes KeyControlNonceDeprecated = 5; 261 | optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this 262 | optional uint32 KeyControlNonce = 7; 263 | optional EncryptedClientIdentification EncryptedClientId = 8; 264 | } 265 | 266 | 267 | message ProvisionedDeviceInfo { 268 | enum WvSecurityLevel { 269 | LEVEL_UNSPECIFIED = 0; 270 | LEVEL_1 = 1; 271 | LEVEL_2 = 2; 272 | LEVEL_3 = 3; 273 | } 274 | optional uint32 SystemId = 1; 275 | optional string Soc = 2; 276 | optional string Manufacturer = 3; 277 | optional string Model = 4; 278 | optional string DeviceType = 5; 279 | optional uint32 ModelYear = 6; 280 | optional WvSecurityLevel SecurityLevel = 7; 281 | optional uint32 TestDevice = 8; // bool? 282 | } 283 | 284 | 285 | // todo: fill 286 | message ProvisioningOptions { 287 | } 288 | 289 | // todo: fill 290 | message ProvisioningRequest { 291 | } 292 | 293 | // todo: fill 294 | message ProvisioningResponse { 295 | } 296 | 297 | message RemoteAttestation { 298 | optional EncryptedClientIdentification Certificate = 1; 299 | optional string Salt = 2; 300 | optional string Signature = 3; 301 | } 302 | 303 | // todo: fill 304 | message SessionInit { 305 | } 306 | 307 | // todo: fill 308 | message SessionState { 309 | } 310 | 311 | // todo: fill 312 | message SignedCertificateStatusList { 313 | } 314 | 315 | message SignedDeviceCertificate { 316 | 317 | //optional bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is: 318 | optional DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later 319 | optional bytes Signature = 2; 320 | optional SignedDeviceCertificate Signer = 3; 321 | } 322 | 323 | 324 | // todo: fill 325 | message SignedProvisioningMessage { 326 | } 327 | 328 | // the root of all messages, from either server or client 329 | message SignedMessage { 330 | enum MessageType { 331 | LICENSE_REQUEST = 1; 332 | LICENSE = 2; 333 | ERROR_RESPONSE = 3; 334 | SERVICE_CERTIFICATE_REQUEST = 4; 335 | SERVICE_CERTIFICATE = 5; 336 | } 337 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 338 | optional bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 339 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 340 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 341 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 342 | optional RemoteAttestation RemoteAttestation = 5; 343 | } 344 | 345 | 346 | 347 | // This message is copied from google's docs, not reversed: 348 | message WidevineCencHeader { 349 | enum Algorithm { 350 | UNENCRYPTED = 0; 351 | AESCTR = 1; 352 | }; 353 | optional Algorithm algorithm = 1; 354 | repeated bytes key_id = 2; 355 | 356 | // Content provider name. 357 | optional string provider = 3; 358 | 359 | // A content identifier, specified by content provider. 360 | optional bytes content_id = 4; 361 | 362 | // Track type. Acceptable values are SD, HD and AUDIO. Used to 363 | // differentiate content keys used by an asset. 364 | optional string track_type_deprecated = 5; 365 | 366 | // The name of a registered policy to be used for this asset. 367 | optional string policy = 6; 368 | 369 | // Crypto period index, for media using key rotation. 370 | optional uint32 crypto_period_index = 7; 371 | 372 | // Optional protected context for group content. The grouped_license is a 373 | // serialized SignedMessage. 374 | optional bytes grouped_license = 8; 375 | 376 | // Protection scheme identifying the encryption algorithm. 377 | // Represented as one of the following 4CC values: 378 | // 'cenc' (AESCTR), 'cbc1' (AESCBC), 379 | // 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample). 380 | optional uint32 protection_scheme = 9; 381 | 382 | // Optional. For media using key rotation, this represents the duration 383 | // of each crypto period in seconds. 384 | optional uint32 crypto_period_seconds = 10; 385 | } 386 | 387 | 388 | // remove these when using it outside of protoc: 389 | 390 | // from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically 391 | message SignedLicenseRequest { 392 | enum MessageType { 393 | LICENSE_REQUEST = 1; 394 | LICENSE = 2; 395 | ERROR_RESPONSE = 3; 396 | SERVICE_CERTIFICATE_REQUEST = 4; 397 | SERVICE_CERTIFICATE = 5; 398 | } 399 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 400 | optional LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 401 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 402 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 403 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 404 | optional RemoteAttestation RemoteAttestation = 5; 405 | } 406 | 407 | // hack 408 | message SignedLicenseRequestRaw { 409 | enum MessageType { 410 | LICENSE_REQUEST = 1; 411 | LICENSE = 2; 412 | ERROR_RESPONSE = 3; 413 | SERVICE_CERTIFICATE_REQUEST = 4; 414 | SERVICE_CERTIFICATE = 5; 415 | } 416 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 417 | optional LicenseRequestRaw Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 418 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 419 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 420 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 421 | optional RemoteAttestation RemoteAttestation = 5; 422 | } 423 | 424 | 425 | message SignedLicense { 426 | enum MessageType { 427 | LICENSE_REQUEST = 1; 428 | LICENSE = 2; 429 | ERROR_RESPONSE = 3; 430 | SERVICE_CERTIFICATE_REQUEST = 4; 431 | SERVICE_CERTIFICATE = 5; 432 | } 433 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 434 | optional License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 435 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 436 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 437 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 438 | optional RemoteAttestation RemoteAttestation = 5; 439 | } 440 | 441 | message SignedServiceCertificate { 442 | enum MessageType { 443 | LICENSE_REQUEST = 1; 444 | LICENSE = 2; 445 | ERROR_RESPONSE = 3; 446 | SERVICE_CERTIFICATE_REQUEST = 4; 447 | SERVICE_CERTIFICATE = 5; 448 | } 449 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 450 | optional SignedDeviceCertificate Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 451 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 452 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 453 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 454 | optional RemoteAttestation RemoteAttestation = 5; 455 | } 456 | 457 | //vmp support 458 | message FileHashes { 459 | message Signature { 460 | optional string filename = 1; 461 | optional bool test_signing = 2; //0 - release, 1 - testing 462 | optional bytes SHA512Hash = 3; 463 | optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file 464 | optional bytes signature = 5; 465 | } 466 | optional bytes signer = 1; 467 | repeated Signature signatures = 2; 468 | } 469 | --------------------------------------------------------------------------------