From ad3688427e146831a902719303c8dbbf519bec55 Mon Sep 17 00:00:00 2001 From: User24kld Date: Wed, 21 May 2025 15:24:07 +0000 Subject: [PATCH] Upload files to "/" both drm and non drm shell logic widevine is there as cdm --- ofdl dl.go | 229 +++++++++++++++++ widevine.go | 719 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 948 insertions(+) create mode 100644 ofdl dl.go create mode 100644 widevine.go diff --git a/ofdl dl.go b/ofdl dl.go new file mode 100644 index 0000000..18b415c --- /dev/null +++ b/ofdl dl.go @@ -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) + } + } +} diff --git a/widevine.go b/widevine.go new file mode 100644 index 0000000..9b7c12a --- /dev/null +++ b/widevine.go @@ -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 +} \ No newline at end of file