Migration
This commit is contained in:
commit
fc731103b5
|
@ -0,0 +1,8 @@
|
||||||
|
device_client_id_blob*
|
||||||
|
device_private_key*
|
||||||
|
manifest.mpd*
|
||||||
|
*.exe
|
||||||
|
headers.json*
|
||||||
|
test.go
|
||||||
|
*.txt
|
||||||
|
downey*
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Downey
|
||||||
|
|
||||||
|
Original repo can be found here https://github.com/dengskoloper/downey
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
To compile downey for your platform, download the golang installation package from here: https://go.dev/dl/. Make sure that the go executable is added to the PATH variable.
|
||||||
|
|
||||||
|
Next, clone the repo locally and change the directory to the root of the repo. Run this command to build the code for your platform:
|
||||||
|
|
||||||
|
```
|
||||||
|
go build -o downey[.exe] -a main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Tool options:
|
||||||
|
|
||||||
|
```
|
||||||
|
downey.exe --lic-server "<License Server URL>" [--add-headers]
|
||||||
|
--lic-server: License Server URL for the manifest.mpd
|
||||||
|
--add-headers: Read HTTP headers from headers.json
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use the --add-headers option, then you need to create a headers.json file with structure like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"cookie": "hs_uid=b82f6b01-4bf2-4828-9cd9-ea64d514f9a6; ajs_group_id=null",
|
||||||
|
"origin": "https://www.hotstar.com",
|
||||||
|
"referer": "https://www.hotstar.com/",
|
||||||
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Downey requires the MPD to be added to the root of the Downey directory, and has to be named as `manifest.mpd`
|
|
@ -0,0 +1,274 @@
|
||||||
|
package widevine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"github.com/aead/cmac"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"lukechampine.com/frand"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CDM struct {
|
||||||
|
privateKey *rsa.PrivateKey
|
||||||
|
clientID []byte
|
||||||
|
sessionID [32]byte
|
||||||
|
|
||||||
|
widevineCencHeader WidevineCencHeader
|
||||||
|
signedDeviceCertificate SignedDeviceCertificate
|
||||||
|
privacyMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key struct {
|
||||||
|
ID []byte
|
||||||
|
Type License_KeyContainer_KeyType
|
||||||
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new CDM object with the specified device information.
|
||||||
|
func NewCDM(privateKey string, clientID []byte, initData []byte) (CDM, error) {
|
||||||
|
block, _ := pem.Decode([]byte(privateKey))
|
||||||
|
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||||
|
return CDM{}, errors.New("failed to decode device private key")
|
||||||
|
}
|
||||||
|
keyParsed, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return CDM{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var widevineCencHeader WidevineCencHeader
|
||||||
|
if len(initData) < 32 {
|
||||||
|
return CDM{}, errors.New("initData not long enough")
|
||||||
|
}
|
||||||
|
if err := proto.Unmarshal(initData[32:], &widevineCencHeader); err != nil {
|
||||||
|
return CDM{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID := func() (s [32]byte) {
|
||||||
|
c := []byte("ABCDEF0123456789")
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
s[i] = c[frand.Intn(len(c))]
|
||||||
|
}
|
||||||
|
s[16] = '0'
|
||||||
|
s[17] = '1'
|
||||||
|
for i := 18; i < 32; i++ {
|
||||||
|
s[i] = '0'
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}()
|
||||||
|
|
||||||
|
return CDM{
|
||||||
|
privateKey: keyParsed,
|
||||||
|
clientID: clientID,
|
||||||
|
|
||||||
|
widevineCencHeader: widevineCencHeader,
|
||||||
|
|
||||||
|
sessionID: sessionID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new CDM object using the default device configuration.
|
||||||
|
func NewDefaultCDM(initData []byte) (CDM, error) {
|
||||||
|
return NewCDM(DefaultPrivateKey, DefaultClientID, initData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets a device certificate. This is makes generating the license request
|
||||||
|
// more complicated but is supported. This is usually not necessary for most
|
||||||
|
// Widevine applications.
|
||||||
|
func (c *CDM) SetServiceCertificate(certData []byte) error {
|
||||||
|
var message SignedMessage
|
||||||
|
if err := proto.Unmarshal(certData, &message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := proto.Unmarshal(message.Msg, &c.signedDeviceCertificate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.privacyMode = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CDM) GetServiceCertificate() *SignedDeviceCertificate {
|
||||||
|
|
||||||
|
return &c.signedDeviceCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the license request data. This is sent to the license server via
|
||||||
|
// HTTP POST and the server in turn returns the license response.
|
||||||
|
func (c *CDM) GetLicenseRequest() ([]byte, error) {
|
||||||
|
var licenseRequest SignedLicenseRequest
|
||||||
|
licenseRequest.Msg = new(LicenseRequest)
|
||||||
|
licenseRequest.Msg.ContentId = new(LicenseRequest_ContentIdentification)
|
||||||
|
licenseRequest.Msg.ContentId.CencId = new(LicenseRequest_ContentIdentification_CENC)
|
||||||
|
|
||||||
|
// this is probably really bad for the GC but protobuf uses pointers for optional
|
||||||
|
// fields so it is necessary and this is not a long running program
|
||||||
|
{
|
||||||
|
v := SignedLicenseRequest_LICENSE_REQUEST
|
||||||
|
licenseRequest.Type = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
licenseRequest.Msg.ContentId.CencId.Pssh = &c.widevineCencHeader
|
||||||
|
|
||||||
|
{
|
||||||
|
v := LicenseType_DEFAULT
|
||||||
|
licenseRequest.Msg.ContentId.CencId.LicenseType = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
licenseRequest.Msg.ContentId.CencId.RequestId = c.sessionID[:]
|
||||||
|
|
||||||
|
{
|
||||||
|
v := LicenseRequest_NEW
|
||||||
|
licenseRequest.Msg.Type = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
v := uint32(time.Now().Unix())
|
||||||
|
licenseRequest.Msg.RequestTime = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
v := ProtocolVersion_CURRENT
|
||||||
|
licenseRequest.Msg.ProtocolVersion = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
v := uint32(frand.Uint64n(math.MaxUint32))
|
||||||
|
licenseRequest.Msg.KeyControlNonce = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.privacyMode {
|
||||||
|
pad := func(data []byte, blockSize int) []byte {
|
||||||
|
padlen := blockSize - (len(data) % blockSize)
|
||||||
|
if padlen == 0 {
|
||||||
|
padlen = blockSize
|
||||||
|
}
|
||||||
|
return append(data, bytes.Repeat([]byte{byte(padlen)}, padlen)...)
|
||||||
|
}
|
||||||
|
const blockSize = 16
|
||||||
|
|
||||||
|
var cidKey, cidIV [blockSize]byte
|
||||||
|
frand.Read(cidKey[:])
|
||||||
|
frand.Read(cidIV[:])
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(cidKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paddedClientID := pad(c.clientID, blockSize)
|
||||||
|
encryptedClientID := make([]byte, len(paddedClientID))
|
||||||
|
cipher.NewCBCEncrypter(block, cidIV[:]).CryptBlocks(encryptedClientID, paddedClientID)
|
||||||
|
|
||||||
|
servicePublicKey, err := x509.ParsePKCS1PublicKey(c.signedDeviceCertificate.XDeviceCertificate.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedCIDKey, err := rsa.EncryptOAEP(sha1.New(), frand.Reader, servicePublicKey, cidKey[:], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
licenseRequest.Msg.EncryptedClientId = new(EncryptedClientIdentification)
|
||||||
|
{
|
||||||
|
v := string(c.signedDeviceCertificate.XDeviceCertificate.ServiceId)
|
||||||
|
licenseRequest.Msg.EncryptedClientId.ServiceId = &v
|
||||||
|
}
|
||||||
|
licenseRequest.Msg.EncryptedClientId.ServiceCertificateSerialNumber = c.signedDeviceCertificate.XDeviceCertificate.SerialNumber
|
||||||
|
licenseRequest.Msg.EncryptedClientId.EncryptedClientId = encryptedClientID
|
||||||
|
licenseRequest.Msg.EncryptedClientId.EncryptedClientIdIv = cidIV[:]
|
||||||
|
licenseRequest.Msg.EncryptedClientId.EncryptedPrivacyKey = encryptedCIDKey
|
||||||
|
} else {
|
||||||
|
licenseRequest.Msg.ClientId = new(ClientIdentification)
|
||||||
|
if err := proto.Unmarshal(c.clientID, licenseRequest.Msg.ClientId); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data, err := proto.Marshal(licenseRequest.Msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hash := sha1.Sum(data)
|
||||||
|
if licenseRequest.Signature, err = rsa.SignPSS(frand.Reader, c.privateKey, crypto.SHA1, hash[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto.Marshal(&licenseRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves the keys from the license response data. These keys can be
|
||||||
|
// used to decrypt the DASH-MP4.
|
||||||
|
func (c *CDM) GetLicenseKeys(licenseRequest []byte, licenseResponse []byte) (keys []Key, err error) {
|
||||||
|
var license SignedLicense
|
||||||
|
if err = proto.Unmarshal(licenseResponse, &license); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var licenseRequestParsed SignedLicenseRequest
|
||||||
|
if err = proto.Unmarshal(licenseRequest, &licenseRequestParsed); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
licenseRequestMsg, err := proto.Marshal(licenseRequestParsed.Msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionKey, err := rsa.DecryptOAEP(sha1.New(), frand.Reader, c.privateKey, license.SessionKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionKeyBlock, err := aes.NewCipher(sessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionKey := []byte{1, 'E', 'N', 'C', 'R', 'Y', 'P', 'T', 'I', 'O', 'N', 0}
|
||||||
|
encryptionKey = append(encryptionKey, licenseRequestMsg...)
|
||||||
|
encryptionKey = append(encryptionKey, []byte{0, 0, 0, 0x80}...)
|
||||||
|
encryptionKeyCmac, err := cmac.Sum(encryptionKey, sessionKeyBlock, sessionKeyBlock.BlockSize())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encryptionKeyCipher, err := aes.NewCipher(encryptionKeyCmac)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unpad := func(b []byte) []byte {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
// pks padding is designed so that the value of all the padding bytes is
|
||||||
|
// the number of padding bytes repeated so to figure out how many
|
||||||
|
// padding bytes there are we can just look at the value of the last
|
||||||
|
// byte
|
||||||
|
// i.e if there are 6 padding bytes then it will look at like
|
||||||
|
// <data> 0x6 0x6 0x6 0x6 0x6 0x6
|
||||||
|
count := int(b[len(b)-1])
|
||||||
|
return b[0 : len(b)-count]
|
||||||
|
}
|
||||||
|
for _, key := range license.Msg.Key {
|
||||||
|
decrypter := cipher.NewCBCDecrypter(encryptionKeyCipher, key.Iv)
|
||||||
|
decryptedKey := make([]byte, len(key.Key))
|
||||||
|
decrypter.CryptBlocks(decryptedKey, key.Key)
|
||||||
|
keys = append(keys, Key{
|
||||||
|
ID: key.Id,
|
||||||
|
Type: *key.Type,
|
||||||
|
Value: unpad(decryptedKey),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package widevine
|
||||||
|
|
||||||
|
import "io/ioutil"
|
||||||
|
|
||||||
|
var DefaultPrivateKey string
|
||||||
|
var DefaultClientID []byte
|
||||||
|
|
||||||
|
func InitConstants() {
|
||||||
|
DefaultPrivateKeyBuffer, err := ioutil.ReadFile("device_private_key")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
DefaultPrivateKey = string(DefaultPrivateKeyBuffer)
|
||||||
|
|
||||||
|
DefaultClientID, err = ioutil.ReadFile("device_client_id_blob")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package widevine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This function retrieves the PSSH/Init Data from a given MPD file reader.
|
||||||
|
// Example file: https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd
|
||||||
|
func InitDataFromMPD(r io.Reader) ([]byte, error) {
|
||||||
|
type mpd struct {
|
||||||
|
XMLName xml.Name `xml:"MPD"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
Profiles string `xml:"profiles,attr"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
AvailabilityStartTime string `xml:"availabilityStartTime,attr"`
|
||||||
|
PublishTime string `xml:"publishTime,attr"`
|
||||||
|
MediaPresentationDuration string `xml:"mediaPresentationDuration,attr"`
|
||||||
|
MinBufferTime string `xml:"minBufferTime,attr"`
|
||||||
|
Version string `xml:"version,attr"`
|
||||||
|
Ns2 string `xml:"ns2,attr"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Bitmovin string `xml:"bitmovin,attr"`
|
||||||
|
Period struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
AdaptationSet []struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
MimeType string `xml:"mimeType,attr"`
|
||||||
|
Codecs string `xml:"codecs,attr"`
|
||||||
|
Lang string `xml:"lang,attr"`
|
||||||
|
Label string `xml:"label,attr"`
|
||||||
|
SegmentTemplate struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Media string `xml:"media,attr"`
|
||||||
|
Initialization string `xml:"initialization,attr"`
|
||||||
|
Duration string `xml:"duration,attr"`
|
||||||
|
StartNumber string `xml:"startNumber,attr"`
|
||||||
|
Timescale string `xml:"timescale,attr"`
|
||||||
|
} `xml:"SegmentTemplate"`
|
||||||
|
ContentProtection []struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
SchemeIdUri string `xml:"schemeIdUri,attr"`
|
||||||
|
Value string `xml:"value,attr"`
|
||||||
|
DefaultKID string `xml:"default_KID,attr"`
|
||||||
|
Pssh string `xml:"pssh"`
|
||||||
|
} `xml:"ContentProtection"`
|
||||||
|
Representation []struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
Bandwidth string `xml:"bandwidth,attr"`
|
||||||
|
Width string `xml:"width,attr"`
|
||||||
|
Height string `xml:"height,attr"`
|
||||||
|
FrameRate string `xml:"frameRate,attr"`
|
||||||
|
AudioSamplingRate string `xml:"audioSamplingRate,attr"`
|
||||||
|
ContentProtection []struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
SchemeIdUri string `xml:"schemeIdUri,attr"`
|
||||||
|
Value string `xml:"value,attr"`
|
||||||
|
DefaultKID string `xml:"default_KID,attr"`
|
||||||
|
Cenc string `xml:"cenc,attr"`
|
||||||
|
Pssh struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Cenc string `xml:"cenc,attr"`
|
||||||
|
} `xml:"pssh"`
|
||||||
|
} `xml:"ContentProtection"`
|
||||||
|
} `xml:"Representation"`
|
||||||
|
AudioChannelConfiguration struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
SchemeIdUri string `xml:"schemeIdUri,attr"`
|
||||||
|
Value string `xml:"value,attr"`
|
||||||
|
} `xml:"AudioChannelConfiguration"`
|
||||||
|
} `xml:"AdaptationSet"`
|
||||||
|
} `xml:"Period"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpdPlaylist mpd
|
||||||
|
if err := xml.NewDecoder(r).Decode(&mpdPlaylist); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const widevineSchemeIdURI = "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"
|
||||||
|
for _, adaptionSet := range mpdPlaylist.Period.AdaptationSet {
|
||||||
|
for _, protection := range adaptionSet.ContentProtection {
|
||||||
|
if protection.SchemeIdUri == widevineSchemeIdURI && len(protection.Pssh) > 0 {
|
||||||
|
return base64.StdEncoding.DecodeString(protection.Pssh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, adaptionSet := range mpdPlaylist.Period.AdaptationSet {
|
||||||
|
for _, representation := range adaptionSet.Representation {
|
||||||
|
for _, protection := range representation.ContentProtection {
|
||||||
|
if protection.SchemeIdUri == widevineSchemeIdURI && len(protection.Pssh.Text) > 0 {
|
||||||
|
return base64.StdEncoding.DecodeString(protection.Pssh.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("no init data found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function retrieves certificate data from a given license server.
|
||||||
|
func GetCertData(client *http.Client, licenseURL string) ([]byte, error) {
|
||||||
|
response, err := client.Post(licenseURL, "application/x-www-form-urlencoded", bytes.NewReader([]byte{0x08, 0x04}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
return io.ReadAll(response.Body)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
||||||
|
module github.com/dengskoloper/downey
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1
|
||||||
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
|
google.golang.org/protobuf v1.28.0
|
||||||
|
lukechampine.com/frand v1.4.2
|
||||||
|
)
|
|
@ -0,0 +1,19 @@
|
||||||
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||||
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||||
|
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY=
|
||||||
|
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
|
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw=
|
||||||
|
lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s=
|
|
@ -0,0 +1,92 @@
|
||||||
|
package main
|
||||||
|
import (
|
||||||
|
"github.com/dengskoloper/downey/cdm"
|
||||||
|
"github.com/dengskoloper/downey/util"
|
||||||
|
"net/http"
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
"os"
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
var opts struct {
|
||||||
|
LicenseServerURL string `long:"lic-server" description:"License Server URL"`
|
||||||
|
AddHeaders bool `long:"add-headers" description:"Read HTTP headers from headers.json"`
|
||||||
|
InitPSSH string `long:"pssh" description:"Override PSSH data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
widevine.InitConstants()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
_, err := flags.Parse(&opts)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var initData []byte
|
||||||
|
if len(opts.InitPSSH) > 0 {
|
||||||
|
initData, err = base64.StdEncoding.DecodeString(opts.InitPSSH)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
file, err := os.Open("manifest.mpd")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initData, err = widevine.InitDataFromMPD(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cdm, err := widevine.NewDefaultCDM(initData)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
licenseRequest, err := cdm.GetLicenseRequest()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest(http.MethodPost, opts.LicenseServerURL, bytes.NewReader(licenseRequest))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.AddHeaders {
|
||||||
|
util.ReadHeadersFromJSON(&request.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Close = true
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
licenseResponse, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
keys, err := cdm.GetLicenseKeys(licenseRequest, licenseResponse)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := ""
|
||||||
|
for _, key := range keys {
|
||||||
|
if key.Type == widevine.License_KeyContainer_CONTENT {
|
||||||
|
command += "\n" + hex.EncodeToString(key.ID) + ":" + hex.EncodeToString(key.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("\nDecryption keys: ", command)
|
||||||
|
}
|
|
@ -0,0 +1,468 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
option go_package = ".;widevine";
|
||||||
|
|
||||||
|
// from x86 (partial), most of it from the ARM version:
|
||||||
|
message ClientIdentification {
|
||||||
|
enum TokenType {
|
||||||
|
KEYBOX = 0;
|
||||||
|
DEVICE_CERTIFICATE = 1;
|
||||||
|
REMOTE_ATTESTATION_CERTIFICATE = 2;
|
||||||
|
}
|
||||||
|
message NameValue {
|
||||||
|
required string Name = 1;
|
||||||
|
required string Value = 2;
|
||||||
|
}
|
||||||
|
message ClientCapabilities {
|
||||||
|
enum HdcpVersion {
|
||||||
|
HDCP_NONE = 0;
|
||||||
|
HDCP_V1 = 1;
|
||||||
|
HDCP_V2 = 2;
|
||||||
|
HDCP_V2_1 = 3;
|
||||||
|
HDCP_V2_2 = 4;
|
||||||
|
}
|
||||||
|
optional uint32 ClientToken = 1;
|
||||||
|
optional uint32 SessionToken = 2;
|
||||||
|
optional uint32 VideoResolutionConstraints = 3;
|
||||||
|
optional HdcpVersion MaxHdcpVersion = 4;
|
||||||
|
optional uint32 OemCryptoApiVersion = 5;
|
||||||
|
}
|
||||||
|
required TokenType Type = 1;
|
||||||
|
//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:
|
||||||
|
optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob
|
||||||
|
repeated NameValue ClientInfo = 3;
|
||||||
|
optional bytes ProviderClientToken = 4;
|
||||||
|
optional uint32 LicenseCounter = 5;
|
||||||
|
optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later
|
||||||
|
optional FileHashes _FileHashes = 7; // vmp blob goes here
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeviceCertificate {
|
||||||
|
enum CertificateType {
|
||||||
|
ROOT = 0;
|
||||||
|
INTERMEDIATE = 1;
|
||||||
|
USER_DEVICE = 2;
|
||||||
|
SERVICE = 3;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
optional bytes SerialNumber = 2;
|
||||||
|
optional uint32 CreationTimeSeconds = 3;
|
||||||
|
optional bytes PublicKey = 4;
|
||||||
|
optional uint32 SystemId = 5;
|
||||||
|
optional uint32 TestDeviceDeprecated = 6; // is it bool or int?
|
||||||
|
optional bytes ServiceId = 7; // service URL for service certificates
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing some references,
|
||||||
|
message DeviceCertificateStatus {
|
||||||
|
enum CertificateStatus {
|
||||||
|
VALID = 0;
|
||||||
|
REVOKED = 1;
|
||||||
|
}
|
||||||
|
optional bytes SerialNumber = 1;
|
||||||
|
optional CertificateStatus Status = 2;
|
||||||
|
optional ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated?
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeviceCertificateStatusList {
|
||||||
|
optional uint32 CreationTimeSeconds = 1;
|
||||||
|
repeated DeviceCertificateStatus CertificateStatus = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EncryptedClientIdentification {
|
||||||
|
required string ServiceId = 1;
|
||||||
|
optional bytes ServiceCertificateSerialNumber = 2;
|
||||||
|
required bytes EncryptedClientId = 3;
|
||||||
|
required bytes EncryptedClientIdIv = 4;
|
||||||
|
required bytes EncryptedPrivacyKey = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: fill (for this top-level type, it might be impossible/difficult)
|
||||||
|
enum LicenseType {
|
||||||
|
ZERO = 0;
|
||||||
|
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
|
||||||
|
OFFLINE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: fill (for this top-level type, it might be impossible/difficult)
|
||||||
|
// this is just a guess because these globals got lost, but really, do we need more?
|
||||||
|
enum ProtocolVersion {
|
||||||
|
CURRENT = 21; // don't have symbols for this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message LicenseIdentification {
|
||||||
|
optional bytes RequestId = 1;
|
||||||
|
optional bytes SessionId = 2;
|
||||||
|
optional bytes PurchaseId = 3;
|
||||||
|
optional LicenseType Type = 4;
|
||||||
|
optional uint32 Version = 5;
|
||||||
|
optional bytes ProviderSessionToken = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message License {
|
||||||
|
message Policy {
|
||||||
|
optional bool CanPlay = 1; // changed from uint32 to bool
|
||||||
|
optional bool CanPersist = 2;
|
||||||
|
optional bool CanRenew = 3;
|
||||||
|
optional uint32 RentalDurationSeconds = 4;
|
||||||
|
optional uint32 PlaybackDurationSeconds = 5;
|
||||||
|
optional uint32 LicenseDurationSeconds = 6;
|
||||||
|
optional uint32 RenewalRecoveryDurationSeconds = 7;
|
||||||
|
optional string RenewalServerUrl = 8;
|
||||||
|
optional uint32 RenewalDelaySeconds = 9;
|
||||||
|
optional uint32 RenewalRetryIntervalSeconds = 10;
|
||||||
|
optional bool RenewWithUsage = 11; // was uint32
|
||||||
|
}
|
||||||
|
message KeyContainer {
|
||||||
|
enum KeyType {
|
||||||
|
SIGNING = 1;
|
||||||
|
CONTENT = 2;
|
||||||
|
KEY_CONTROL = 3;
|
||||||
|
OPERATOR_SESSION = 4;
|
||||||
|
}
|
||||||
|
enum SecurityLevel {
|
||||||
|
SW_SECURE_CRYPTO = 1;
|
||||||
|
SW_SECURE_DECODE = 2;
|
||||||
|
HW_SECURE_CRYPTO = 3;
|
||||||
|
HW_SECURE_DECODE = 4;
|
||||||
|
HW_SECURE_ALL = 5;
|
||||||
|
}
|
||||||
|
message OutputProtection {
|
||||||
|
enum CGMS {
|
||||||
|
COPY_FREE = 0;
|
||||||
|
COPY_ONCE = 2;
|
||||||
|
COPY_NEVER = 3;
|
||||||
|
CGMS_NONE = 0x2A; // PC default!
|
||||||
|
}
|
||||||
|
optional ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away
|
||||||
|
optional CGMS CgmsFlags = 2;
|
||||||
|
}
|
||||||
|
message KeyControl {
|
||||||
|
required bytes KeyControlBlock = 1; // what is this?
|
||||||
|
required bytes Iv = 2;
|
||||||
|
}
|
||||||
|
message OperatorSessionKeyPermissions {
|
||||||
|
optional uint32 AllowEncrypt = 1;
|
||||||
|
optional uint32 AllowDecrypt = 2;
|
||||||
|
optional uint32 AllowSign = 3;
|
||||||
|
optional uint32 AllowSignatureVerify = 4;
|
||||||
|
}
|
||||||
|
message VideoResolutionConstraint {
|
||||||
|
optional uint32 MinResolutionPixels = 1;
|
||||||
|
optional uint32 MaxResolutionPixels = 2;
|
||||||
|
optional OutputProtection RequiredProtection = 3;
|
||||||
|
}
|
||||||
|
optional bytes Id = 1;
|
||||||
|
optional bytes Iv = 2;
|
||||||
|
optional bytes Key = 3;
|
||||||
|
optional KeyType Type = 4;
|
||||||
|
optional SecurityLevel Level = 5;
|
||||||
|
optional OutputProtection RequiredProtection = 6;
|
||||||
|
optional OutputProtection RequestedProtection = 7;
|
||||||
|
optional KeyControl _KeyControl = 8; // duped names, etc
|
||||||
|
optional OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc
|
||||||
|
repeated VideoResolutionConstraint VideoResolutionConstraints = 10;
|
||||||
|
}
|
||||||
|
optional LicenseIdentification Id = 1;
|
||||||
|
optional Policy _Policy = 2; // duped names, etc
|
||||||
|
repeated KeyContainer Key = 3;
|
||||||
|
optional uint32 LicenseStartTime = 4;
|
||||||
|
optional uint32 RemoteAttestationVerified = 5; // bool?
|
||||||
|
optional bytes ProviderClientToken = 6;
|
||||||
|
// there might be more, check with newer versions (I see field 7-8 in a lic)
|
||||||
|
// this appeared in latest x86:
|
||||||
|
optional uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc)
|
||||||
|
}
|
||||||
|
|
||||||
|
message LicenseError {
|
||||||
|
enum Error {
|
||||||
|
INVALID_DEVICE_CERTIFICATE = 1;
|
||||||
|
REVOKED_DEVICE_CERTIFICATE = 2;
|
||||||
|
SERVICE_UNAVAILABLE = 3;
|
||||||
|
}
|
||||||
|
//LicenseRequest.RequestType ErrorCode; // clang mismatch
|
||||||
|
optional Error ErrorCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LicenseRequest {
|
||||||
|
message ContentIdentification {
|
||||||
|
message CENC {
|
||||||
|
//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:
|
||||||
|
optional WidevineCencHeader Pssh = 1;
|
||||||
|
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!)
|
||||||
|
optional bytes RequestId = 3;
|
||||||
|
}
|
||||||
|
message WebM {
|
||||||
|
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
|
||||||
|
optional LicenseType LicenseType = 2;
|
||||||
|
optional bytes RequestId = 3;
|
||||||
|
}
|
||||||
|
message ExistingLicense {
|
||||||
|
optional LicenseIdentification LicenseId = 1;
|
||||||
|
optional uint32 SecondsSinceStarted = 2;
|
||||||
|
optional uint32 SecondsSinceLastPlayed = 3;
|
||||||
|
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
|
||||||
|
}
|
||||||
|
optional CENC CencId = 1;
|
||||||
|
optional WebM WebmId = 2;
|
||||||
|
optional ExistingLicense License = 3;
|
||||||
|
}
|
||||||
|
enum RequestType {
|
||||||
|
NEW = 1;
|
||||||
|
RENEWAL = 2;
|
||||||
|
RELEASE = 3;
|
||||||
|
}
|
||||||
|
optional ClientIdentification ClientId = 1;
|
||||||
|
optional ContentIdentification ContentId = 2;
|
||||||
|
optional RequestType Type = 3;
|
||||||
|
optional uint32 RequestTime = 4;
|
||||||
|
optional bytes KeyControlNonceDeprecated = 5;
|
||||||
|
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
|
||||||
|
optional uint32 KeyControlNonce = 7;
|
||||||
|
optional EncryptedClientIdentification EncryptedClientId = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// raw pssh hack
|
||||||
|
message LicenseRequestRaw {
|
||||||
|
message ContentIdentification {
|
||||||
|
message CENC {
|
||||||
|
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:
|
||||||
|
//optional WidevineCencHeader Pssh = 1;
|
||||||
|
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!)
|
||||||
|
optional bytes RequestId = 3;
|
||||||
|
}
|
||||||
|
message WebM {
|
||||||
|
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
|
||||||
|
optional LicenseType LicenseType = 2;
|
||||||
|
optional bytes RequestId = 3;
|
||||||
|
}
|
||||||
|
message ExistingLicense {
|
||||||
|
optional LicenseIdentification LicenseId = 1;
|
||||||
|
optional uint32 SecondsSinceStarted = 2;
|
||||||
|
optional uint32 SecondsSinceLastPlayed = 3;
|
||||||
|
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
|
||||||
|
}
|
||||||
|
optional CENC CencId = 1;
|
||||||
|
optional WebM WebmId = 2;
|
||||||
|
optional ExistingLicense License = 3;
|
||||||
|
}
|
||||||
|
enum RequestType {
|
||||||
|
NEW = 1;
|
||||||
|
RENEWAL = 2;
|
||||||
|
RELEASE = 3;
|
||||||
|
}
|
||||||
|
optional ClientIdentification ClientId = 1;
|
||||||
|
optional ContentIdentification ContentId = 2;
|
||||||
|
optional RequestType Type = 3;
|
||||||
|
optional uint32 RequestTime = 4;
|
||||||
|
optional bytes KeyControlNonceDeprecated = 5;
|
||||||
|
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
|
||||||
|
optional uint32 KeyControlNonce = 7;
|
||||||
|
optional EncryptedClientIdentification EncryptedClientId = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message ProvisionedDeviceInfo {
|
||||||
|
enum WvSecurityLevel {
|
||||||
|
LEVEL_UNSPECIFIED = 0;
|
||||||
|
LEVEL_1 = 1;
|
||||||
|
LEVEL_2 = 2;
|
||||||
|
LEVEL_3 = 3;
|
||||||
|
}
|
||||||
|
optional uint32 SystemId = 1;
|
||||||
|
optional string Soc = 2;
|
||||||
|
optional string Manufacturer = 3;
|
||||||
|
optional string Model = 4;
|
||||||
|
optional string DeviceType = 5;
|
||||||
|
optional uint32 ModelYear = 6;
|
||||||
|
optional WvSecurityLevel SecurityLevel = 7;
|
||||||
|
optional uint32 TestDevice = 8; // bool?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// todo: fill
|
||||||
|
message ProvisioningOptions {
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: fill
|
||||||
|
message ProvisioningRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: fill
|
||||||
|
message ProvisioningResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoteAttestation {
|
||||||
|
optional EncryptedClientIdentification Certificate = 1;
|
||||||
|
optional string Salt = 2;
|
||||||
|
optional string Signature = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: fill
|
||||||
|
message SessionInit {
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: fill
|
||||||
|
message SessionState {
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: fill
|
||||||
|
message SignedCertificateStatusList {
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignedDeviceCertificate {
|
||||||
|
|
||||||
|
//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:
|
||||||
|
optional DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later
|
||||||
|
optional bytes Signature = 2;
|
||||||
|
optional SignedDeviceCertificate Signer = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// todo: fill
|
||||||
|
message SignedProvisioningMessage {
|
||||||
|
}
|
||||||
|
|
||||||
|
// the root of all messages, from either server or client
|
||||||
|
message SignedMessage {
|
||||||
|
enum MessageType {
|
||||||
|
LICENSE_REQUEST = 1;
|
||||||
|
LICENSE = 2;
|
||||||
|
ERROR_RESPONSE = 3;
|
||||||
|
SERVICE_CERTIFICATE_REQUEST = 4;
|
||||||
|
SERVICE_CERTIFICATE = 5;
|
||||||
|
}
|
||||||
|
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
|
||||||
|
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
|
||||||
|
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
|
||||||
|
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
|
||||||
|
optional bytes SessionKey = 4; // often RSA wrapped for licenses
|
||||||
|
optional RemoteAttestation RemoteAttestation = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// This message is copied from google's docs, not reversed:
|
||||||
|
message WidevineCencHeader {
|
||||||
|
enum Algorithm {
|
||||||
|
UNENCRYPTED = 0;
|
||||||
|
AESCTR = 1;
|
||||||
|
};
|
||||||
|
optional Algorithm algorithm = 1;
|
||||||
|
repeated bytes key_id = 2;
|
||||||
|
|
||||||
|
// Content provider name.
|
||||||
|
optional string provider = 3;
|
||||||
|
|
||||||
|
// A content identifier, specified by content provider.
|
||||||
|
optional bytes content_id = 4;
|
||||||
|
|
||||||
|
// Track type. Acceptable values are SD, HD and AUDIO. Used to
|
||||||
|
// differentiate content keys used by an asset.
|
||||||
|
optional string track_type_deprecated = 5;
|
||||||
|
|
||||||
|
// The name of a registered policy to be used for this asset.
|
||||||
|
optional string policy = 6;
|
||||||
|
|
||||||
|
// Crypto period index, for media using key rotation.
|
||||||
|
optional uint32 crypto_period_index = 7;
|
||||||
|
|
||||||
|
// Optional protected context for group content. The grouped_license is a
|
||||||
|
// serialized SignedMessage.
|
||||||
|
optional bytes grouped_license = 8;
|
||||||
|
|
||||||
|
// Protection scheme identifying the encryption algorithm.
|
||||||
|
// Represented as one of the following 4CC values:
|
||||||
|
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
|
||||||
|
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
|
||||||
|
optional uint32 protection_scheme = 9;
|
||||||
|
|
||||||
|
// Optional. For media using key rotation, this represents the duration
|
||||||
|
// of each crypto period in seconds.
|
||||||
|
optional uint32 crypto_period_seconds = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// remove these when using it outside of protoc:
|
||||||
|
|
||||||
|
// 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
|
||||||
|
message SignedLicenseRequest {
|
||||||
|
enum MessageType {
|
||||||
|
LICENSE_REQUEST = 1;
|
||||||
|
LICENSE = 2;
|
||||||
|
ERROR_RESPONSE = 3;
|
||||||
|
SERVICE_CERTIFICATE_REQUEST = 4;
|
||||||
|
SERVICE_CERTIFICATE = 5;
|
||||||
|
}
|
||||||
|
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
|
||||||
|
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
|
||||||
|
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
|
||||||
|
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
|
||||||
|
optional bytes SessionKey = 4; // often RSA wrapped for licenses
|
||||||
|
optional RemoteAttestation RemoteAttestation = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hack
|
||||||
|
message SignedLicenseRequestRaw {
|
||||||
|
enum MessageType {
|
||||||
|
LICENSE_REQUEST = 1;
|
||||||
|
LICENSE = 2;
|
||||||
|
ERROR_RESPONSE = 3;
|
||||||
|
SERVICE_CERTIFICATE_REQUEST = 4;
|
||||||
|
SERVICE_CERTIFICATE = 5;
|
||||||
|
}
|
||||||
|
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
|
||||||
|
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
|
||||||
|
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
|
||||||
|
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
|
||||||
|
optional bytes SessionKey = 4; // often RSA wrapped for licenses
|
||||||
|
optional RemoteAttestation RemoteAttestation = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message SignedLicense {
|
||||||
|
enum MessageType {
|
||||||
|
LICENSE_REQUEST = 1;
|
||||||
|
LICENSE = 2;
|
||||||
|
ERROR_RESPONSE = 3;
|
||||||
|
SERVICE_CERTIFICATE_REQUEST = 4;
|
||||||
|
SERVICE_CERTIFICATE = 5;
|
||||||
|
}
|
||||||
|
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
|
||||||
|
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
|
||||||
|
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
|
||||||
|
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
|
||||||
|
optional bytes SessionKey = 4; // often RSA wrapped for licenses
|
||||||
|
optional RemoteAttestation RemoteAttestation = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignedServiceCertificate {
|
||||||
|
enum MessageType {
|
||||||
|
LICENSE_REQUEST = 1;
|
||||||
|
LICENSE = 2;
|
||||||
|
ERROR_RESPONSE = 3;
|
||||||
|
SERVICE_CERTIFICATE_REQUEST = 4;
|
||||||
|
SERVICE_CERTIFICATE = 5;
|
||||||
|
}
|
||||||
|
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
|
||||||
|
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
|
||||||
|
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
|
||||||
|
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
|
||||||
|
optional bytes SessionKey = 4; // often RSA wrapped for licenses
|
||||||
|
optional RemoteAttestation RemoteAttestation = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
//vmp support
|
||||||
|
message FileHashes {
|
||||||
|
message Signature {
|
||||||
|
optional string filename = 1;
|
||||||
|
optional bool test_signing = 2; //0 - release, 1 - testing
|
||||||
|
optional bytes SHA512Hash = 3;
|
||||||
|
optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
|
||||||
|
optional bytes signature = 5;
|
||||||
|
}
|
||||||
|
optional bytes signer = 1;
|
||||||
|
repeated Signature signatures = 2;
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReadHeadersFromJSON(r *http.Header) {
|
||||||
|
jsonBytesFromFile, err := ioutil.ReadFile("headers.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var jsonHeaderMap map[string]string
|
||||||
|
|
||||||
|
err = json.Unmarshal(jsonBytesFromFile, &jsonHeaderMap)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range jsonHeaderMap {
|
||||||
|
fmt.Printf("Setting HTTP Header \"%s : %s\"\n", k, v)
|
||||||
|
r.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue