| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/390223051): Remove C-library calls to fix the errors. |
| #pragma allow_unsafe_libc_calls |
| #endif |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <string> |
| #include <vector> |
| |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/test/bind.h" |
| #include "media/base/cdm_callback_promise.h" |
| #include "media/base/cdm_promise.h" |
| #include "media/base/content_decryption_module.h" |
| #include "media/base/decoder_buffer.h" |
| #include "media/base/decrypt_config.h" |
| #include "media/base/subsample_entry.h" |
| #include "media/cdm/aes_decryptor.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/fuzztest/src/fuzztest/fuzztest.h" |
| |
| namespace { |
| |
| // Data below is all taken from aes_decryptor_unittest.cc. The tests have a |
| // better description of what is happening. |
| |
| const uint8_t webm_init_data[] = { |
| // base64 equivalent is AAECAw |
| 0x00, 0x01, 0x02, 0x03}; |
| |
| const uint8_t cenc_init_data[] = { |
| 0x00, 0x00, 0x00, 0x44, // size = 68 |
| 0x70, 0x73, 0x73, 0x68, // 'pssh' |
| 0x01, // version |
| 0x00, 0x00, 0x00, // flags |
| 0x10, 0x77, 0xEF, 0xEC, 0xC0, 0xB2, 0x4D, 0x02, // SystemID |
| 0xAC, 0xE3, 0x3C, 0x1E, 0x52, 0xE2, 0xFB, 0x4B, |
| 0x00, 0x00, 0x00, 0x02, // key count |
| 0x7E, 0x57, 0x1D, 0x03, 0x7E, 0x57, 0x1D, 0x03, // key1 |
| 0x7E, 0x57, 0x1D, 0x03, 0x7E, 0x57, 0x1D, 0x03, |
| 0x7E, 0x57, 0x1D, 0x04, 0x7E, 0x57, 0x1D, 0x04, // key2 |
| 0x7E, 0x57, 0x1D, 0x04, 0x7E, 0x57, 0x1D, 0x04, |
| 0x00, 0x00, 0x00, 0x00 // datasize |
| }; |
| |
| const uint8_t keyids_init_data[] = |
| "{\"kids\":[\"AQI\",\"AQIDBA\",\"AQIDBAUGBwgJCgsMDQ4PEA\"]}"; |
| |
| // 3 valid JWKs used as seeds to Update(). |
| const char kKeyAsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"BAUGBwgJCgsMDQ4PEBESEw\"" |
| " }" |
| " ]," |
| " \"type\": \"temporary\"" |
| "}"; |
| |
| const char kKeyAlternateAsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"FBUWFxgZGhscHR4fICEiIw\"" |
| " }" |
| " ]" |
| "}"; |
| |
| const char kWrongKeyAsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"7u7u7u7u7u7u7u7u7u7u7g\"" |
| " }" |
| " ]" |
| "}"; |
| |
| const uint8_t kIv[] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; |
| |
| const media::SubsampleEntry kSubsampleEntriesNormal[] = {{2, 7}, |
| {3, 11}, |
| {1, 0}}; |
| |
| // This is the string "Original subsample data." encrypted with key |
| // 0x0405060708090a0b0c0d0e0f10111213 (base64 equivalent is |
| // BAUGBwgJCgsMDQ4PEBESEw) and kIv but without any subsamples (thus all |
| // encrypted). |
| const uint8_t kEncryptedData[] = { |
| 0x2f, 0x03, 0x09, 0xef, 0x71, 0xaf, 0x31, 0x16, 0xfa, 0x9d, 0x18, 0x43, |
| 0x1e, 0x96, 0x71, 0xb5, 0xbf, 0xf5, 0x30, 0x53, 0x9a, 0x20, 0xdf, 0x95}; |
| |
| } // namespace |
| |
| // Create a session using |int_init_data_type| and |init_data|. First parameter |
| // should be EmeInitDataType, but Fuzz tests currently don't support "enum |
| // class", so using an int (1-3) instead. Test will check parsing of |
| // |init_data|. |
| void CreateSessionDoesNotCrash(int int_init_data_type, |
| const std::vector<uint8_t>& init_data) { |
| media::EmeInitDataType init_data_type; |
| switch (int_init_data_type) { |
| case 1: |
| init_data_type = media::EmeInitDataType::WEBM; |
| break; |
| case 2: |
| init_data_type = media::EmeInitDataType::CENC; |
| break; |
| case 3: |
| init_data_type = media::EmeInitDataType::KEYIDS; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| |
| // Create an AesDecryptor. Ignore any messages that may be generated. |
| auto aes_decryptor = base::MakeRefCounted<media::AesDecryptor>( |
| base::DoNothing(), base::DoNothing(), base::DoNothing(), |
| base::DoNothing()); |
| |
| // Create a session. Ignore the result of the promise as most often it will be |
| // rejected due to an error. However, if a session was created, save the |
| // session_id so it can be closed. |
| std::string session_id; |
| auto create_promise = |
| std::make_unique<media::CdmCallbackPromise<std::string>>( |
| base::BindLambdaForTesting( |
| [&](const std::string& session) { session_id = session; }), |
| base::DoNothing()); |
| aes_decryptor->CreateSessionAndGenerateRequest( |
| media::CdmSessionType::kTemporary, init_data_type, init_data, |
| std::move(create_promise)); |
| |
| // If a session was created, free up any session resources. |
| if (!session_id.empty()) { |
| auto close_promise = std::make_unique<media::CdmCallbackPromise<>>( |
| base::DoNothing(), base::DoNothing()); |
| aes_decryptor->CloseSession(session_id, std::move(close_promise)); |
| } |
| } |
| |
| // Call Update() with |response|. As UpdateSession takes a JSON, passing |
| // |response| as a string to make specifying seeds easier. Test will check |
| // parsing of JWK. |
| void UpdateSessionDoesNotCrash(const std::string& response) { |
| // Create an AesDecryptor. Ignore any messages that may be generated. |
| auto aes_decryptor = base::MakeRefCounted<media::AesDecryptor>( |
| base::DoNothing(), base::DoNothing(), base::DoNothing(), |
| base::DoNothing()); |
| |
| // Create a session. We need to keep track of the session_id as Update() needs |
| // a valid session_id. Use WEBM init_data as it's the simplest. |
| std::string session_id; |
| std::vector<uint8_t> key_id(std::begin(webm_init_data), |
| std::end(webm_init_data)); |
| auto create_promise = |
| std::make_unique<media::CdmCallbackPromise<std::string>>( |
| base::BindLambdaForTesting( |
| [&](const std::string& session) { session_id = session; }), |
| base::DoNothing()); |
| aes_decryptor->CreateSessionAndGenerateRequest( |
| media::CdmSessionType::kTemporary, media::EmeInitDataType::WEBM, key_id, |
| std::move(create_promise)); |
| EXPECT_GT(session_id.length(), 0ul); |
| |
| // Now try UpdateSession with the fuzzed data. Don't bother checking the |
| // result of the promise, as most often it will be rejected. |
| std::vector<uint8_t> data(std::begin(response), std::end(response)); |
| auto update_promise = std::make_unique<media::CdmCallbackPromise<>>( |
| base::DoNothing(), base::DoNothing()); |
| aes_decryptor->UpdateSession(session_id, data, std::move(update_promise)); |
| |
| // Free up any session resources. |
| auto close_promise = std::make_unique<media::CdmCallbackPromise<>>( |
| base::DoNothing(), base::DoNothing()); |
| aes_decryptor->CloseSession(session_id, std::move(close_promise)); |
| } |
| |
| // Decrypts |data| and checks that it succeeds without crashing. The data is |
| // assumed to have |clear_bytes| in the clear followed by |encrypted_bytes| that |
| // need to be decrypted, repeated up to the length of |data|. A new session is |
| // created, and initialized with key AAECAw(base64). Decryption will produce |
| // random data, but at least we can verify that it doesn't crash. |
| void DecryptDoesNotCrash(std::size_t clear_bytes, |
| std::size_t encrypted_bytes, |
| const std::vector<uint8_t>& data) { |
| // Create an AesDecryptor. Ignore any messages that may be generated. |
| auto aes_decryptor = base::MakeRefCounted<media::AesDecryptor>( |
| base::DoNothing(), base::DoNothing(), base::DoNothing(), |
| base::DoNothing()); |
| |
| // Create a session. We need to keep track of the session_id as Update() needs |
| // a valid session_id. |
| std::string session_id; |
| std::vector<uint8_t> key_id(std::begin(webm_init_data), |
| std::end(webm_init_data)); |
| auto create_promise = |
| std::make_unique<media::CdmCallbackPromise<std::string>>( |
| base::BindLambdaForTesting( |
| [&](const std::string& session) { session_id = session; }), |
| base::DoNothing()); |
| aes_decryptor->CreateSessionAndGenerateRequest( |
| media::CdmSessionType::kTemporary, media::EmeInitDataType::WEBM, key_id, |
| std::move(create_promise)); |
| EXPECT_GT(session_id.length(), 0ul); |
| |
| // Now UpdateSession with |kKeyAsJWK|. This uses key |
| // 0x0405060708090a0b0c0d0e0f10111213 to decrypt the data. Don't bother |
| // checking the result of the promise. (Using pop_back() to remove the \0 at |
| // the end of the string.) |
| std::vector<uint8_t> response(std::begin(kKeyAsJWK), std::end(kKeyAsJWK)); |
| response.pop_back(); |
| auto update_promise = std::make_unique<media::CdmCallbackPromise<>>( |
| base::DoNothing(), base::DoNothing()); |
| aes_decryptor->UpdateSession(session_id, response, std::move(update_promise)); |
| |
| // Create the subsample array. Each subsample will have |clear_bytes| in the |
| // clear followed by |encrypted_bytes| that need to be decrypted, repeated up |
| // to the length of |data|. If one value is 0, use a single subsample for the |
| // whole buffer. |
| std::vector<media::SubsampleEntry> subsamples; |
| std::size_t length = data.size(); |
| if (clear_bytes == 0ul) { |
| // Assume the whole buffer is encrypted. |
| subsamples.emplace_back(0ul, length); |
| } else if (encrypted_bytes == 0ul) { |
| // Assume the whole buffer is clear. |
| subsamples.emplace_back(length, 0ul); |
| } else { |
| while (length > 0ul) { |
| std::size_t clear = std::min(clear_bytes, length); |
| length -= clear; |
| std::size_t encrypted = std::min(encrypted_bytes, length); |
| length -= encrypted; |
| subsamples.emplace_back(clear, encrypted); |
| } |
| } |
| |
| // Now attempt to decrypt the fuzzed data. Seed data should be properly |
| // encrypted buffer. |
| auto encrypted_buffer = |
| base::MakeRefCounted<media::DecoderBuffer>(data.size()); |
| memcpy(encrypted_buffer->writable_data(), data.data(), data.size()); |
| std::string key_id_string(std::begin(key_id), std::end(key_id)); |
| std::string iv_string(std::begin(kIv), std::end(kIv)); |
| encrypted_buffer->set_decrypt_config(media::DecryptConfig::CreateCencConfig( |
| key_id_string, iv_string, subsamples)); |
| media::Decryptor::Status result; |
| aes_decryptor->Decrypt( |
| media::Decryptor::kVideo, encrypted_buffer, |
| base::BindLambdaForTesting( |
| [&](media::Decryptor::Status status, |
| scoped_refptr<media::DecoderBuffer>) { result = status; })); |
| // Decrypt should always succeed. Data will be random. |
| EXPECT_EQ(result, media::Decryptor::Status::kSuccess); |
| |
| // Free up any session resources. |
| auto close_promise = std::make_unique<media::CdmCallbackPromise<>>( |
| base::DoNothing(), base::DoNothing()); |
| aes_decryptor->CloseSession(session_id, std::move(close_promise)); |
| } |
| |
| // Note that most functions check that something is specified, so setting |
| // MinSize to 1. |
| |
| // Seed data for CreateSession is valid WEBM, CENC, and KEYIDS init data. |
| // No support for "enum class" EmeInitDataType, so using an int (1-3) instead. |
| FUZZ_TEST(AesDecryptorFuzzTests, CreateSessionDoesNotCrash) |
| .WithDomains(fuzztest::InRange<int>(1, 3), |
| fuzztest::Arbitrary<std::vector<uint8_t>>().WithMinSize(1)) |
| .WithSeeds({{1, std::vector<uint8_t>(std::begin(webm_init_data), |
| std::end(webm_init_data))}, |
| {2, std::vector<uint8_t>(std::begin(cenc_init_data), |
| std::end(cenc_init_data))}, |
| {3, std::vector<uint8_t>(std::begin(keyids_init_data), |
| std::end(keyids_init_data))}}); |
| |
| // Seed data for UpdateSession is valid JWK. |
| FUZZ_TEST(AesDecryptorFuzzTests, UpdateSessionDoesNotCrash) |
| .WithDomains(fuzztest::Arbitrary<std::string>().WithMinSize(1)) |
| .WithSeeds({kKeyAsJWK, kKeyAlternateAsJWK, kWrongKeyAsJWK}); |
| |
| // Seed data for Decrypt is a fully encrypted data. First parameter is number of |
| // clear bytes per subsample, second is number of encrypted bytes per subsample. |
| FUZZ_TEST(AesDecryptorFuzzTests, DecryptDoesNotCrash) |
| .WithDomains(fuzztest::InRange<std::size_t>(0, 100), |
| fuzztest::InRange<std::size_t>(0, 100), |
| fuzztest::Arbitrary<std::vector<uint8_t>>().WithMinSize(1)) |
| .WithSeeds({{0ul, sizeof(kEncryptedData), |
| std::vector<uint8_t>(std::begin(kEncryptedData), |
| std::end(kEncryptedData))}}); |