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 }