| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/content_verify_job.h" |
| |
| #include <algorithm> |
| |
| #include "base/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/task/post_task.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "crypto/secure_hash.h" |
| #include "crypto/sha2.h" |
| #include "extensions/browser/content_hash_reader.h" |
| #include "extensions/browser/content_verifier.h" |
| #include "extensions/browser/content_verifier/content_hash.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| bool g_ignore_verification_for_tests = false; |
| ContentVerifyJob::TestObserver* g_content_verify_job_test_observer = NULL; |
| |
| class ScopedElapsedTimer { |
| public: |
| explicit ScopedElapsedTimer(base::TimeDelta* total) : total_(total) { |
| DCHECK(total_); |
| } |
| |
| ~ScopedElapsedTimer() { *total_ += timer.Elapsed(); } |
| |
| private: |
| // Some total amount of time we should add our elapsed time to at |
| // destruction. |
| base::TimeDelta* total_; |
| |
| // A timer for how long this object has been alive. |
| base::ElapsedTimer timer; |
| }; |
| |
| } // namespace |
| |
| ContentVerifyJob::ContentVerifyJob(const ExtensionId& extension_id, |
| const base::Version& extension_version, |
| const base::FilePath& extension_root, |
| const base::FilePath& relative_path, |
| FailureCallback failure_callback) |
| : done_reading_(false), |
| hashes_ready_(false), |
| total_bytes_read_(0), |
| current_block_(0), |
| current_hash_byte_count_(0), |
| extension_id_(extension_id), |
| extension_version_(extension_version), |
| extension_root_(extension_root), |
| relative_path_(relative_path), |
| failure_callback_(std::move(failure_callback)), |
| failed_(false) {} |
| |
| ContentVerifyJob::~ContentVerifyJob() { |
| UMA_HISTOGRAM_COUNTS_1M("ExtensionContentVerifyJob.TimeSpentUS", |
| time_spent_.InMicroseconds()); |
| } |
| |
| void ContentVerifyJob::Start(ContentVerifier* verifier) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| base::AutoLock auto_lock(lock_); |
| verifier->GetContentHash( |
| extension_id_, extension_root_, extension_version_, |
| true /* force_missing_computed_hashes_creation */, |
| base::BindOnce(&ContentVerifyJob::DidGetContentHashOnIO, this)); |
| } |
| |
| void ContentVerifyJob::DidGetContentHashOnIO( |
| scoped_refptr<const ContentHash> content_hash) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| base::AutoLock auto_lock(lock_); |
| if (g_content_verify_job_test_observer) |
| g_content_verify_job_test_observer->JobStarted(extension_id_, |
| relative_path_); |
| // Build |hash_reader_|. |
| base::PostTaskWithTraitsAndReplyWithResult( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE}, |
| base::BindOnce(&ContentHashReader::Create, relative_path_, content_hash), |
| base::BindOnce(&ContentVerifyJob::OnHashesReady, this)); |
| } |
| |
| void ContentVerifyJob::BytesRead(int count, const char* data) { |
| base::AutoLock auto_lock(lock_); |
| DCHECK(!done_reading_); |
| BytesReadImpl(count, data); |
| } |
| |
| void ContentVerifyJob::DoneReading() { |
| base::AutoLock auto_lock(lock_); |
| ScopedElapsedTimer timer(&time_spent_); |
| if (failed_) |
| return; |
| if (g_ignore_verification_for_tests) |
| return; |
| DCHECK(!done_reading_); |
| done_reading_ = true; |
| if (hashes_ready_) { |
| if (!FinishBlock()) { |
| DispatchFailureCallback(HASH_MISMATCH); |
| } else if (g_content_verify_job_test_observer) { |
| g_content_verify_job_test_observer->JobFinished(extension_id_, |
| relative_path_, NONE); |
| } |
| } |
| } |
| |
| void ContentVerifyJob::BytesReadImpl(int count, const char* data) { |
| ScopedElapsedTimer timer(&time_spent_); |
| if (failed_) |
| return; |
| if (g_ignore_verification_for_tests) |
| return; |
| if (!hashes_ready_) { |
| queue_.append(data, count); |
| return; |
| } |
| DCHECK_GE(count, 0); |
| int bytes_added = 0; |
| |
| while (bytes_added < count) { |
| if (current_block_ >= hash_reader_->block_count()) |
| return DispatchFailureCallback(HASH_MISMATCH); |
| |
| if (!current_hash_) { |
| current_hash_byte_count_ = 0; |
| current_hash_ = crypto::SecureHash::Create(crypto::SecureHash::SHA256); |
| } |
| // Compute how many bytes we should hash, and add them to the current hash. |
| int bytes_to_hash = |
| std::min(hash_reader_->block_size() - current_hash_byte_count_, |
| count - bytes_added); |
| DCHECK_GT(bytes_to_hash, 0); |
| current_hash_->Update(data + bytes_added, bytes_to_hash); |
| bytes_added += bytes_to_hash; |
| current_hash_byte_count_ += bytes_to_hash; |
| total_bytes_read_ += bytes_to_hash; |
| |
| // If we finished reading a block worth of data, finish computing the hash |
| // for it and make sure the expected hash matches. |
| if (current_hash_byte_count_ == hash_reader_->block_size() && |
| !FinishBlock()) { |
| DispatchFailureCallback(HASH_MISMATCH); |
| return; |
| } |
| } |
| } |
| |
| bool ContentVerifyJob::FinishBlock() { |
| DCHECK(!failed_); |
| if (current_hash_byte_count_ == 0) { |
| if (!done_reading_ || |
| // If we have checked all blocks already, then nothing else to do here. |
| current_block_ == hash_reader_->block_count()) { |
| return true; |
| } |
| } |
| if (!current_hash_) { |
| // This happens when we fail to read the resource. Compute empty content's |
| // hash in this case. |
| current_hash_ = crypto::SecureHash::Create(crypto::SecureHash::SHA256); |
| } |
| std::string final(crypto::kSHA256Length, 0); |
| current_hash_->Finish(base::data(final), final.size()); |
| current_hash_.reset(); |
| current_hash_byte_count_ = 0; |
| |
| int block = current_block_++; |
| |
| const std::string* expected_hash = NULL; |
| if (!hash_reader_->GetHashForBlock(block, &expected_hash) || |
| *expected_hash != final) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void ContentVerifyJob::OnHashesReady( |
| std::unique_ptr<const ContentHashReader> hash_reader) { |
| base::AutoLock auto_lock(lock_); |
| const bool success = hash_reader->succeeded(); |
| hash_reader_ = std::move(hash_reader); |
| |
| if (g_ignore_verification_for_tests) |
| return; |
| if (g_content_verify_job_test_observer) { |
| g_content_verify_job_test_observer->OnHashesReady(extension_id_, |
| relative_path_, success); |
| } |
| if (!success) { |
| if (!hash_reader_->has_content_hashes()) { |
| DispatchFailureCallback(MISSING_ALL_HASHES); |
| return; |
| } |
| |
| if (hash_reader_->file_missing_from_verified_contents()) { |
| // Ignore verification of non-existent resources. |
| if (g_content_verify_job_test_observer) { |
| g_content_verify_job_test_observer->JobFinished(extension_id_, |
| relative_path_, NONE); |
| } |
| return; |
| } |
| DispatchFailureCallback(NO_HASHES_FOR_FILE); |
| return; |
| } |
| |
| DCHECK(!failed_); |
| |
| hashes_ready_ = true; |
| if (!queue_.empty()) { |
| std::string tmp; |
| queue_.swap(tmp); |
| BytesReadImpl(tmp.size(), base::data(tmp)); |
| if (failed_) |
| return; |
| } |
| if (done_reading_) { |
| ScopedElapsedTimer timer(&time_spent_); |
| if (!FinishBlock()) { |
| DispatchFailureCallback(HASH_MISMATCH); |
| } else if (g_content_verify_job_test_observer) { |
| g_content_verify_job_test_observer->JobFinished(extension_id_, |
| relative_path_, NONE); |
| } |
| } |
| } |
| |
| // static |
| void ContentVerifyJob::SetIgnoreVerificationForTests(bool value) { |
| DCHECK_NE(g_ignore_verification_for_tests, value); |
| g_ignore_verification_for_tests = value; |
| } |
| |
| // static |
| void ContentVerifyJob::SetObserverForTests(TestObserver* observer) { |
| DCHECK(observer == nullptr || g_content_verify_job_test_observer == nullptr) |
| << "SetObserverForTests does not support interleaving. Observers should " |
| << "be set and then cleared one at a time."; |
| g_content_verify_job_test_observer = observer; |
| } |
| |
| void ContentVerifyJob::DispatchFailureCallback(FailureReason reason) { |
| DCHECK(!failed_); |
| failed_ = true; |
| if (!failure_callback_.is_null()) { |
| VLOG(1) << "job failed for " << extension_id_ << " " |
| << relative_path_.MaybeAsASCII() << " reason:" << reason; |
| std::move(failure_callback_).Run(reason); |
| } |
| if (g_content_verify_job_test_observer) { |
| g_content_verify_job_test_observer->JobFinished(extension_id_, |
| relative_path_, reason); |
| } |
| } |
| |
| } // namespace extensions |