Widevine CAS plugin updates include: - Make Android session id little endian - Rename ca_descriptor.proto to media_cas.proto
499 lines
17 KiB
C++
499 lines
17 KiB
C++
#include "widevine_cas_api.h"
|
|
|
|
#include <openssl/sha.h>
|
|
|
|
#include "cas_events.h"
|
|
#include "cas_util.h"
|
|
#include "license_protocol.pb.h"
|
|
#include "log.h"
|
|
#include "media_cas.pb.h"
|
|
#include "string_conversions.h"
|
|
#include "widevine_cas_session_map.h"
|
|
|
|
constexpr char kBasePathPrefix[] = "/data/vendor/mediacas/IDM/widevine/";
|
|
constexpr char kCertFileBase[] = "cert.bin";
|
|
constexpr char kLicenseFileNameSuffix[] = ".lic";
|
|
|
|
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& content_id,
|
|
const std::string& provider_id) {
|
|
std::string data(content_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(kLicenseFileNameSuffix));
|
|
}
|
|
} // 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(); }
|
|
|
|
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 = 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,
|
|
std::string& license_id) {
|
|
media_id_ = CasMediaId::create();
|
|
CasStatus status = media_id_->initialize(init_data);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
std::string license_file;
|
|
std::string 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");
|
|
}
|
|
license_id =
|
|
filename.substr(0, filename.size() - strlen(kLicenseFileNameSuffix));
|
|
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 = 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() - strlen(kLicenseFileNameSuffix));
|
|
}
|
|
}
|
|
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 = 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 = 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 and content 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());
|
|
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
|