// Copyright 2013 Google Inc. All Rights Reserved. #include "cdm_engine.h" #include #include #include "buffer_reader.h" #include "cdm_session.h" #include "crypto_engine.h" #include "device_files.h" #include "license_protocol.pb.h" #include "log.h" #include "properties.h" #include "string_conversions.h" #include "wv_cdm_constants.h" #include "wv_cdm_event_listener.h" #ifndef CDM_POLICY_TIMER_DURATION_SECONDS #define CDM_POLICY_TIMER_DURATION_SECONDS 1 #endif namespace { const std::string kDefaultProvisioningServerUrl = "https://www.googleapis.com/" "certificateprovisioning/v1/devicecertificates/create" "?key=AIzaSyB-5OLKTx2iU5mko18DfdwK5611JIjbUhE"; } namespace wvcdm { // Protobuf generated classes. using video_widevine_server::sdk::ClientIdentification; using video_widevine_server::sdk::ProvisioningRequest; using video_widevine_server::sdk::ProvisioningResponse; using video_widevine_server::sdk::SignedProvisioningMessage; typedef std::map::const_iterator CdmSessionIter; CdmEngine::CdmEngine() : provisioning_session_(NULL) { Properties::Init(); } CdmEngine::~CdmEngine() { CancelSessions(); CdmSessionMap::iterator i(sessions_.begin()); for (; i != sessions_.end(); ++i) delete i->second; sessions_.clear(); } CdmResponseType CdmEngine::OpenSession( const CdmKeySystem& key_system, CdmSessionId* session_id) { LOGI("CdmEngine::OpenSession"); if (!ValidateKeySystem(key_system)) { LOGI("CdmEngine::OpenSession: invalid key_system = %s", key_system.c_str()); return KEY_ERROR; } if (!session_id) { LOGE("CdmEngine::OpenSession: no session ID destination provided"); return KEY_ERROR; } // TODO(edwinwong, rfrias): Save key_system in session for validation checks CdmSession* new_session = new CdmSession(); if (!new_session) { return KEY_ERROR; } if (new_session->session_id().empty()) { LOGE("CdmEngine::OpenSession: failure to generate session ID"); delete(new_session); return UNKNOWN_ERROR; } CdmSessionId new_session_id = new_session->session_id(); CdmResponseType sts = new_session->Init(); if (sts != NO_ERROR) { LOGE("CdmEngine::OpenSession: bad session init"); delete(new_session); return sts; } sessions_[new_session_id] = new_session; *session_id = new_session_id; return NO_ERROR; } CdmResponseType CdmEngine::CloseSession(const CdmSessionId& session_id) { LOGI("CdmEngine::CloseSession"); CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::CloseSession: session not found = %s", session_id.c_str()); return KEY_ERROR; } sessions_.erase(session_id); if (sessions_.size() == 0) DisablePolicyTimer(); iter->second->DestroySession(); delete iter->second; return NO_ERROR; } CdmResponseType CdmEngine::GenerateKeyRequest( const CdmSessionId& session_id, bool is_key_system_present, const CdmKeySystem& key_system, const CdmInitData& init_data, const CdmLicenseType license_type, CdmAppParameterMap& app_parameters, CdmKeyMessage* key_request, std::string* server_url) { LOGI("CdmEngine::GenerateKeyRequest"); CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::GenerateKeyRequest: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } if (is_key_system_present) { // TODO(edwinwong, rfrias): validate key_system has not changed } if (!key_request) { LOGE("CdmEngine::GenerateKeyRequest: no key request destination provided"); return KEY_ERROR; } key_request->clear(); // TODO(edwinwong, rfrias): need to pass in license type and app parameters CdmResponseType sts = iter->second->GenerateKeyRequest(init_data, license_type, app_parameters, key_request, server_url); if (KEY_MESSAGE != sts) { LOGE("CdmEngine::GenerateKeyRequest: key request generation failed, sts=%d", (int)sts); return sts; } // TODO(edwinwong, rfrias): persist init_data, license_type, app_parameters // in session return KEY_MESSAGE; } CdmResponseType CdmEngine::AddKey( const CdmSessionId& session_id, const CdmKeyResponse& key_data) { LOGI("CdmEngine::AddKey"); CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::AddKey: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } if (key_data.empty()) { LOGE("CdmEngine::AddKey: no key_data"); return KEY_ERROR; } CdmResponseType sts = iter->second->AddKey(key_data); if (KEY_ADDED != sts) { LOGE("CdmEngine::AddKey: keys not added, result = %d", (int)sts); } EnablePolicyTimer(); return sts; } CdmResponseType CdmEngine::CancelKeyRequest( const CdmSessionId& session_id, bool is_key_system_present, const CdmKeySystem& key_system) { LOGI("CdmEngine::CancelKeyRequest"); //TODO(gmorgan): Issue: what is semantics of canceling a key request. Should //this call cancel all keys for the session? // TODO(jfore): We should disable the policy timer here if there are no // active sessions. Sessions are currently not being destroyed here. We can // add this logic once the semantics of canceling the key is worked out. CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::CancelKeyRequest: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } if (is_key_system_present) { // TODO(edwinwong, rfrias): validate key_system has not changed } // TODO(edwinwong, rfrias): unload keys here DisablePolicyTimer(); return NO_ERROR; } CdmResponseType CdmEngine::GenerateRenewalRequest( const CdmSessionId& session_id, CdmKeyMessage* key_request, std::string* server_url) { LOGI("CdmEngine::GenerateRenewalRequest"); CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::GenerateRenewalRequest: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } if (!key_request) { LOGE("CdmEngine::GenerateRenewalRequest: no key request destination provided"); return KEY_ERROR; } key_request->clear(); CdmResponseType sts = iter->second->GenerateRenewalRequest(key_request, server_url); if (KEY_MESSAGE != sts) { LOGE("CdmEngine::GenerateRenewalRequest: key request generation failed, sts=%d", (int)sts); return sts; } return KEY_MESSAGE; } CdmResponseType CdmEngine::RenewKey( const CdmSessionId& session_id, const CdmKeyResponse& key_data) { LOGI("CdmEngine::RenewKey"); CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::RenewKey: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } if (key_data.empty()) { LOGE("CdmEngine::RenewKey: no key_data"); return KEY_ERROR; } CdmResponseType sts = iter->second->RenewKey(key_data); if (KEY_ADDED != sts) { LOGE("CdmEngine::RenewKey: keys not added, sts=%d", (int)sts); return sts; } return KEY_ADDED; } CdmResponseType CdmEngine::QueryStatus(CdmQueryMap* key_info) { LOGI("CdmEngine::QueryStatus"); CryptoEngine* crypto_engine = CryptoEngine::GetInstance(); if (!crypto_engine) { return KEY_ERROR; } switch (crypto_engine->GetSecurityLevel()) { case CryptoEngine::kSecurityLevelL1: (*key_info)[QUERY_KEY_SECURITY_LEVEL] = QUERY_VALUE_SECURITY_LEVEL_L1; break; case CryptoEngine::kSecurityLevelL2: (*key_info)[QUERY_KEY_SECURITY_LEVEL] = QUERY_VALUE_SECURITY_LEVEL_L2; break; case CryptoEngine::kSecurityLevelL3: (*key_info)[QUERY_KEY_SECURITY_LEVEL] = QUERY_VALUE_SECURITY_LEVEL_L3; break; case CryptoEngine::kSecurityLevelUnknown: (*key_info)[QUERY_KEY_SECURITY_LEVEL] = QUERY_VALUE_SECURITY_LEVEL_Unknown; break; default: return KEY_ERROR; } std::string deviceId; bool success = crypto_engine->GetDeviceUniqueId(&deviceId); if (success) { (*key_info)[QUERY_KEY_DEVICE_ID] = deviceId; } uint32_t system_id; success = crypto_engine->GetSystemId(&system_id); if (success) { std::ostringstream system_id_stream; system_id_stream << system_id; (*key_info)[QUERY_KEY_SYSTEM_ID] = system_id_stream.str(); } return NO_ERROR; } CdmResponseType CdmEngine::QueryKeyStatus( const CdmSessionId& session_id, CdmQueryMap* key_info) { LOGI("CdmEngine::QueryKeyStatus"); CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::QueryKeyStatus: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } return iter->second->QueryKeyStatus(key_info); } CdmResponseType CdmEngine::QueryKeyControlInfo( const CdmSessionId& session_id, CdmQueryMap* key_info) { LOGI("CdmEngine::QueryKeyControlInfo"); CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGE("CdmEngine::QueryKeyControlInfo: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } return iter->second->QueryKeyControlInfo(key_info); } /* * The certificate provisioning process creates a cdm and a crypto session. * The lives of these sessions are short and therefore, not added to the * CdmSessionMap. We need to explicitly delete these objects when error occurs * or when we are done with provisioning. */ void CdmEngine::CleanupProvisioningSession() { if (provisioning_session_) { CryptoEngine* crypto_engine = CryptoEngine::GetInstance(); if (crypto_engine) { CdmSessionId cdm_session_id = provisioning_session_->session_id(); CryptoSession* crypto_session = crypto_engine->FindSession(cdm_session_id); if (crypto_session) { LOGV("delete crypto session for id=%s", cdm_session_id.c_str()); delete crypto_session; } else { LOGE("CleanupProvisioningSession: cannot find crypto_session"); } } delete provisioning_session_; provisioning_session_ = NULL; } } /* * This function converts SignedProvisioningRequest into base64 string. * It then wraps it in JSON format expected by the Apiary frontend. * Apiary requires the base64 encoding to replace '+' with minus '-', * and '/' with underscore '_'; opposite to stubby's. * * Returns the JSON formated string in *request. The JSON string will be * appended as a query parameter, i.e. signedRequest=. All base64 '=' padding chars must be removed. * * The JSON formated request takes the following format: * * base64 encoded message */ void CdmEngine::ComposeJsonRequestAsQueryString( const std::string& message, CdmProvisioningRequest* request) { // performs base64 encoding for message std::vector message_vector(message.begin(), message.end()); std::string message_b64 = Base64SafeEncode(message_vector); // removes trailing '=' padding characters; // the encoded string can have at most 2 '=' padding chars, so start // searching at the end minus four characters size_t found_pos = message_b64.find("=", message_b64.size() - 4); if (std::string::npos != found_pos) { message_b64.resize(found_pos); } request->assign(message_b64); } /* * Composes a device provisioning request and output the request in JSON format * in *request. It also returns the default url for the provisioning server * in *default_url. * * Returns NO_ERROR for success and UNKNOWN_ERROR if fails. */ CdmResponseType CdmEngine::GetProvisioningRequest( CdmProvisioningRequest* request, std::string* default_url) { if (!request || !default_url) { LOGE("GetProvisioningRequest: invalid input parameters"); return UNKNOWN_ERROR; } default_url->assign(kDefaultProvisioningServerUrl); if (provisioning_session_) { CleanupProvisioningSession(); } // //--------------------------------------------------------------------------- // This function can be called before a cdm session is created. // First creates a cdm session, then creates a crypto session. // CdmSession* cdm_session = new CdmSession(); if (!cdm_session) { LOGE("GetProvisioningRequest: fails to create a cdm session"); return UNKNOWN_ERROR; } if (cdm_session->session_id().empty()) { LOGE("GetProvisioningRequest: fails to generate session ID"); delete cdm_session; return UNKNOWN_ERROR; } CryptoEngine* crypto_engine = CryptoEngine::GetInstance(); if (!crypto_engine) { LOGE("GetProvisioningRequest: fails to create a crypto engine"); delete cdm_session; return UNKNOWN_ERROR; } CdmSessionId cdm_session_id = cdm_session->session_id(); CryptoSession* crypto_session = crypto_engine->CreateSession(cdm_session_id); if (!crypto_session) { LOGE("GetProvisioningRequest: fails to create a crypto session"); delete cdm_session; return UNKNOWN_ERROR; } // TODO(edwinwong): replace this cdm session pointer with crypto session // pointer if feasible provisioning_session_ = cdm_session; LOGV("provisioning session id=%s", cdm_session_id.c_str()); // //--------------------------------------------------------------------------- // Prepares device provisioning request. // ProvisioningRequest provisioning_request; ClientIdentification* client_id = provisioning_request.mutable_client_id(); client_id->set_type(ClientIdentification::KEYBOX); std::string token; if (!crypto_engine->GetToken(&token)) { LOGE("GetProvisioningRequest: fails to get token"); CleanupProvisioningSession(); return UNKNOWN_ERROR; } client_id->set_token(token); uint32_t nonce; if (!crypto_session->GenerateNonce(&nonce)) { LOGE("GetProvisioningRequest: fails to generate a nonce"); CleanupProvisioningSession(); return UNKNOWN_ERROR; } // The provisioning server does not convert the nonce to uint32_t, it just // passes the binary data to the response message. std::string the_nonce(reinterpret_cast(&nonce), sizeof(nonce)); provisioning_request.set_nonce(the_nonce); std::string serialized_message; provisioning_request.SerializeToString(&serialized_message); // Derives signing and encryption keys and constructs signature. std::string request_signature; if (!crypto_session->PrepareRequest(serialized_message, &request_signature, true)) { request->clear(); CleanupProvisioningSession(); return UNKNOWN_ERROR; } if (request_signature.empty()) { request->clear(); CleanupProvisioningSession(); return UNKNOWN_ERROR; } SignedProvisioningMessage signed_provisioning_msg; signed_provisioning_msg.set_message(serialized_message); signed_provisioning_msg.set_signature(request_signature); std::string serialized_request; signed_provisioning_msg.SerializeToString(&serialized_request); // converts request into JSON string ComposeJsonRequestAsQueryString(serialized_request, request); return NO_ERROR; } /* * Parses the input json_str and locates substring using start_substr and * end_stubstr. The found base64 substring is then decoded and returns * in *result. * * Returns true for success and false if fails. */ bool CdmEngine::ParseJsonResponse( const CdmProvisioningResponse& json_str, const std::string& start_substr, const std::string& end_substr, std::string* result) { std::string b64_string; size_t start = json_str.find(start_substr); if (start == json_str.npos) { LOGE("ParseJsonResponse: cannot find start substring"); return false; } else { size_t end = json_str.find(end_substr, start + start_substr.length()); if (end == json_str.npos) { LOGE("ParseJsonResponse cannot locate end substring"); return false; } size_t b64_string_size = end - start - start_substr.length(); b64_string.assign(json_str, start + start_substr.length(), b64_string_size); } // Decodes base64 substring and returns it in *result std::vector result_vector = Base64SafeDecode(b64_string); result->assign(result_vector.begin(), result_vector.end()); return true; } /* * The response message consists of a device certificate and the device RSA key. * The device RSA key is stored in the T.E.E. The device certificate is stored * in the device. * * Returns NO_ERROR for success and UNKNOWN_ERROR if fails. */ CdmResponseType CdmEngine::HandleProvisioningResponse( CdmProvisioningResponse& response) { if (response.empty()) { LOGE("Empty provisioning response."); return UNKNOWN_ERROR; } //--------------------------------------------------------------------------- // Extracts signed response from JSON string, decodes base64 signed response const std::string kMessageStart = "\"signedResponse\": \""; const std::string kMessageEnd = "\""; std::string serialized_signed_response; if (!ParseJsonResponse(response, kMessageStart, kMessageEnd, &serialized_signed_response)) { LOGE("Fails to extract signed serialized response from JSON response"); return UNKNOWN_ERROR; } // //--------------------------------------------------------------------------- // Creates a crypto session using provisioning_session_. // if (!provisioning_session_) { LOGE("HandleProvisioningResponse: invalid provisioning session"); return UNKNOWN_ERROR; } CryptoEngine* crypto_engine = CryptoEngine::GetInstance(); if (!crypto_engine) { LOGE("HandleProvisioningResponse: fails to create a crypto engine"); return UNKNOWN_ERROR; } CdmSessionId cdm_session_id = provisioning_session_->session_id(); CryptoSession* crypto_session = crypto_engine->FindSession(cdm_session_id); if (!crypto_session) { LOGE("HandleProvisioningResponse: fails to find %s", cdm_session_id.c_str()); return UNKNOWN_ERROR; } //--------------------------------------------------------------------------- // Authenticates provisioning response using D1s (server key derived from // the provisioing request's input). Validate provisioning response and // stores private device RSA key and certificate. SignedProvisioningMessage signed_response; if (!signed_response.ParseFromString(serialized_signed_response)) { LOGE("Fails to parse signed serialized response"); CleanupProvisioningSession(); return UNKNOWN_ERROR; } if (!signed_response.has_signature() || !signed_response.has_message()) { LOGE("Invalid response - signature or message not found"); CleanupProvisioningSession(); return UNKNOWN_ERROR; } const std::string& signed_message = signed_response.message(); ProvisioningResponse provisioning_response; if (!provisioning_response.ParseFromString(signed_message)) { LOGE("Fails to parse signed message"); CleanupProvisioningSession(); return UNKNOWN_ERROR; } if (!provisioning_response.has_device_rsa_key()) { LOGE("Invalid response - key not found"); CleanupProvisioningSession(); return UNKNOWN_ERROR; } const std::string& enc_rsa_key = provisioning_response.device_rsa_key(); const std::string& nonce = provisioning_response.nonce(); const std::string& rsa_key_iv = provisioning_response.device_rsa_key_iv(); const std::string& signature = signed_response.signature(); std::string wrapped_rsa_key; if (!crypto_session->RewrapDeviceRSAKey(signed_message, signature, nonce.data(), enc_rsa_key, enc_rsa_key.size(), rsa_key_iv, &wrapped_rsa_key)) { LOGE("HandleProvisioningResponse: RewrapDeviceRSAKey fails"); CleanupProvisioningSession(); return UNKNOWN_ERROR; } const std::string& device_certificate = provisioning_response.device_certificate(); DeviceFiles::StoreCertificate(device_certificate, wrapped_rsa_key); // //--------------------------------------------------------------------------- // Deletes cdm and crypto sessions created for provisioning. // CleanupProvisioningSession(); return NO_ERROR; } CdmResponseType CdmEngine::GetSecureStops( CdmSecureStops* secure_stops) { // TODO(edwinwong, rfrias): add implementation return NO_ERROR; } CdmResponseType CdmEngine::ReleaseSecureStops( const CdmSecureStopReleaseMessage& message) { // TODO(edwinwong, rfrias): add implementation return NO_ERROR; } CdmResponseType CdmEngine::Decrypt( const CdmSessionId& session_id, bool is_encrypted, bool is_secure, const KeyId& key_id, const uint8_t* encrypt_buffer, size_t encrypt_length, const std::vector& iv, size_t block_offset, void* decrypt_buffer, size_t decrypt_buffer_offset, bool is_video) { CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { LOGW("CdmEngine::Decrypt: session_id not found = %s", session_id.c_str()); return KEY_ERROR; } return iter->second->Decrypt(is_encrypted, is_secure, key_id, encrypt_buffer, encrypt_length, iv, block_offset, decrypt_buffer, decrypt_buffer_offset, is_video); } bool CdmEngine::IsKeyValid(const KeyId& key_id) { for (CdmSessionIter iter = sessions_.begin(); iter != sessions_.end(); ++iter) { if (iter->second->IsKeyValid(key_id)) { return true; } } return false; } bool CdmEngine::AttachEventListener( const CdmSessionId& session_id, WvCdmEventListener* listener) { CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { return false; } return iter->second->AttachEventListener(listener); } bool CdmEngine::DetachEventListener( const CdmSessionId& session_id, WvCdmEventListener* listener) { CdmSessionIter iter = sessions_.find(session_id); if (iter == sessions_.end()) { return false; } return iter->second->DetachEventListener(listener); } bool CdmEngine::ValidateKeySystem(const CdmKeySystem& key_system) { return (key_system.find("widevine") != std::string::npos); } bool CdmEngine::CancelSessions() { // TODO(gmorgan) Implement CancelSessions() return true; } // Parse a blob of multiple concatenated PSSH atoms to extract the first // widevine pssh // TODO(kqyang): temporary workaround - remove after b/7928472 is resolved bool CdmEngine::ExtractWidevinePssh( const CdmInitData& init_data, CdmInitData* output) { BufferReader reader( reinterpret_cast(init_data.data()), init_data.length()); // TODO(kqyang): Extracted from an actual init_data; // Need to find out where it comes from. static const uint8_t kWidevineSystemId[] = { 0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED, }; // one PSSH blob consists of: // 4 byte size of the PSSH atom, inclusive // "pssh" // 4 byte flags, value 0 // 16 byte system id // 4 byte size of PSSH data, exclusive while (1) { // size of PSSH atom, used for skipping uint32_t size; if (!reader.Read4(&size)) return false; // "pssh" std::vector pssh; if (!reader.ReadVec(&pssh, 4)) return false; if (memcmp(&pssh[0], "pssh", 4)) return false; // flags uint32_t flags; if (!reader.Read4(&flags)) return false; if (flags != 0) return false; // system id std::vector system_id; if (!reader.ReadVec(&system_id, sizeof(kWidevineSystemId))) return false; if (memcmp(&system_id[0], kWidevineSystemId, sizeof(kWidevineSystemId))) { // skip the remaining contents of the atom, // after size field, atom name, flags and system id if (!reader.SkipBytes( size - 4 - 4 - 4 - sizeof(kWidevineSystemId))) return false; continue; } // size of PSSH box uint32_t pssh_length; if (!reader.Read4(&pssh_length)) return false; output->clear(); if (!reader.ReadString(output, pssh_length)) return false; return true; } // we did not find a matching record return false; } void CdmEngine::EnablePolicyTimer() { if (!policy_timer_.IsRunning()) policy_timer_.Start(this, CDM_POLICY_TIMER_DURATION_SECONDS); } void CdmEngine::DisablePolicyTimer() { if (policy_timer_.IsRunning()) policy_timer_.Stop(); } void CdmEngine::OnTimerEvent() { for (CdmSessionIter iter = sessions_.begin(); iter != sessions_.end(); ++iter) { iter->second->OnTimerEvent(); } } } // namespace wvcdm