Widevine MediaCas client code that works with Android R
This commit is contained in:
535
plugin/src/widevine_cas_api.cpp
Normal file
535
plugin/src/widevine_cas_api.cpp
Normal file
@@ -0,0 +1,535 @@
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include "ca_descriptor.pb.h"
|
||||
#include "cas_events.h"
|
||||
#include "cas_util.h"
|
||||
#include "license_protocol.pb.h"
|
||||
#include "log.h"
|
||||
#include "string_conversions.h"
|
||||
#include "widevine_cas_api.h"
|
||||
#include "widevine_cas_session_map.h"
|
||||
|
||||
static constexpr char kBasePathPrefix[] = "/data/vendor/mediacas/IDM/widevine/";
|
||||
static constexpr char kCertFileBase[] = "cert.bin";
|
||||
|
||||
namespace {
|
||||
bool ReadFileFromStorage(wvutil::FileSystem& file_system,
|
||||
const std::string& filename, std::string* file_data) {
|
||||
if (nullptr == file_data) {
|
||||
return false;
|
||||
}
|
||||
if (!file_system.Exists(filename)) {
|
||||
return false;
|
||||
}
|
||||
size_t filesize = file_system.FileSize(filename);
|
||||
if (0 == filesize) {
|
||||
return false;
|
||||
}
|
||||
file_data->resize(filesize);
|
||||
std::unique_ptr<wvutil::File> file =
|
||||
file_system.Open(filename, wvutil::FileSystem::kReadOnly);
|
||||
if (nullptr == file) {
|
||||
return false;
|
||||
}
|
||||
size_t bytes_read = file->Read(&(*file_data)[0], file_data->size());
|
||||
if (bytes_read != filesize) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoveFile(wvutil::FileSystem& file_system, const std::string& filename) {
|
||||
if (!file_system.Exists(filename)) {
|
||||
return false;
|
||||
}
|
||||
if(!file_system.Remove(filename)){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StoreFile(wvutil::FileSystem& file_system, const std::string& filename,
|
||||
const std::string& file_data) {
|
||||
std::unique_ptr<wvutil::File> file(file_system.Open(
|
||||
filename, wvutil::FileSystem::kTruncate | wvutil::FileSystem::kCreate));
|
||||
if (nullptr == file) {
|
||||
return false;
|
||||
}
|
||||
size_t bytes_written = file->Write(file_data.data(), file_data.size());
|
||||
if (bytes_written != file_data.size()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GenerateLicenseFilename(const std::string& id_data_from_media_id,
|
||||
const std::string& provider_id) {
|
||||
std::string data(id_data_from_media_id + provider_id);
|
||||
std::string hash;
|
||||
hash.resize(SHA256_DIGEST_LENGTH);
|
||||
const unsigned char* input =
|
||||
reinterpret_cast<const unsigned char*>(data.data());
|
||||
unsigned char* output = reinterpret_cast<unsigned char*>(&hash[0]);
|
||||
SHA256(input, data.size(), output);
|
||||
return std::string(std::string(kBasePathPrefix) + wvutil::b2a_hex(hash) +
|
||||
std::string(".lic"));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace wvcas {
|
||||
|
||||
class MediaContext : public CasMediaId {
|
||||
public:
|
||||
MediaContext() : CasMediaId() {}
|
||||
~MediaContext() override {}
|
||||
MediaContext(const MediaContext&) = delete;
|
||||
MediaContext& operator=(const MediaContext&) = delete;
|
||||
|
||||
const std::string content_id() override { return pssh_.content_id(); }
|
||||
const std::string provider_id() override { return pssh_.provider(); }
|
||||
const int group_ids_size() override { return pssh_.group_ids_size(); }
|
||||
const std::string group_id() override {
|
||||
if (group_ids_size() > 0) {
|
||||
return pssh_.group_ids(0);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
CasStatus initialize(const std::string& init_data) override {
|
||||
if (!pssh_.ParseFromString(init_data)) {
|
||||
return CasStatus(CasStatusCode::kInvalidParameter, "invalid init_data");
|
||||
}
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
private:
|
||||
video_widevine::WidevinePsshData pssh_;
|
||||
};
|
||||
|
||||
std::unique_ptr<CasMediaId> CasMediaId::create() {
|
||||
std::unique_ptr<MediaContext> ctx = make_unique<MediaContext>();
|
||||
return std::move(ctx);
|
||||
}
|
||||
|
||||
std::shared_ptr<CryptoSession> WidevineCas::getCryptoSession() {
|
||||
return std::make_shared<CryptoSession>();
|
||||
}
|
||||
|
||||
std::unique_ptr<CasLicense> WidevineCas::getCasLicense() {
|
||||
return make_unique<CasLicense>();
|
||||
}
|
||||
|
||||
std::unique_ptr<wvutil::FileSystem> WidevineCas::getFileSystem() {
|
||||
return make_unique<wvutil::FileSystem>();
|
||||
}
|
||||
|
||||
std::shared_ptr<WidevineCasSession> WidevineCas::newCasSession() {
|
||||
return std::make_shared<WidevineCasSession>();
|
||||
}
|
||||
|
||||
void WidevineCas::OnTimerEvent() {
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
if (cas_license_.get() != nullptr) {
|
||||
cas_license_->OnTimerEvent();
|
||||
|
||||
// Delete expired license after firing expired event in policy_engine
|
||||
if (cas_license_->IsExpired() && (media_id_.get() != nullptr)) {
|
||||
std::string filename;
|
||||
if (media_id_->group_ids_size() > 0) {
|
||||
filename = GenerateLicenseFilename(media_id_->group_id(),
|
||||
media_id_->provider_id());
|
||||
} else {
|
||||
filename = GenerateLicenseFilename(media_id_->content_id(),
|
||||
media_id_->provider_id());
|
||||
}
|
||||
if (!file_system_->Exists(filename)) {
|
||||
LOGI("No expired license file stored in disk");
|
||||
}else{
|
||||
if(RemoveFile(*file_system_, filename)) {
|
||||
LOGI("Remove expired license file from disk successfully.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::initialize(CasEventListener* event_listener) {
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
crypto_session_ = getCryptoSession();
|
||||
// For session name generation.
|
||||
srand(time(nullptr));
|
||||
// Setup an oemcrypto session.
|
||||
CasStatus status = crypto_session_->initialize();
|
||||
if (!status.ok()) {
|
||||
LOGE("WidevineCas initialization failed: %d", status.status_code());
|
||||
return status;
|
||||
}
|
||||
|
||||
file_system_ = getFileSystem();
|
||||
cas_license_ = getCasLicense();
|
||||
status = cas_license_->initialize(crypto_session_, event_listener);
|
||||
if (!status.ok()) {
|
||||
LOGE("WidevineCas initialization failed: %d", status.status_code());
|
||||
return status;
|
||||
}
|
||||
|
||||
std::string cert_filename_path(std::string(kBasePathPrefix) +
|
||||
std::string(kCertFileBase));
|
||||
|
||||
// Try to read a certificate if one exists. If any error occurs, just ignore
|
||||
// it and let new cert file overwrite the existing file.
|
||||
std::string cert_file;
|
||||
if (ReadFileFromStorage(*file_system_, cert_filename_path, &cert_file)) {
|
||||
LOGI("read cert.bin successfully");
|
||||
if (!HandleStoredDrmCert(cert_file).ok()) {
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
}
|
||||
|
||||
event_listener_ = event_listener;
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
// TODO(jfore): Split out the functionality and move the callback out of this
|
||||
// class.
|
||||
CasStatus WidevineCas::openSession(WvCasSessionId* sessionId) {
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
if (nullptr == sessionId) {
|
||||
return CasStatus(CasStatusCode::kInvalidParameter,
|
||||
"missing openSession sessionId");
|
||||
}
|
||||
|
||||
CasSessionPtr session = newCasSession();
|
||||
CasStatus status = session->initialize(
|
||||
crypto_session_, reinterpret_cast<uint32_t*>(sessionId));
|
||||
if (CasStatusCode::kNoError != status.status_code()) {
|
||||
return status;
|
||||
}
|
||||
WidevineCasSessionMap::instance().AddSession(*sessionId, session);
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::closeSession(WvCasSessionId sessionId) {
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
CasSessionPtr session =
|
||||
WidevineCasSessionMap::instance().GetSession(sessionId);
|
||||
// TODO(jfore): Add a log event if the session doesn't exist and perhaps raise
|
||||
// an error.`
|
||||
if (session == nullptr) {
|
||||
return CasStatus(CasStatusCode::kSessionNotFound, "unknown session id");
|
||||
}
|
||||
WidevineCasSessionMap::instance().RemoveSession(sessionId);
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
// TODO(jfore): Add unit test to widevine_cas_api_test.cpp that is added in
|
||||
// another cl.
|
||||
CasStatus WidevineCas::processEcm(WvCasSessionId sessionId, const CasEcm& ecm) {
|
||||
LOGD("WidevineCasPlugin::processEcm");
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
// If we don't have a license yet, save the ecm and session id.
|
||||
if (!has_license_) {
|
||||
deferred_ecms_.emplace(sessionId, ecm);
|
||||
return CasStatusCode::kDeferedEcmProcessing;
|
||||
}
|
||||
return HandleProcessEcm(sessionId, ecm);
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::HandleProcessEcm(const WvCasSessionId& sessionId,
|
||||
const CasEcm& ecm) {
|
||||
if (cas_license_->IsExpired()) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError,
|
||||
"license is expired, unable to process ecm");
|
||||
}
|
||||
CasSessionPtr session =
|
||||
WidevineCasSessionMap::instance().GetSession(sessionId);
|
||||
if (session == nullptr) {
|
||||
return CasStatus(CasStatusCode::kSessionNotFound,
|
||||
"unknown session for processEcm");
|
||||
}
|
||||
uint8_t ecm_age_previous = session->GetEcmAgeRestriction();
|
||||
|
||||
CasStatus status = session->processEcm(ecm, parental_control_age_);
|
||||
uint8_t ecm_age_current = session->GetEcmAgeRestriction();
|
||||
if (event_listener_ != nullptr && ecm_age_current != ecm_age_previous) {
|
||||
event_listener_->OnAgeRestrictionUpdated(sessionId, ecm_age_current);
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
cas_license_->BeginDecryption();
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::HandleDeferredECMs() {
|
||||
for (const auto& deferred_ecm : deferred_ecms_) {
|
||||
CasStatus status =
|
||||
HandleProcessEcm(deferred_ecm.first, deferred_ecm.second);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
deferred_ecms_.clear();
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::generateDeviceProvisioningRequest(
|
||||
std::string* provisioning_request) {
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
if (provisioning_request == nullptr) {
|
||||
return CasStatus(CasStatusCode::kInvalidParameter,
|
||||
"missing output buffer for provisioning request");
|
||||
}
|
||||
return cas_license_->GenerateDeviceProvisioningRequest(provisioning_request);
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::handleProvisioningResponse(const std::string& response) {
|
||||
if (response.empty()) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError,
|
||||
"empty individualization response");
|
||||
}
|
||||
std::string device_file;
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
CasStatus status = cas_license_->HandleDeviceProvisioningResponse(
|
||||
response, &device_certificate_, &wrapped_rsa_key_, &device_file);
|
||||
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (!device_file.empty()) {
|
||||
std::string cert_filename(std::string(kBasePathPrefix) +
|
||||
std::string(kCertFileBase));
|
||||
StoreFile(*file_system_, cert_filename, device_file);
|
||||
}
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::generateEntitlementRequest(
|
||||
const std::string& init_data, std::string* entitlement_request) {
|
||||
media_id_ = CasMediaId::create();
|
||||
CasStatus status = media_id_->initialize(init_data);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
std::string license_file;
|
||||
std::string filename;
|
||||
if (media_id_->group_ids_size() > 0) {
|
||||
filename = GenerateLicenseFilename(media_id_->group_id(),
|
||||
media_id_->provider_id());
|
||||
} else {
|
||||
filename = GenerateLicenseFilename(media_id_->content_id(),
|
||||
media_id_->provider_id());
|
||||
}
|
||||
if (ReadFileFromStorage(*file_system_, filename, &license_file)) {
|
||||
CasStatus status =
|
||||
cas_license_->HandleStoredLicense(wrapped_rsa_key_, license_file);
|
||||
if (status.ok()) {
|
||||
// If license file is expired, don't proceed the request. Also
|
||||
// delete the stored license file.
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
if (cas_license_->IsExpired()) {
|
||||
if(!RemoveFile(*file_system_, filename)) {
|
||||
return CasStatus(CasStatusCode::kInvalidLicenseFile,
|
||||
"unable to remove expired license file from disk");
|
||||
}
|
||||
LOGI("Remove expired license file from disk successfully.");
|
||||
return CasStatus(CasStatusCode::kCasLicenseError,
|
||||
"license is expired, unable to process emm");
|
||||
}
|
||||
policy_timer_.Start(this, 1);
|
||||
has_license_ = true;
|
||||
return HandleDeferredECMs();
|
||||
}
|
||||
LOGI("Fallthru");
|
||||
}
|
||||
|
||||
if (entitlement_request == nullptr) {
|
||||
return CasStatus(CasStatusCode::kInvalidParameter,
|
||||
"missing output buffer for entitlement request");
|
||||
}
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
return cas_license_->GenerateEntitlementRequest(
|
||||
init_data, device_certificate_, wrapped_rsa_key_, license_type_,
|
||||
entitlement_request);
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::handleEntitlementResponse(const std::string& response,
|
||||
std::string& license_id) {
|
||||
if (response.empty()) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError,
|
||||
"empty entitlement response");
|
||||
}
|
||||
std::string device_file;
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
CasStatus status =
|
||||
cas_license_->HandleEntitlementResponse(response, &device_file);
|
||||
if (status.ok()) {
|
||||
// A license has been successfully loaded. Load any ecms that may have been
|
||||
// deferred waiting for the license.
|
||||
has_license_ = true;
|
||||
status = HandleDeferredECMs();
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
policy_timer_.Start(this, 1);
|
||||
|
||||
if (device_file.empty()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (!device_file.empty()) {
|
||||
std::string filename;
|
||||
if (media_id_->group_ids_size() > 0) {
|
||||
filename = GenerateLicenseFilename(media_id_->group_id(),
|
||||
media_id_->provider_id());
|
||||
} else {
|
||||
filename = GenerateLicenseFilename(media_id_->content_id(),
|
||||
media_id_->provider_id());
|
||||
}
|
||||
StoreFile(*file_system_, filename, device_file);
|
||||
// license_id will be the filename without ".lic" extension.
|
||||
license_id = filename.substr(0,
|
||||
filename.size() - std::string(".lic").size());
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::generateEntitlementRenewalRequest(
|
||||
std::string* entitlement_renewal_request) {
|
||||
if (entitlement_renewal_request == nullptr) {
|
||||
return CasStatus(CasStatusCode::kInvalidParameter,
|
||||
"missing output buffer for entitlement renewal request");
|
||||
}
|
||||
return cas_license_->GenerateEntitlementRenewalRequest(
|
||||
device_certificate_, entitlement_renewal_request);
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::handleEntitlementRenewalResponse(
|
||||
const std::string& response, std::string& license_id) {
|
||||
if (response.empty()) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError,
|
||||
"empty entitlement renewal response");
|
||||
}
|
||||
std::string device_file;
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
CasStatus status =
|
||||
cas_license_->HandleEntitlementRenewalResponse(response, &device_file);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (!device_file.empty()) {
|
||||
std::string filename;
|
||||
if (media_id_->group_ids_size() > 0) {
|
||||
filename = GenerateLicenseFilename(media_id_->group_id(),
|
||||
media_id_->provider_id());
|
||||
} else {
|
||||
filename = GenerateLicenseFilename(media_id_->content_id(),
|
||||
media_id_->provider_id());
|
||||
}
|
||||
StoreFile(*file_system_, filename, device_file);
|
||||
// license_id will be the filename without ".lic" extension.
|
||||
license_id = filename.substr(0,
|
||||
filename.size() - std::string(".lic").size());
|
||||
}
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
|
||||
CasStatus WidevineCas::RemoveLicense(const std::string file_name) {
|
||||
// Check if the license is in use. If it is, besides removing the license,
|
||||
// update policy in current license. Else, we just directly remove it.
|
||||
if (nullptr == media_id_.get() ) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError, "No media id");
|
||||
}
|
||||
// Remove the license file given the file_name user provides.
|
||||
if (!RemoveFile(*file_system_, file_name)) {
|
||||
return CasStatus(CasStatusCode::kInvalidLicenseFile,
|
||||
"unable to remove license file from disk");
|
||||
}
|
||||
LOGI("Remove license file from disk successfully.");
|
||||
|
||||
std::string used_license_filename;
|
||||
if (media_id_->group_ids_size() > 0) {
|
||||
used_license_filename = GenerateLicenseFilename(media_id_->group_id(),
|
||||
media_id_->provider_id());
|
||||
} else {
|
||||
used_license_filename = GenerateLicenseFilename(media_id_->content_id(),
|
||||
media_id_->provider_id());
|
||||
}
|
||||
if (file_name.compare(used_license_filename) == 0) {
|
||||
// Update license policy for the in-used license. Plugin will not allowed to
|
||||
// play stream, store and renew license unless a new plugin instance is created.
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
cas_license_->UpdateLicenseForLicenseRemove();
|
||||
}
|
||||
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
bool WidevineCas::is_provisioned() const {
|
||||
return (!(device_certificate_.empty() || wrapped_rsa_key_.empty()));
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::ProcessCAPrivateData(const CasData& private_data,
|
||||
std::string* init_data) {
|
||||
if (init_data == nullptr) {
|
||||
return CasStatus(CasStatusCode::kInvalidParameter,
|
||||
"missing output buffer for init_data");
|
||||
}
|
||||
// Parse provider, content id and group id from CA descriptor.
|
||||
video_widevine::CaDescriptorPrivateData descriptor;
|
||||
descriptor.ParseFromArray(private_data.data(), private_data.size());
|
||||
if (!descriptor.has_content_id() || !descriptor.has_provider()) {
|
||||
return CasStatus(CasStatusCode::kInvalidParameter,
|
||||
"unable to parse private data");
|
||||
}
|
||||
|
||||
// Build PSSH of type ENTITLEMENT.
|
||||
video_widevine::WidevinePsshData pssh;
|
||||
pssh.set_provider(descriptor.provider());
|
||||
pssh.set_content_id(descriptor.content_id());
|
||||
// group id is optional in ca_descriptor.
|
||||
if (descriptor.has_group_id()) {
|
||||
pssh.add_group_ids(descriptor.group_id());
|
||||
}
|
||||
pssh.set_type(video_widevine::WidevinePsshData::ENTITLEMENT);
|
||||
pssh.SerializeToString(init_data);
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::ProcessSessionCAPrivateData(WvCasSessionId session_id,
|
||||
const CasData& private_data,
|
||||
std::string* init_data) {
|
||||
if (!WidevineCasSessionMap::instance().GetSession(session_id)) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError, "invalid session id");
|
||||
}
|
||||
return ProcessCAPrivateData(private_data, init_data);
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::GetUniqueID(std::string* buffer) {
|
||||
return crypto_session_->GetDeviceID(buffer);
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::HandleStoredDrmCert(const std::string& certificate) {
|
||||
if (certificate.empty()) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError, "empty certificate data");
|
||||
}
|
||||
CasStatus status = cas_license_->HandleStoredDrmCert(
|
||||
certificate, &device_certificate_, &wrapped_rsa_key_);
|
||||
return status;
|
||||
}
|
||||
|
||||
CasStatus WidevineCas::HandleSetParentalControlAge(const CasData& data) {
|
||||
if (data.empty()) {
|
||||
return CasStatus(CasStatusCode::kCasLicenseError,
|
||||
"missing value of parental control min age");
|
||||
}
|
||||
parental_control_age_ = data[0];
|
||||
LOGI("Parental control age set to: ", parental_control_age_);
|
||||
return CasStatusCode::kNoError;
|
||||
}
|
||||
|
||||
} // namespace wvcas
|
||||
Reference in New Issue
Block a user