// // Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary // source code may only be used and distributed under the Widevine Master // License Agreement. // //#define LOG_NDEBUG 0 #define LOG_TAG "WVCdm" #include #include "WVCryptoPlugin.h" #include #include #include #include #include #include #include "mapErrors-inl.h" #include "media/stagefright/MediaErrors.h" #include "OEMCryptoCENC.h" #include "openssl/sha.h" #include "utils/Errors.h" #include "utils/String8.h" #include "wv_cdm_constants.h" #include "WVErrors.h" namespace wvdrm { using namespace android; using namespace std; using namespace wvcdm; WVCryptoPlugin::WVCryptoPlugin(const void* data, size_t size, const sp& cdm) : mCDM(cdm), mTestMode(false), mSessionId(configureTestMode(data, size)) {} CdmSessionId WVCryptoPlugin::configureTestMode(const void* data, size_t size) { CdmSessionId sessionId; if (data != NULL) { sessionId.assign(static_cast(data), size); size_t index = sessionId.find("test_mode"); if (index != string::npos) { sessionId = sessionId.substr(0, index); mTestMode = true; } } if (!mCDM->IsOpenSession(sessionId)) { sessionId.clear(); } return sessionId; } bool WVCryptoPlugin::requiresSecureDecoderComponent(const char* mime) const { if (!strncasecmp(mime, "video/", 6)) { // Type is video, so query CDM to see if we require a secure decoder. CdmQueryMap status; CdmResponseType res = mCDM->QuerySessionStatus(mSessionId, &status); if (!isCdmResponseTypeSuccess(res)) { ALOGE("Error querying CDM status: %u", res); return false; } return status[QUERY_KEY_SECURITY_LEVEL] == QUERY_VALUE_SECURITY_LEVEL_L1; } else { // Type is not video, so never require a secure decoder. return false; } } void WVCryptoPlugin::notifyResolution(uint32_t width, uint32_t height) { mCDM->NotifyResolution(mSessionId, width, height); } status_t WVCryptoPlugin::setMediaDrmSession(const Vector& sessionId) { CdmSessionId cdmSessionId(reinterpret_cast(sessionId.array()), sessionId.size()); if (sessionId.size() == 0) { return android::BAD_VALUE; } if (!mCDM->IsOpenSession(cdmSessionId)) { return android::ERROR_DRM_SESSION_NOT_OPENED; } else { mSessionId = cdmSessionId; return android::NO_ERROR; } } // Returns negative values for error code and positive values for the size of // decrypted data. In theory, the output size can be larger than the input // size, but in practice this should never happen for AES-CTR. ssize_t WVCryptoPlugin::decrypt(bool secure, const uint8_t key[KEY_ID_SIZE], const uint8_t iv[KEY_IV_SIZE], Mode mode, const Pattern& pattern, const void* srcPtr, const SubSample* subSamples, size_t numSubSamples, void* dstPtr, AString* errorDetailMsg) { if (mode != kMode_Unencrypted && mode != kMode_AES_CTR && mode != kMode_AES_CBC) { errorDetailMsg->setTo( "The requested encryption mode is not supported by Widevine CDM."); return kErrorUnsupportedCrypto; } else if (mode == kMode_AES_CTR && (pattern.mEncryptBlocks != 0 || pattern.mSkipBlocks != 0)) { errorDetailMsg->setTo( "The 'cens' schema is not supported by Widevine CDM."); return kErrorUnsupportedCrypto; } // Convert parameters to the form the CDM wishes to consume them in. const KeyId keyId(reinterpret_cast(key), KEY_ID_SIZE); vector ivVector(iv, iv + KEY_IV_SIZE); const uint8_t* const source = static_cast(srcPtr); uint8_t* const dest = static_cast(dstPtr); // Set up the decrypt params CdmDecryptionParametersV16 params; params.key_id = keyId; params.is_secure = secure; if (mode == kMode_AES_CTR) { params.cipher_mode = kCipherModeCtr; } else if (mode == kMode_AES_CBC) { params.cipher_mode = kCipherModeCbc; } params.pattern.encrypt_blocks = pattern.mEncryptBlocks; params.pattern.skip_blocks = pattern.mSkipBlocks; // Set up the sample // Android's API only supports one at a time params.samples.emplace_back(); CdmDecryptionSample& sample = params.samples.back(); sample.encrypt_buffer = source; sample.decrypt_buffer = dest; sample.decrypt_buffer_offset = 0; sample.iv = ivVector; // Set up the subsamples // We abuse std::transform() here to also do some side-effects: Tallying the // total size of the sample and checking if any of the data is protected. size_t totalSize = 0; bool hasProtectedData = false; sample.subsamples.reserve(numSubSamples); std::transform(subSamples, subSamples + numSubSamples, std::back_inserter(sample.subsamples), [&](const SubSample& subSample) -> CdmDecryptionSubsample { totalSize += subSample.mNumBytesOfClearData + subSample.mNumBytesOfEncryptedData; hasProtectedData |= subSample.mNumBytesOfEncryptedData > 0; return CdmDecryptionSubsample(subSample.mNumBytesOfClearData, subSample.mNumBytesOfEncryptedData); }); sample.encrypt_buffer_length = totalSize; sample.decrypt_buffer_size = totalSize; if (mode == kMode_Unencrypted && hasProtectedData) { errorDetailMsg->setTo("Protected ranges found in allegedly clear data."); return kErrorExpectedUnencrypted; } // Decrypt status_t res = attemptDecrypt(params, hasProtectedData, errorDetailMsg); if (res != android::OK) { return res; } // In test mode, we return an error that causes a detailed error // message string containing a SHA256 hash of the decrypted data // to get passed to the java app via CryptoException. The test app // can then use the hash to verify that decryption was successful. if (mTestMode) { if (secure) { // can't access data in secure mode errorDetailMsg->setTo("secure"); } else { SHA256_CTX ctx; uint8_t digest[SHA256_DIGEST_LENGTH]; SHA256_Init(&ctx); SHA256_Update(&ctx, dstPtr, totalSize); SHA256_Final(digest, &ctx); String8 buf; for (size_t i = 0; i < sizeof(digest); i++) { buf.appendFormat("%02x", digest[i]); } errorDetailMsg->setTo(buf.string()); } return kErrorTestMode; } return static_cast(totalSize); } status_t WVCryptoPlugin::attemptDecrypt( const CdmDecryptionParametersV16& params, bool hasProtectedData, AString* errorDetailMsg) { CdmResponseType res = mCDM->DecryptV16(mSessionId, hasProtectedData, params); if (isCdmResponseTypeSuccess(res)) { return android::OK; } else { ALOGE("Decrypt error in session %s during a sample %s protected data: %d", mSessionId.c_str(), hasProtectedData ? "with" : "without", res); bool actionableError = true; switch (res) { case wvcdm::INSUFFICIENT_CRYPTO_RESOURCES: errorDetailMsg->setTo( "Error decrypting data: insufficient crypto resources"); break; case wvcdm::NEED_KEY: case wvcdm::KEY_NOT_FOUND_IN_SESSION: errorDetailMsg->setTo( "Error decrypting data: requested key has not been loaded"); break; case wvcdm::DECRYPT_NOT_READY: errorDetailMsg->setTo( "Error decrypting data: license validity period is in the future"); break; case wvcdm::SESSION_NOT_FOUND_FOR_DECRYPT: errorDetailMsg->setTo( "Error decrypting data: session not found, possibly reclaimed"); break; case wvcdm::DECRYPT_ERROR: errorDetailMsg->setTo( "Error decrypting data: unspecified error"); break; case wvcdm::INSUFFICIENT_OUTPUT_PROTECTION: case wvcdm::ANALOG_OUTPUT_ERROR: errorDetailMsg->setTo( "Error decrypting data: insufficient output protection"); break; case wvcdm::KEY_PROHIBITED_FOR_SECURITY_LEVEL: errorDetailMsg->setTo( "Error decrypting data: key prohibited for security level"); break; default: actionableError = false; break; } if (actionableError) { // This error is actionable by the app and should be passed up. return mapCdmResponseType(res); } else { // Swallow the specifics of other errors to obscure decrypt internals. return kErrorCDMGeneric; } } } } // namespace wvdrm