Upload files to "/"
both drm and non drm shell logic widevine is there as cdm
This commit is contained in:
parent
c9e5e60bc5
commit
ad3688427e
|
|
@ -0,0 +1,229 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MPD struct {
|
||||||
|
XMLName xml.Name `xml:"MPD"`
|
||||||
|
PSSHs []string `xml:"Period>AdaptationSet>ContentProtection>PSSH"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDRMMPDPSSH(mpdURL, policy, signature, kvp string) (string, error) {
|
||||||
|
req, _ := http.NewRequest("GET", mpdURL, nil)
|
||||||
|
req.Header.Set("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||||
|
req.Header.Set("Cookie", fmt.Sprintf("CloudFront-Policy=%s; CloudFront-Signature=%s; CloudFront-Key-Pair-Id=%s", policy, signature, kvp))
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
xmlData, _ := io.ReadAll(resp.Body)
|
||||||
|
var mpd MPD
|
||||||
|
if err := xml.Unmarshal(xmlData, &mpd); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(mpd.PSSHs) == 0 {
|
||||||
|
return "", errors.New("PSSH not found")
|
||||||
|
}
|
||||||
|
return mpd.PSSHs[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDRMMPDLastModified(mpdURL, policy, signature, kvp string) (string, error) {
|
||||||
|
req, _ := http.NewRequest("HEAD", mpdURL, nil)
|
||||||
|
req.Header.Set("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||||
|
req.Header.Set("Cookie", fmt.Sprintf("CloudFront-Policy=%s; CloudFront-Signature=%s; CloudFront-Key-Pair-Id=%s", policy, signature, kvp))
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
lastModified := resp.Header.Get("Last-Modified")
|
||||||
|
return lastModified, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDecryptionKeyOFDL(drmHeaders map[string]string, licenseURL, pssh string) (string, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
headersJSON, _ := json.Marshal(drmHeaders)
|
||||||
|
|
||||||
|
form.Set("license_url", licenseURL)
|
||||||
|
form.Set("headers", string(headersJSON))
|
||||||
|
form.Set("pssh", pssh)
|
||||||
|
form.Set("build_identifier", "windows_software_widevinecdm_win_x86_64")
|
||||||
|
form.Set("proxy", "")
|
||||||
|
|
||||||
|
resp, err := http.PostForm("", form)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Keys []struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
} `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(result.Keys) == 0 {
|
||||||
|
return "", errors.New("no keys returned")
|
||||||
|
}
|
||||||
|
return result.Keys[0].Key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⬇ Replace with your own CDM client logic
|
||||||
|
func GetDecryptionKeyCDM(drmHeaders map[string]string, licenseURL, pssh string) (string, error) {
|
||||||
|
// This is a stub. Replace with your CDM library logic.
|
||||||
|
// Example:
|
||||||
|
// return widevine.GetContentKey(licenseURL, drmHeaders, pssh)
|
||||||
|
return "", errors.New("local CDM support not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadPurchasedMessageDRMVideo(mpdURL, decryptionKey, outputDir, filename string) error {
|
||||||
|
outPath := filepath.Join(outputDir, filename)
|
||||||
|
if fileExists(outPath) {
|
||||||
|
fmt.Println("Already downloaded:", outPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("shaka-packager",
|
||||||
|
fmt.Sprintf("input=%s,stream=video,output=%s", mpdURL, outPath),
|
||||||
|
fmt.Sprintf("--keys=key_id=00000000000000000000000000000000:key=%s", decryptionKey),
|
||||||
|
"--allow-multiple-key-ids",
|
||||||
|
"--quiet",
|
||||||
|
)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadPurchasedMedia(url, outputDir, filename string) error {
|
||||||
|
outPath := filepath.Join(outputDir, filename)
|
||||||
|
if fileExists(outPath) {
|
||||||
|
fmt.Println("Already downloaded:", outPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(outPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
fmt.Println("Downloading:", outPath)
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧠 Smart filename formatting
|
||||||
|
func GenerateFilename(mediaID, userID, msgID string, isDRM bool) string {
|
||||||
|
if isDRM {
|
||||||
|
return fmt.Sprintf("drm_%s_%s_%s.mp4", userID, msgID, mediaID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("media_%s_%s_%s.mp4", userID, msgID, mediaID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧠 Dummy auth header generator
|
||||||
|
func GetDynamicHeaders(endpoint, query string) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"User-Agent": "Mozilla/5.0",
|
||||||
|
"Cookie": "sess=your_cookie_here",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Example data — Replace with real input
|
||||||
|
paidMessageValue := "https://cdn3.onlyfans.com/dash/files,..."
|
||||||
|
paidMessageKey := "mediaId123"
|
||||||
|
outputDir := "./downloads"
|
||||||
|
deviceFolder := "./device"
|
||||||
|
deviceName := "default"
|
||||||
|
hasSelectedUsers := true
|
||||||
|
|
||||||
|
clientIdBlobMissing := !fileExists(fmt.Sprintf("%s/%s/device_client_id_blob", deviceFolder, deviceName))
|
||||||
|
devicePrivateKeyMissing := !fileExists(fmt.Sprintf("%s/%s/device_private_key", deviceFolder, deviceName))
|
||||||
|
|
||||||
|
err := os.MkdirAll(outputDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(paidMessageValue, "cdn3.onlyfans.com/dash/files") {
|
||||||
|
parts := strings.Split(paidMessageValue, ",")
|
||||||
|
if len(parts) < 6 {
|
||||||
|
fmt.Println("Invalid media info string.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mpdURL := parts[0]
|
||||||
|
policy := parts[1]
|
||||||
|
signature := parts[2]
|
||||||
|
kvp := parts[3]
|
||||||
|
mediaID := parts[4]
|
||||||
|
messageID := parts[5]
|
||||||
|
userID := "userXYZ" // Replace with actual username
|
||||||
|
|
||||||
|
pssh, err := GetDRMMPDPSSH(mpdURL, policy, signature, kvp)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to get PSSH:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
drmHeaders := GetDynamicHeaders(fmt.Sprintf("/api2/v2/users/media/%s/drm/message/%s", mediaID, messageID), "?type=widevine")
|
||||||
|
|
||||||
|
var decryptionKey string
|
||||||
|
if clientIdBlobMissing || devicePrivateKeyMissing {
|
||||||
|
decryptionKey, err = GetDecryptionKeyOFDL(drmHeaders, fmt.Sprintf("https://onlyfans.com/api2/v2/users/media/%s/drm/message/%s?type=widevine", mediaID, messageID), pssh)
|
||||||
|
} else {
|
||||||
|
decryptionKey, err = GetDecryptionKeyCDM(drmHeaders, fmt.Sprintf("https://onlyfans.com/api2/v2/users/media/%s/drm/message/%s?type=widevine", mediaID, messageID), pssh)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to get decryption key:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := GenerateFilename(mediaID, userID, messageID, true)
|
||||||
|
err = DownloadPurchasedMessageDRMVideo(mpdURL, decryptionKey, outputDir, filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to download DRM video:", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userID := "userXYZ"
|
||||||
|
msgID := "msg456"
|
||||||
|
filename := GenerateFilename(paidMessageKey, userID, msgID, false)
|
||||||
|
err := DownloadPurchasedMedia(paidMessageValue, outputDir, filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to download non-DRM video:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,719 @@
|
||||||
|
package widevine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const (
|
||||||
|
DeviceName = "chrome_1610"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CDM represents the Content Decryption Module
|
||||||
|
type CDM struct {
|
||||||
|
Devices map[string]*CDMDevice
|
||||||
|
Sessions map[string]*Session
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCDM creates a new CDM instance
|
||||||
|
func NewCDM() *CDM {
|
||||||
|
return &CDM{
|
||||||
|
Devices: map[string]*CDMDevice{
|
||||||
|
DeviceName: NewCDMDevice(DeviceName, nil, nil, nil),
|
||||||
|
},
|
||||||
|
Sessions: make(map[string]*Session),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPSSH validates and processes PSSH data
|
||||||
|
func (cdm *CDM) CheckPSSH(psshB64 string) ([]byte, error) {
|
||||||
|
systemID := []byte{237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237}
|
||||||
|
|
||||||
|
// Pad the base64 string if needed
|
||||||
|
if len(psshB64)%4 != 0 {
|
||||||
|
psshB64 += strings.Repeat("=", 4-(len(psshB64)%4))
|
||||||
|
}
|
||||||
|
|
||||||
|
pssh, err := base64.StdEncoding.DecodeString(psshB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pssh) < 30 {
|
||||||
|
return pssh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(pssh[12:28], systemID) {
|
||||||
|
newPssh := []byte{0, 0, 0}
|
||||||
|
newPssh = append(newPssh, byte(32+len(pssh)))
|
||||||
|
newPssh = append(newPssh, []byte("pssh")...)
|
||||||
|
newPssh = append(newPssh, []byte{0, 0, 0, 0}...)
|
||||||
|
newPssh = append(newPssh, systemID...)
|
||||||
|
newPssh = append(newPssh, []byte{0, 0, 0, 0}...)
|
||||||
|
newPssh = append(newPssh, byte(len(pssh)))
|
||||||
|
newPssh = append(newPssh, pssh...)
|
||||||
|
return newPssh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pssh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSession opens a new DRM session
|
||||||
|
func (cdm *CDM) OpenSession(initDataB64, deviceName string, offline, raw bool) (string, error) {
|
||||||
|
initData, err := cdm.CheckPSSH(initDataB64)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
device, ok := cdm.Devices[deviceName]
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("device not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionID []byte
|
||||||
|
if device.IsAndroid {
|
||||||
|
randHex := ""
|
||||||
|
choices := "ABCDEF0123456789"
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
randHex += string(choices[randInt(len(choices))])
|
||||||
|
}
|
||||||
|
counter := "01"
|
||||||
|
rest := "00000000000000"
|
||||||
|
sessionID = []byte(randHex + counter + rest)
|
||||||
|
} else {
|
||||||
|
sessionID = make([]byte, 16)
|
||||||
|
if _, err := rand.Read(sessionID); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var session *Session
|
||||||
|
parsedInitData, err := cdm.ParseInitData(initData)
|
||||||
|
if err == nil {
|
||||||
|
session = NewSession(sessionID, parsedInitData, device, offline)
|
||||||
|
} else if raw {
|
||||||
|
session = NewSession(sessionID, initData, device, offline)
|
||||||
|
} else {
|
||||||
|
return "", errors.New("unable to parse init data")
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionIDHex := hex.EncodeToString(sessionID)
|
||||||
|
cdm.Sessions[sessionIDHex] = session
|
||||||
|
|
||||||
|
return sessionIDHex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseInitData parses the initialization data
|
||||||
|
func (cdm *CDM) ParseInitData(initData []byte) (*WidevineCencHeader, error) {
|
||||||
|
var cencHeader WidevineCencHeader
|
||||||
|
|
||||||
|
if len(initData) < 32 {
|
||||||
|
return nil, errors.New("init data too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := proto.Unmarshal(initData[32:], &cencHeader)
|
||||||
|
if err == nil {
|
||||||
|
return &cencHeader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try HBO Max format
|
||||||
|
psshBox, err := ParsePSSHBox(initData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("unable to parse init data format")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = proto.Unmarshal(psshBox.Data, &cencHeader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("unable to parse init data format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cencHeader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSession closes a DRM session
|
||||||
|
func (cdm *CDM) CloseSession(sessionID string) bool {
|
||||||
|
if _, exists := cdm.Sessions[sessionID]; exists {
|
||||||
|
delete(cdm.Sessions, sessionID)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetServiceCertificate sets the service certificate for a session
|
||||||
|
func (cdm *CDM) SetServiceCertificate(sessionID string, certData []byte) bool {
|
||||||
|
session, exists := cdm.Sessions[sessionID]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var signedMessage SignedMessage
|
||||||
|
if err := proto.Unmarshal(certData, &signedMessage); err == nil {
|
||||||
|
var serviceCert SignedDeviceCertificate
|
||||||
|
if err := proto.Unmarshal(signedMessage.Msg, &serviceCert); err == nil {
|
||||||
|
session.ServiceCertificate = &serviceCert
|
||||||
|
session.PrivacyMode = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceCert SignedDeviceCertificate
|
||||||
|
if err := proto.Unmarshal(certData, &serviceCert); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
session.ServiceCertificate = &serviceCert
|
||||||
|
session.PrivacyMode = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLicenseRequest generates a license request
|
||||||
|
func (cdm *CDM) GetLicenseRequest(sessionID string) ([]byte, error) {
|
||||||
|
session, exists := cdm.Sessions[sessionID]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("session ID doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
var licenseRequest proto.Message
|
||||||
|
|
||||||
|
switch initData := session.InitData.(type) {
|
||||||
|
case *WidevineCencHeader:
|
||||||
|
licenseRequest = &SignedLicenseRequest{
|
||||||
|
Type: SignedLicenseRequest_LICENSE_REQUEST.Enum(),
|
||||||
|
Msg: &LicenseRequest{
|
||||||
|
Type: LicenseRequest_NEW.Enum(),
|
||||||
|
KeyControlNonce: proto.Uint32(1093602366),
|
||||||
|
ProtocolVersion: ProtocolVersion_CURRENT.Enum(),
|
||||||
|
ContentId: &LicenseRequest_ContentIdentification{
|
||||||
|
CencId: &LicenseRequest_ContentIdentification_Cenc{
|
||||||
|
LicenseType: func() *LicenseType {
|
||||||
|
if session.Offline {
|
||||||
|
return LicenseType_OFFLINE.Enum()
|
||||||
|
}
|
||||||
|
return LicenseType_DEFAULT.Enum()
|
||||||
|
}(),
|
||||||
|
RequestId: session.SessionId,
|
||||||
|
Pssh: initData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
licenseRequest = &SignedLicenseRequestRaw{
|
||||||
|
Type: SignedLicenseRequestRaw_LICENSE_REQUEST.Enum(),
|
||||||
|
Msg: &LicenseRequestRaw{
|
||||||
|
Type: LicenseRequestRaw_NEW.Enum(),
|
||||||
|
KeyControlNonce: proto.Uint32(1093602366),
|
||||||
|
ProtocolVersion: ProtocolVersion_CURRENT.Enum(),
|
||||||
|
ContentId: &LicenseRequestRaw_ContentIdentification{
|
||||||
|
CencId: &LicenseRequestRaw_ContentIdentification_Cenc{
|
||||||
|
LicenseType: func() *LicenseType {
|
||||||
|
if session.Offline {
|
||||||
|
return LicenseType_OFFLINE.Enum()
|
||||||
|
}
|
||||||
|
return LicenseType_DEFAULT.Enum()
|
||||||
|
}(),
|
||||||
|
RequestId: session.SessionId,
|
||||||
|
Pssh: initData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported init data type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.PrivacyMode && session.ServiceCertificate != nil {
|
||||||
|
encryptedClientID, err := cdm.encryptClientID(session)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch lr := licenseRequest.(type) {
|
||||||
|
case *SignedLicenseRequest:
|
||||||
|
lr.Msg.EncryptedClientId = encryptedClientID
|
||||||
|
case *SignedLicenseRequestRaw:
|
||||||
|
lr.Msg.EncryptedClientId = encryptedClientID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch lr := licenseRequest.(type) {
|
||||||
|
case *SignedLicenseRequest:
|
||||||
|
lr.Msg.ClientId = session.Device.ClientID
|
||||||
|
case *SignedLicenseRequestRaw:
|
||||||
|
lr.Msg.ClientId = session.Device.ClientID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the message to sign it
|
||||||
|
var msgBytes []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch lr := licenseRequest.(type) {
|
||||||
|
case *SignedLicenseRequest:
|
||||||
|
msgBytes, err = proto.Marshal(lr.Msg)
|
||||||
|
case *SignedLicenseRequestRaw:
|
||||||
|
msgBytes, err = proto.Marshal(lr.Msg)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
session.LicenseRequest = msgBytes
|
||||||
|
|
||||||
|
// Sign the license request
|
||||||
|
signature, err := session.Device.Sign(msgBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch lr := licenseRequest.(type) {
|
||||||
|
case *SignedLicenseRequest:
|
||||||
|
lr.Signature = signature
|
||||||
|
case *SignedLicenseRequestRaw:
|
||||||
|
lr.Signature = signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the complete license request
|
||||||
|
requestBytes, err := proto.Marshal(licenseRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryptClientID encrypts the client ID for privacy mode
|
||||||
|
func (cdm *CDM) encryptClientID(session *Session) (*EncryptedClientIdentification, error) {
|
||||||
|
clientIDBytes, err := proto.Marshal(session.Device.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add PKCS7 padding
|
||||||
|
blockSize := 16
|
||||||
|
padding := blockSize - (len(clientIDBytes) % blockSize
|
||||||
|
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
clientIDBytes = append(clientIDBytes, padText...)
|
||||||
|
|
||||||
|
// Generate AES key and IV
|
||||||
|
aesKey := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(aesKey); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := make([]byte, aes.BlockSize)
|
||||||
|
if _, err := rand.Read(iv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the client ID
|
||||||
|
block, err := aes.NewCipher(aesKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
encryptedClientID := make([]byte, len(clientIDBytes))
|
||||||
|
mode.CryptBlocks(encryptedClientID, clientIDBytes)
|
||||||
|
|
||||||
|
// Encrypt the AES key with RSA
|
||||||
|
pubKey, err := parseRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedKey, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, pubKey, aesKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EncryptedClientIdentification{
|
||||||
|
ServiceId: string(session.ServiceCertificate.DeviceCertificate.ServiceId),
|
||||||
|
ServiceCertificateSerialNumber: session.ServiceCertificate.DeviceCertificate.SerialNumber,
|
||||||
|
EncryptedClientId: encryptedClientID,
|
||||||
|
EncryptedClientIdIv: iv,
|
||||||
|
EncryptedPrivacyKey: encryptedKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseRSAPublicKey parses an RSA public key from bytes
|
||||||
|
func parseRSAPublicKey(keyBytes []byte) (*rsa.PublicKey, error) {
|
||||||
|
// This is a simplified version - actual implementation depends on the key format
|
||||||
|
// In a real implementation, you would properly parse the ASN.1 encoded key
|
||||||
|
if len(keyBytes) < 12 {
|
||||||
|
return nil, errors.New("invalid key length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming the key is in a simple format for this example
|
||||||
|
// In reality, you'd use x509.ParsePKIXPublicKey or similar
|
||||||
|
n := new(big.Int)
|
||||||
|
n.SetBytes(keyBytes[8:]) // Skip some header bytes
|
||||||
|
return &rsa.PublicKey{
|
||||||
|
N: n,
|
||||||
|
E: 65537, // Common exponent
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideLicense processes the license response
|
||||||
|
func (cdm *CDM) ProvideLicense(sessionID string, license []byte) error {
|
||||||
|
session, exists := cdm.Sessions[sessionID]
|
||||||
|
if !exists {
|
||||||
|
return errors.New("session ID doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.LicenseRequest == nil {
|
||||||
|
return errors.New("generate a license request first")
|
||||||
|
}
|
||||||
|
|
||||||
|
var signedLicense SignedLicense
|
||||||
|
if err := proto.Unmarshal(license, &signedLicense); err != nil {
|
||||||
|
return errors.New("unable to parse license")
|
||||||
|
}
|
||||||
|
|
||||||
|
session.License = &signedLicense
|
||||||
|
|
||||||
|
// Decrypt the session key
|
||||||
|
sessionKey, err := session.Device.Decrypt(session.License.SessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("unable to decrypt session key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sessionKey) != 16 {
|
||||||
|
return errors.New("invalid session key length")
|
||||||
|
}
|
||||||
|
|
||||||
|
session.SessionKey = sessionKey
|
||||||
|
|
||||||
|
// Derive keys
|
||||||
|
session.DerivedKeys = cdm.DeriveKeys(session.LicenseRequest, session.SessionKey)
|
||||||
|
|
||||||
|
// Verify license signature
|
||||||
|
licenseBytes, err := proto.Marshal(signedLicense.Msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hmacHash := getHMACSHA256Digest(licenseBytes, session.DerivedKeys.Auth1)
|
||||||
|
if !hmac.Equal(hmacHash, signedLicense.Signature) {
|
||||||
|
return errors.New("license signature mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt content keys
|
||||||
|
for _, key := range signedLicense.Msg.Keys {
|
||||||
|
if key.GetType() == License_KeyContainer_SIGNING {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keyID := key.Id
|
||||||
|
if keyID == nil {
|
||||||
|
keyID = []byte(key.GetType().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedKey, err := decryptKey(key.Key, key.Iv, session.DerivedKeys.Enc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentKey := &ContentKey{
|
||||||
|
KeyID: keyID,
|
||||||
|
Type: key.GetType().String(),
|
||||||
|
Bytes: decryptedKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.GetType() == License_KeyContainer_OPERATOR_SESSION {
|
||||||
|
// Handle permissions if this is an operator session key
|
||||||
|
// (Implementation depends on your specific needs)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.ContentKeys = append(session.ContentKeys, contentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptKey decrypts a content key using AES-CBC
|
||||||
|
func decryptKey(encryptedKey, iv, key []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(iv) != aes.BlockSize {
|
||||||
|
return nil, errors.New("invalid IV length")
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
decryptedKey := make([]byte, len(encryptedKey))
|
||||||
|
mode.CryptBlocks(decryptedKey, encryptedKey)
|
||||||
|
|
||||||
|
// Remove PKCS7 padding
|
||||||
|
padding := int(decryptedKey[len(decryptedKey)-1])
|
||||||
|
if padding > aes.BlockSize || padding == 0 {
|
||||||
|
return nil, errors.New("invalid padding")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(decryptedKey) - padding; i < len(decryptedKey); i++ {
|
||||||
|
if int(decryptedKey[i]) != padding {
|
||||||
|
return nil, errors.New("invalid padding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedKey[:len(decryptedKey)-padding], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveKeys derives encryption keys from the session key
|
||||||
|
func (cdm *CDM) DeriveKeys(message, key []byte) *DerivedKeys {
|
||||||
|
encKeyBase := append([]byte("ENCRYPTION\x00"), message...)
|
||||||
|
encKeyBase = append(encKeyBase, []byte{0x00, 0x00, 0x00, 0x80}...)
|
||||||
|
|
||||||
|
authKeyBase := append([]byte("AUTHENTICATION\x00"), message...)
|
||||||
|
authKeyBase = append(authKeyBase, []byte{0x00, 0x00, 0x02, 0x00}...)
|
||||||
|
|
||||||
|
encKey := append([]byte{0x01}, encKeyBase...)
|
||||||
|
authKey1 := append([]byte{0x01}, authKeyBase...)
|
||||||
|
authKey2 := append([]byte{0x02}, authKeyBase...)
|
||||||
|
authKey3 := append([]byte{0x03}, authKeyBase...)
|
||||||
|
authKey4 := append([]byte{0x04}, authKeyBase...)
|
||||||
|
|
||||||
|
encCmacKey := getCMACDigest(encKey, key)
|
||||||
|
authCmacKey1 := getCMACDigest(authKey1, key)
|
||||||
|
authCmacKey2 := getCMACDigest(authKey2, key)
|
||||||
|
authCmacKey3 := getCMACDigest(authKey3, key)
|
||||||
|
authCmacKey4 := getCMACDigest(authKey4, key)
|
||||||
|
|
||||||
|
authCombined1 := append(authCmacKey1, authCmacKey2...)
|
||||||
|
authCombined2 := append(authCmacKey3, authCmacKey4...)
|
||||||
|
|
||||||
|
return &DerivedKeys{
|
||||||
|
Auth1: authCombined1,
|
||||||
|
Auth2: authCombined2,
|
||||||
|
Enc: encCmacKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeys returns the content keys for a session
|
||||||
|
func (cdm *CDM) GetKeys(sessionID string) ([]*ContentKey, error) {
|
||||||
|
session, exists := cdm.Sessions[sessionID]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("session not found")
|
||||||
|
}
|
||||||
|
return session.ContentKeys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHMACSHA256Digest computes HMAC-SHA256
|
||||||
|
func getHMACSHA256Digest(data, key []byte) []byte {
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write(data)
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCMACDigest computes CMAC (using AES-CBC as a simplified stand-in)
|
||||||
|
func getCMACDigest(data, key []byte) []byte {
|
||||||
|
// Note: This is a simplified version. Real CMAC is more complex.
|
||||||
|
// For a proper implementation, you'd need a dedicated CMAC implementation.
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := cipher.NewCBCEncrypter(block, make([]byte, aes.BlockSize))
|
||||||
|
paddedData := padData(data, aes.BlockSize)
|
||||||
|
result := make([]byte, len(paddedData))
|
||||||
|
mode.CryptBlocks(result, paddedData)
|
||||||
|
|
||||||
|
// Return the last block as the CMAC
|
||||||
|
return result[len(result)-aes.BlockSize:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// padData pads data to the block size
|
||||||
|
func padData(data []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - (len(data) % blockSize)
|
||||||
|
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(data, padText...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// randInt generates a random integer up to max
|
||||||
|
func randInt(max int) int {
|
||||||
|
n, _ := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||||
|
return int(n.Int64())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CDMDevice represents a CDM device
|
||||||
|
type CDMDevice struct {
|
||||||
|
DeviceName string
|
||||||
|
ClientID *ClientIdentification
|
||||||
|
DeviceKeys *rsa.PrivateKey
|
||||||
|
IsAndroid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCDMDevice creates a new CDM device
|
||||||
|
func NewCDMDevice(deviceName string, clientIDBytes, privateKeyBytes, vmpBytes []byte) *CDMDevice {
|
||||||
|
// In a real implementation, you would load these from files
|
||||||
|
// as shown in the C# code, but we'll simplify for this example
|
||||||
|
|
||||||
|
clientID := &ClientIdentification{
|
||||||
|
Type: ClientIdentification_KEYBOX.Enum(),
|
||||||
|
// Other fields would be initialized here
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKey *rsa.PrivateKey
|
||||||
|
// Parse private key from bytes if provided
|
||||||
|
|
||||||
|
return &CDMDevice{
|
||||||
|
DeviceName: deviceName,
|
||||||
|
ClientID: clientID,
|
||||||
|
DeviceKeys: privateKey,
|
||||||
|
IsAndroid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data using the device's private key
|
||||||
|
func (d *CDMDevice) Decrypt(data []byte) ([]byte, error) {
|
||||||
|
if d.DeviceKeys == nil {
|
||||||
|
return nil, errors.New("no device keys available")
|
||||||
|
}
|
||||||
|
|
||||||
|
blockSize := d.DeviceKeys.Size()
|
||||||
|
var plaintext []byte
|
||||||
|
|
||||||
|
for i := 0; i < len(data); i += blockSize {
|
||||||
|
end := i + blockSize
|
||||||
|
if end > len(data) {
|
||||||
|
end = len(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, d.DeviceKeys, data[i:end], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext = append(plaintext, chunk...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs data using the device's private key
|
||||||
|
func (d *CDMDevice) Sign(data []byte) ([]byte, error) {
|
||||||
|
if d.DeviceKeys == nil {
|
||||||
|
return nil, errors.New("no device keys available")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := sha1.New()
|
||||||
|
hasher.Write(data)
|
||||||
|
hash := hasher.Sum(nil)
|
||||||
|
|
||||||
|
return rsa.SignPSS(rand.Reader, d.DeviceKeys, crypto.SHA1, hash, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session represents a DRM session
|
||||||
|
type Session struct {
|
||||||
|
SessionId []byte
|
||||||
|
InitData interface{} // *WidevineCencHeader or []byte
|
||||||
|
Offline bool
|
||||||
|
Device *CDMDevice
|
||||||
|
SessionKey []byte
|
||||||
|
DerivedKeys *DerivedKeys
|
||||||
|
LicenseRequest []byte
|
||||||
|
License *SignedLicense
|
||||||
|
ServiceCertificate *SignedDeviceCertificate
|
||||||
|
PrivacyMode bool
|
||||||
|
ContentKeys []*ContentKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSession creates a new session
|
||||||
|
func NewSession(sessionID []byte, initData interface{}, device *CDMDevice, offline bool) *Session {
|
||||||
|
return &Session{
|
||||||
|
SessionId: sessionID,
|
||||||
|
InitData: initData,
|
||||||
|
Offline: offline,
|
||||||
|
Device: device,
|
||||||
|
ContentKeys: make([]*ContentKey, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DerivedKeys contains the derived encryption keys
|
||||||
|
type DerivedKeys struct {
|
||||||
|
Auth1 []byte
|
||||||
|
Auth2 []byte
|
||||||
|
Enc []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentKey represents a decrypted content key
|
||||||
|
type ContentKey struct {
|
||||||
|
KeyID []byte
|
||||||
|
Type string
|
||||||
|
Bytes []byte
|
||||||
|
Permissions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSSHBox represents a PSSH box
|
||||||
|
type PSSHBox struct {
|
||||||
|
KIDs [][]byte
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePSSHBox parses a PSSH box from bytes
|
||||||
|
func ParsePSSHBox(data []byte) (*PSSHBox, error) {
|
||||||
|
if len(data) < 32 {
|
||||||
|
return nil, errors.New("PSSH box too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the first 20 bytes (box size and type)
|
||||||
|
stream := bytes.NewReader(data)
|
||||||
|
stream.Seek(20, io.SeekStart)
|
||||||
|
|
||||||
|
// Read KID count
|
||||||
|
kidCountBytes := make([]byte, 4)
|
||||||
|
if _, err := stream.Read(kidCountBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kidCount := binary.BigEndian.Uint32(kidCountBytes)
|
||||||
|
kids := make([][]byte, 0, kidCount)
|
||||||
|
|
||||||
|
for i := 0; i < int(kidCount); i++ {
|
||||||
|
kid := make([]byte, 16)
|
||||||
|
if _, err := stream.Read(kid); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kids = append(kids, kid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data length
|
||||||
|
dataLenBytes := make([]byte, 4)
|
||||||
|
if _, err := stream.Read(dataLenBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataLen := binary.BigEndian.Uint32(dataLenBytes)
|
||||||
|
if dataLen == 0 {
|
||||||
|
return &PSSHBox{KIDs: kids}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data
|
||||||
|
psshData := make([]byte, dataLen)
|
||||||
|
if _, err := stream.Read(psshData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PSSHBox{
|
||||||
|
KIDs: kids,
|
||||||
|
Data: psshData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue