diff --git a/libwvdrmengine/cdm/core/include/cdm_engine.h b/libwvdrmengine/cdm/core/include/cdm_engine.h index 2bc0a7a8..313fc80e 100644 --- a/libwvdrmengine/cdm/core/include/cdm_engine.h +++ b/libwvdrmengine/cdm/core/include/cdm_engine.h @@ -235,6 +235,10 @@ class CdmEngine { virtual CdmResponseType RemoveOfflineLicense(const std::string& key_set_id, CdmSecurityLevel security_level); + virtual CdmResponseType StoreAtscLicense( + RequestedSecurityLevel security_level, const CdmKeySetId& key_set_id, + const std::string& serialized_license_data); + // Usage related methods for streaming licenses // Retrieve a random usage info from the list of all usage infos for this app // id. If |error_detail| is not null, an additional error code may be provided diff --git a/libwvdrmengine/cdm/core/include/device_files.h b/libwvdrmengine/cdm/core/include/device_files.h index 8b3926ef..99854b2d 100644 --- a/libwvdrmengine/cdm/core/include/device_files.h +++ b/libwvdrmengine/cdm/core/include/device_files.h @@ -155,6 +155,9 @@ class DeviceFiles { virtual bool RetrieveLicense(const std::string& key_set_id, CdmLicenseData* license_data, ResponseType* result); + virtual ResponseType StoreAtscLicense( + const CdmKeySetId& key_set_id, + const std::string& serialized_license_data); virtual bool DeleteLicense(const std::string& key_set_id); virtual bool ListLicenses(std::vector* key_set_ids); diff --git a/libwvdrmengine/cdm/core/src/cdm_engine.cpp b/libwvdrmengine/cdm/core/src/cdm_engine.cpp index 91c415a8..4c033637 100644 --- a/libwvdrmengine/cdm/core/src/cdm_engine.cpp +++ b/libwvdrmengine/cdm/core/src/cdm_engine.cpp @@ -1413,6 +1413,35 @@ CdmResponseType CdmEngine::RemoveOfflineLicense( return sts; } +CdmResponseType CdmEngine::StoreAtscLicense( + RequestedSecurityLevel requested_security_level, + const CdmKeySetId& key_set_id, const std::string& serialized_license_data) { + std::string security_level_string; + + CdmResponseType status = + QueryStatus(requested_security_level, QUERY_KEY_SECURITY_LEVEL, + &security_level_string); + if (status != NO_ERROR) return status; + + DeviceFiles handle(file_system_); + CdmSecurityLevel security_level = + security_level_string == QUERY_VALUE_SECURITY_LEVEL_L1 ? kSecurityLevelL1 + : kSecurityLevelL3; + if (!handle.Init(security_level)) { + LOGE("Unable to initialize device files"); + return CdmResponseType(STORE_ATSC_LICENSE_DEVICE_FILES_INIT_ERROR); + } + + DeviceFiles::ResponseType response_type = + handle.StoreAtscLicense(key_set_id, serialized_license_data); + if (response_type != DeviceFiles::kNoError) { + LOGE("Unable to store ATSC license: response = %s", + DeviceFiles::ResponseTypeToString(response_type)); + return CdmResponseType(STORE_ATSC_LICENSE_ERROR); + } + return CdmResponseType(NO_ERROR); +} + CdmResponseType CdmEngine::GetUsageInfo(const std::string& app_id, const CdmSecureStopId& ssid, int* error_detail, diff --git a/libwvdrmengine/cdm/core/src/device_files.cpp b/libwvdrmengine/cdm/core/src/device_files.cpp index b637b6f2..885eecf0 100644 --- a/libwvdrmengine/cdm/core/src/device_files.cpp +++ b/libwvdrmengine/cdm/core/src/device_files.cpp @@ -943,6 +943,11 @@ bool DeviceFiles::RetrieveLicense(const std::string& key_set_id, &license_data->wrapped_private_key); } +DeviceFiles::ResponseType DeviceFiles::StoreAtscLicense( + const CdmKeySetId& key_set_id, const std::string& serialized_data) { + return StoreFileWithHash(key_set_id + kLicenseFileNameExt, serialized_data); +} + bool DeviceFiles::DeleteLicense(const std::string& key_set_id) { RETURN_FALSE_IF_UNINITIALIZED(); return RemoveFile(key_set_id + kLicenseFileNameExt); diff --git a/libwvdrmengine/cdm/include/wv_content_decryption_module.h b/libwvdrmengine/cdm/include/wv_content_decryption_module.h index bc6145f8..186b6d8f 100644 --- a/libwvdrmengine/cdm/include/wv_content_decryption_module.h +++ b/libwvdrmengine/cdm/include/wv_content_decryption_module.h @@ -194,6 +194,10 @@ class WvContentDecryptionModule : public android::RefBase, public TimerHandler { virtual CdmResponseType GetSessionUserId(const CdmSessionId& session_id, uint32_t* user_id); + virtual CdmResponseType StoreAtscLicense( + const CdmIdentifier& identifier, RequestedSecurityLevel security_level, + const CdmKeySetId& key_set_id, + const std::string& serialized_license_data); virtual bool SetDefaultOtaKeyboxFallbackDurationRules(); virtual bool SetFastOtaKeyboxFallbackDurationRules(); diff --git a/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp b/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp index fcc1fb0c..8c95cb8c 100644 --- a/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp +++ b/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp @@ -656,6 +656,15 @@ CdmResponseType WvContentDecryptionModule::GetSessionUserId( return CdmResponseType(NO_ERROR); } +CdmResponseType WvContentDecryptionModule::StoreAtscLicense( + const CdmIdentifier& identifier, + RequestedSecurityLevel requested_security_level, + const CdmKeySetId& key_set_id, const std::string& serialized_license_data) { + CdmEngine* cdm_engine = EnsureCdmForIdentifier(identifier); + return cdm_engine->StoreAtscLicense(requested_security_level, key_set_id, + serialized_license_data); +} + bool WvContentDecryptionModule::SetDefaultOtaKeyboxFallbackDurationRules() { CdmEngine* cdm_engine = EnsureCdmForIdentifier(kDefaultCdmIdentifier); if (!cdm_engine) return false; diff --git a/libwvdrmengine/mediadrm/include/WVDrmPlugin.h b/libwvdrmengine/mediadrm/include/WVDrmPlugin.h index 6a4a644a..7fb97e27 100644 --- a/libwvdrmengine/mediadrm/include/WVDrmPlugin.h +++ b/libwvdrmengine/mediadrm/include/WVDrmPlugin.h @@ -456,6 +456,10 @@ class WVDrmPlugin : public ::aidl::android::hardware::drm::BnDrmPlugin, in_keyStatusList, bool in_hasNewUsableKey); void sendSessionLostState(const std::vector& in_sessionId); + + WvStatus parseAtscLicenseData(const std::string& value, + std::string* key_set_id, + std::string* serialized_license_data); }; } // namespace widevine diff --git a/libwvdrmengine/mediadrm/src/WVDrmPlugin.cpp b/libwvdrmengine/mediadrm/src/WVDrmPlugin.cpp index 8899754c..c984eff3 100644 --- a/libwvdrmengine/mediadrm/src/WVDrmPlugin.cpp +++ b/libwvdrmengine/mediadrm/src/WVDrmPlugin.cpp @@ -26,6 +26,7 @@ #include "mapErrors-inl.h" #include "media/stagefright/MediaErrors.h" #include "openssl/sha.h" +#include "string_conversions.h" #include "wv_cdm_constants.h" #include "wv_metrics.pb.h" #include "wv_metrics_adapter.h" @@ -1344,6 +1345,32 @@ Status WVDrmPlugin::unprovisionDevice() { if (!success) { return toNdkScopedAStatus(Status::ERROR_DRM_UNKNOWN); } + } else if (name == "storeAtscLicense") { + if (!mCdmIdentifierBuilder.is_sealed()) { + ALOGE("Cdm identifier builder is not sealed. Storing ATSC license " + "prohibited"); + return toNdkScopedAStatus(Status::ERROR_DRM_CANNOT_HANDLE); + } + if (!mPropertySet.use_atsc_mode()) { + ALOGE("ATSC mode not enabled. Storing ATSC license prohibited"); + return toNdkScopedAStatus(Status::ERROR_DRM_CANNOT_HANDLE); + } + + CdmIdentifier identifier; + auto status = mCdmIdentifierBuilder.getCdmIdentifier(&identifier); + if (status != Status::OK) { + ALOGE("Unable to get CDM Identifier = %d", status.get()); + return toNdkScopedAStatus(status); + } + std::string key_set_id, license_data; + status = parseAtscLicenseData(_value, &key_set_id, &license_data); + if (status != Status::OK) + return toNdkScopedAStatus(status); + + const CdmResponseType res = mCDM->StoreAtscLicense( + identifier, getRequestedSecurityLevel(), key_set_id, license_data); + + return toNdkScopedAStatus(mapCdmResponseType(res)); } else { ALOGE("App set unknown string property %s", name.c_str()); return toNdkScopedAStatus(Status::ERROR_DRM_CANNOT_HANDLE); @@ -1871,6 +1898,57 @@ bool WVDrmPlugin::isProvisioned(wvcdm::CdmSecurityLevel securityLevel, return mCDM->IsProvisioned(securityLevel, origin, spoid, atsc_mode_enabled); } +WvStatus WVDrmPlugin::parseAtscLicenseData( + const std::string& in_value, + std::string* key_set_id, + std::string* serialized_license_data) { + if (key_set_id == nullptr) { + ALOGE("key_set_id null"); + return WvStatus(Status::ERROR_DRM_CANNOT_HANDLE); + } + + if (serialized_license_data == nullptr) { + ALOGE("serialized_license_data null"); + return WvStatus(Status::ERROR_DRM_CANNOT_HANDLE); + } + + if (in_value.compare(0, + strlen(wvcdm::ATSC_KEY_SET_ID_PREFIX), + &wvcdm::ATSC_KEY_SET_ID_PREFIX[0]) != 0) { + ALOGE("ATSC license input does not conform to expectations. Key set does " + "not have a valid ATSC Key set prefix %s", in_value.c_str()); + return WvStatus(Status::BAD_VALUE); + } + const char kColon = ':'; + const size_t pos = in_value.find(kColon); + if (pos == std::string::npos) { + ALOGE("ATSC license input does not conform to expectations. Missing colon " + "= %s", in_value.c_str()); + return WvStatus(Status::BAD_VALUE); + } + + if (pos == in_value.length()) { + ALOGE("ATSC license input does not conform to expectations. No data after " + "colon"); + return WvStatus(Status::BAD_VALUE); + } + + *key_set_id = in_value.substr(0, pos); + const std::vector license_data_binary = + wvutil::Base64Decode(in_value.substr(pos+1)); + + if (license_data_binary.empty()) { + ALOGE("ATSC license input does not conform to expectations. License data " + "failed to decode from Base64"); + return WvStatus(Status::BAD_VALUE); + } + serialized_license_data->assign(license_data_binary.begin(), + license_data_binary.end()); + + return WvStatus(Status::OK); +} + + WvStatus WVDrmPlugin::mapAndNotifyOfCdmResponseType( const vector& sessionId, CdmResponseType res) { notifyOfCdmResponseType(sessionId, res); diff --git a/libwvdrmengine/mediadrm/test/WVDrmPlugin_hal_test.cpp b/libwvdrmengine/mediadrm/test/WVDrmPlugin_hal_test.cpp index 4fda3b95..527e4833 100644 --- a/libwvdrmengine/mediadrm/test/WVDrmPlugin_hal_test.cpp +++ b/libwvdrmengine/mediadrm/test/WVDrmPlugin_hal_test.cpp @@ -239,6 +239,11 @@ public: MOCK_METHOD(CdmResponseType, RemoveOfflineLicense, (const std::string &, CdmSecurityLevel, const CdmIdentifier &), (override)); + + MOCK_METHOD(CdmResponseType, StoreAtscLicense, + (const CdmIdentifier &, wvcdm::RequestedSecurityLevel, + const std::string &, const std::string &), + (override)); }; class MockCrypto : public WVGenericCryptoInterface { @@ -2909,6 +2914,253 @@ TEST_F(WVDrmPluginHalTest, RemoveOfflineLicense) { EXPECT_TRUE(ret.isOk()); } +TEST_F(WVDrmPluginHalTest, CanStoreAtscLicense) { + android::sp> cdm = new StrictMock(); + StrictMock crypto; + std::string appPackageName = "com.test.package"; + + const CdmClientPropertySet *propertySet = nullptr; + CdmIdentifier mCdmIdAtscModeSet; + + // Generate ATSC keyset and license data + uint8_t atscKeySetIdRaw[kKeySetIdSize]; + std::vector licenseDataRaw; + const size_t kLicenseDataSize = 512; + licenseDataRaw.resize(kLicenseDataSize); + + FILE *fp = fopen("/dev/urandom", "r"); + ASSERT_NE(fp, nullptr) << "Failed to open /dev/urandom"; + + fread(atscKeySetIdRaw, sizeof(uint8_t), sizeof(atscKeySetIdRaw), fp); + fread(&licenseDataRaw[0], sizeof(uint8_t), licenseDataRaw.size(), fp); + fclose(fp); + + // Generate an ATSC key set ID of the form + + // with a total length of kKeySetIdSize + std::string atscKeySetId(wvcdm::ATSC_KEY_SET_ID_PREFIX); + atscKeySetId.append(wvutil::HexEncode(atscKeySetIdRaw, kKeySetIdSize)); + atscKeySetId.resize(kKeySetIdSize); + memcpy(atscKeySetIdRaw, atscKeySetId.c_str(), sizeof(atscKeySetIdRaw)); + + std::string licenseDataBase64 = Base64Encode(licenseDataRaw); + std::string atscLicenseData = atscKeySetId + ":" + licenseDataBase64; + + // Provide expected mock behavior + { + // Provide expected behavior in response to OpenSession and store the + // property set + EXPECT_CALL(*cdm, OpenSession(_, _, _, _, _)) + .WillOnce(DoAll(SetArgPointee<4>(mCdmSessionId), + SaveArg<1>(&propertySet), + SaveArg<2>(&mCdmIdAtscModeSet), + testing::Return(CdmResponseType(wvcdm::NO_ERROR)))); + + // Provide expected behavior when plugin requests session control info + EXPECT_CALL(*cdm, QueryOemCryptoSessionId(mCdmSessionId, _)) + .WillRepeatedly(Invoke(setSessionIdOnMap<4>)); + + EXPECT_CALL( + *cdm, StoreAtscLicense(HasOrigin(EMPTY_ORIGIN), wvcdm::kLevelDefault, + ElementsAreArray(atscKeySetIdRaw, kKeySetIdSize), + ElementsAreArray(licenseDataRaw))) + .Times(1); + + EXPECT_CALL(*cdm, CloseSession(_)).Times(1); + } + + std::shared_ptr plugin = ::ndk::SharedRefBase::make( + cdm.get(), appPackageName, &crypto, false); + + // Test turning on ATSC mode + auto ret = + plugin->setPropertyString(std::string("atscMode"), std::string("enable")); + EXPECT_TRUE(ret.isOk()); + + ret = plugin->openSession(SecurityLevel::DEFAULT, &sessionId); + EXPECT_TRUE(ret.isOk()); + ASSERT_THAT(propertySet, NotNull()); + EXPECT_TRUE(propertySet->use_atsc_mode()); + EXPECT_EQ(mCdmIdAtscModeSet.app_package_name, wvcdm::ATSC_APP_PACKAGE_NAME); + + ret = plugin->setPropertyString(std::string("storeAtscLicense"), + atscLicenseData); + EXPECT_TRUE(ret.isOk()); + + ret = plugin->closeSession(sessionId); + EXPECT_TRUE(ret.isOk()); +} + +TEST_F(WVDrmPluginHalTest, FailsToStoreAtscLicense) { + android::sp> cdm1 = new StrictMock(); + android::sp> cdm2 = new StrictMock(); + android::sp> cdm3 = new StrictMock(); + StrictMock crypto; + std::string appPackageName = "com.test.package"; + + const CdmClientPropertySet *propertySet = nullptr; + CdmIdentifier mCdmIdAtscModeSet; + + // Generate ATSC keyset and license data + uint8_t atscKeySetIdRaw[kKeySetIdSize]; + std::vector licenseDataRaw; + const size_t kLicenseDataSize = 512; + licenseDataRaw.resize(kLicenseDataSize); + + FILE *fp = fopen("/dev/urandom", "r"); + ASSERT_NE(fp, nullptr) << "Failed to open /dev/urandom"; + + fread(atscKeySetIdRaw, sizeof(uint8_t), sizeof(atscKeySetIdRaw), fp); + fread(&licenseDataRaw[0], sizeof(uint8_t), licenseDataRaw.size(), fp); + fclose(fp); + + std::string atscKeySetId(wvcdm::ATSC_KEY_SET_ID_PREFIX); + atscKeySetId.append(wvutil::HexEncode(&atscKeySetIdRaw[0], kKeySetIdSize)); + atscKeySetId.resize(kKeySetIdSize); + memcpy(atscKeySetIdRaw, atscKeySetId.c_str(), sizeof(atscKeySetIdRaw)); + + std::string licenseDataBase64 = Base64Encode(licenseDataRaw); + std::string atscLicenseData = atscKeySetId + ":" + licenseDataBase64; + + // Provide expected mock behavior + { + // No mock behavior expected for plugin1/cdm1 + + // For plugin2/cdm2 + // Provide expected behavior in response to OpenSession and store the + // property set + EXPECT_CALL(*cdm2, OpenSession(_, _, _, _, _)) + .WillOnce(DoAll(SetArgPointee<4>(mCdmSessionId), + SaveArg<1>(&propertySet), + SaveArg<2>(&mCdmIdAtscModeSet), + testing::Return(CdmResponseType(wvcdm::NO_ERROR)))); + + // Provide expected behavior when plugin2 requests session control info + EXPECT_CALL(*cdm2, QueryOemCryptoSessionId(mCdmSessionId, _)) + .WillRepeatedly(Invoke(setSessionIdOnMap<4>)); + + EXPECT_CALL(*cdm2, CloseSession(_)).Times(1); + + // For plugin3/cdm3 + // Provide expected behavior in response to OpenSession and store the + // property set + EXPECT_CALL(*cdm3, OpenSession(_, _, _, _, _)) + .WillOnce(DoAll(SetArgPointee<4>(mCdmSessionId), + SaveArg<1>(&propertySet), + SaveArg<2>(&mCdmIdAtscModeSet), + testing::Return(CdmResponseType(wvcdm::NO_ERROR)))); + + // Provide expected behavior when plugin3 requests session control info + EXPECT_CALL(*cdm3, QueryOemCryptoSessionId(mCdmSessionId, _)) + .WillRepeatedly(Invoke(setSessionIdOnMap<4>)); + + EXPECT_CALL(*cdm3, CloseSession(_)).Times(1); + } + + // Try storing an ATSC license without opening a session. Should fail. + std::shared_ptr plugin1 = + ::ndk::SharedRefBase::make( + cdm1.get(), appPackageName.c_str(), &crypto, false); + + auto ret = plugin1->setPropertyString(std::string("storeAtscLicense"), + atscLicenseData); + EXPECT_TRUE(!ret.isOk()); + EXPECT_EQ(static_cast(Status::ERROR_DRM_CANNOT_HANDLE), + ret.getServiceSpecificError()); + + // Try storing an ATSC license without setting ATSC mode. Should fail. + std::shared_ptr plugin2 = + ::ndk::SharedRefBase::make( + cdm2.get(), appPackageName.c_str(), &crypto, false); + + ret = plugin2->openSession(SecurityLevel::DEFAULT, &sessionId); + EXPECT_TRUE(ret.isOk()); + ASSERT_THAT(propertySet, NotNull()); + EXPECT_FALSE(propertySet->use_atsc_mode()); + EXPECT_NE(mCdmIdAtscModeSet.app_package_name, wvcdm::ATSC_APP_PACKAGE_NAME); + + ret = plugin2->setPropertyString(std::string("storeAtscLicense"), + atscLicenseData); + EXPECT_TRUE(!ret.isOk()); + EXPECT_EQ(static_cast(Status::ERROR_DRM_CANNOT_HANDLE), + ret.getServiceSpecificError()); + + ret = plugin2->closeSession(sessionId); + EXPECT_TRUE(ret.isOk()); + + // Try storing an ATSC license + // (a) Without ATSC key set prefix + // (b) Only ATSC key set + // (c) Only license data + // (d) Invalid Base64 license data + // (e) No ATSC key set + // (f) No license data + // All these should fail. + std::shared_ptr plugin3 = + ::ndk::SharedRefBase::make( + cdm3.get(), appPackageName.c_str(), &crypto, false); + + // Test turning on ATSC mode + ret = plugin3->setPropertyString(std::string("atscMode"), + std::string("enable")); + EXPECT_TRUE(ret.isOk()); + + ret = plugin3->openSession(SecurityLevel::DEFAULT, &sessionId); + EXPECT_TRUE(ret.isOk()); + ASSERT_THAT(propertySet, NotNull()); + EXPECT_TRUE(propertySet->use_atsc_mode()); + EXPECT_EQ(mCdmIdAtscModeSet.app_package_name, wvcdm::ATSC_APP_PACKAGE_NAME); + + // (a) No ATSC key set prefix + atscKeySetId.assign(wvutil::HexEncode(&atscKeySetIdRaw[0], kKeySetIdSize)); + atscKeySetId.resize(kKeySetIdSize); + atscLicenseData = atscKeySetId + ":" + licenseDataBase64; + + ret = plugin3->setPropertyString(std::string("storeAtscLicense"), + atscLicenseData); + EXPECT_TRUE(!ret.isOk()); + EXPECT_EQ(static_cast(Status::BAD_VALUE), ret.getServiceSpecificError()); + + // (b) Only ATSC key set + atscKeySetId.assign(wvcdm::ATSC_KEY_SET_ID_PREFIX); + atscKeySetId.append(wvutil::HexEncode(&atscKeySetIdRaw[0], kKeySetIdSize)); + atscKeySetId.resize(kKeySetIdSize); + + ret = + plugin3->setPropertyString(std::string("storeAtscLicense"), atscKeySetId); + EXPECT_TRUE(!ret.isOk()); + + // (c) Only license data + ret = plugin3->setPropertyString(std::string("storeAtscLicense"), + licenseDataBase64); + EXPECT_TRUE(!ret.isOk()); + + // (d) Invalid Base64 license data + licenseDataBase64.append("$"); + atscLicenseData = atscKeySetId + ":" + licenseDataBase64; + + ret = plugin3->setPropertyString(std::string("storeAtscLicense"), + atscLicenseData); + EXPECT_TRUE(!ret.isOk()); + + // (e) No ATSC key set + licenseDataBase64 = Base64Encode(licenseDataRaw); + atscLicenseData = ":" + licenseDataBase64; + + ret = plugin3->setPropertyString(std::string("storeAtscLicense"), + atscLicenseData); + EXPECT_TRUE(!ret.isOk()); + + // (f) No license data + atscLicenseData = atscKeySetId + ":"; + + ret = plugin3->setPropertyString(std::string("storeAtscLicense"), + atscLicenseData); + EXPECT_TRUE(!ret.isOk()); + + ret = plugin3->closeSession(sessionId); + EXPECT_TRUE(ret.isOk()); +} + } // namespace widevine } // namespace drm } // namespace hardware