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