Add ability to delete plugin private data

Currently encrypted media content licenses are stored in plugin
private data (as decryption happens in a pepper plugin). Add the
ability to erase the data on request.

As the data is organized by origin and plugin, all files for a
particular origin/plugin combination are removed if any of the files
matches the time criteria. The existing plugins only use 1 file
currently, but this should avoid issues if the plugins change to
use multiple files.

The UI is still being debated, so this is simply adding the ability
to delete the data once called.

BUG=607631
TEST=new content_unittests pass

Review-Url: https://codereview.chromium.org/1979733002
Cr-Commit-Position: refs/heads/master@{#399322}
diff --git a/content/browser/plugin_private_storage_helper.cc b/content/browser/plugin_private_storage_helper.cc
new file mode 100644
index 0000000..0236932
--- /dev/null
+++ b/content/browser/plugin_private_storage_helper.cc
@@ -0,0 +1,415 @@
+// Copyright 2016 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 "content/browser/plugin_private_storage_helper.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <memory>
+#include <set>
+#include <string>
+
+#include "base/bind.h"
+#include "base/compiler_specific.h"
+#include "base/files/file.h"
+#include "base/files/file_enumerator.h"
+#include "base/files/file_path.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/stl_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "content/public/browser/browser_thread.h"
+#include "ppapi/shared_impl/ppapi_constants.h"
+#include "storage/browser/fileapi/async_file_util.h"
+#include "storage/browser/fileapi/async_file_util_adapter.h"
+#include "storage/browser/fileapi/file_system_context.h"
+#include "storage/browser/fileapi/isolated_context.h"
+#include "storage/browser/fileapi/obfuscated_file_util.h"
+#include "storage/common/fileapi/file_system_util.h"
+
+namespace content {
+
+namespace {
+
+std::string StringTypeToString(const base::FilePath::StringType& value) {
+#if defined(OS_POSIX)
+  return value;
+#elif defined(OS_WIN)
+  return base::WideToUTF8(value);
+#endif
+}
+
+// Helper for checking the plugin private data for a specified origin and
+// plugin for the existance of any file that matches the time range specified.
+// All of the operations in this class are done on the IO thread.
+//
+// This class keeps track of outstanding async requests it generates, and does
+// not call |callback_| until they all respond (and thus don't need to worry
+// about lifetime of |this| in the async requests). If the data for the origin
+// needs to be deleted, it needs to be done on the file task runner, so we
+// want to ensure that there are no pending requests that may prevent the
+// date from being deleted.
+class PluginPrivateDataByOriginChecker {
+ public:
+  PluginPrivateDataByOriginChecker(
+      storage::FileSystemContext* filesystem_context,
+      const GURL& origin,
+      const std::string& plugin_name,
+      const base::Time begin,
+      const base::Time end,
+      const base::Callback<void(bool, const GURL&)>& callback)
+      : filesystem_context_(filesystem_context),
+        origin_(origin),
+        plugin_name_(plugin_name),
+        begin_(begin),
+        end_(end),
+        callback_(callback) {
+    // Create the filesystem ID.
+    fsid_ = storage::IsolatedContext::GetInstance()
+                ->RegisterFileSystemForVirtualPath(
+                    storage::kFileSystemTypePluginPrivate,
+                    ppapi::kPluginPrivateRootName, base::FilePath());
+  }
+  ~PluginPrivateDataByOriginChecker() {}
+
+  // Checks the files contained in the plugin private filesystem for |origin_|
+  // and |plugin_name_| for any file whose last modified time is between
+  // |begin_| and |end_|. |callback_| is called when all actions are complete
+  // with true and the origin if any such file is found, false and empty GURL
+  // otherwise.
+  void CheckFilesOnIOThread();
+
+ private:
+  void OnFileSystemOpened(base::File::Error result);
+  void OnDirectoryRead(const std::string& root,
+                       base::File::Error result,
+                       const storage::AsyncFileUtil::EntryList& file_list,
+                       bool has_more);
+  void OnFileInfo(const std::string& file_name,
+                  base::File::Error result,
+                  const base::File::Info& file_info);
+
+  // Keeps track of the pending work. When |task_count_| goes to 0 then
+  // |callback_| is called and this helper object is destroyed.
+  void IncrementTaskCount();
+  void DecrementTaskCount();
+
+  // Not owned by this object. Caller is responsible for keeping the
+  // FileSystemContext alive until |callback_| is called.
+  storage::FileSystemContext* filesystem_context_;
+
+  const GURL origin_;
+  const std::string plugin_name_;
+  const base::Time begin_;
+  const base::Time end_;
+  const base::Callback<void(bool, const GURL&)> callback_;
+  std::string fsid_;
+  int task_count_ = 0;
+
+  // Keep track if the data for this origin needs to be deleted due to
+  // any file found that has last modified time between |begin_| and |end_|.
+  bool delete_this_origin_data_ = false;
+};
+
+void PluginPrivateDataByOriginChecker::CheckFilesOnIOThread() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DCHECK(storage::ValidateIsolatedFileSystemId(fsid_));
+
+  IncrementTaskCount();
+  filesystem_context_->OpenPluginPrivateFileSystem(
+      origin_, storage::kFileSystemTypePluginPrivate, fsid_, plugin_name_,
+      storage::OPEN_FILE_SYSTEM_FAIL_IF_NONEXISTENT,
+      base::Bind(&PluginPrivateDataByOriginChecker::OnFileSystemOpened,
+                 base::Unretained(this)));
+}
+
+void PluginPrivateDataByOriginChecker::OnFileSystemOpened(
+    base::File::Error result) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DVLOG(3) << "Opened filesystem for " << origin_ << ":" << plugin_name_
+           << ", result: " << result;
+
+  // If we can't open the directory, we can't delete files so simply return.
+  if (result != base::File::FILE_OK) {
+    DecrementTaskCount();
+    return;
+  }
+
+  storage::AsyncFileUtil* file_util = filesystem_context_->GetAsyncFileUtil(
+      storage::kFileSystemTypePluginPrivate);
+  std::string root = storage::GetIsolatedFileSystemRootURIString(
+      origin_, fsid_, ppapi::kPluginPrivateRootName);
+  std::unique_ptr<storage::FileSystemOperationContext> operation_context =
+      base::WrapUnique(
+          new storage::FileSystemOperationContext(filesystem_context_));
+  file_util->ReadDirectory(
+      std::move(operation_context), filesystem_context_->CrackURL(GURL(root)),
+      base::Bind(&PluginPrivateDataByOriginChecker::OnDirectoryRead,
+                 base::Unretained(this), root));
+}
+
+void PluginPrivateDataByOriginChecker::OnDirectoryRead(
+    const std::string& root,
+    base::File::Error result,
+    const storage::AsyncFileUtil::EntryList& file_list,
+    bool has_more) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DVLOG(3) << __FUNCTION__ << " result: " << result
+           << ", #files: " << file_list.size();
+
+  // Quit if there is an error.
+  if (result != base::File::FILE_OK) {
+    DLOG(ERROR) << "Unable to read directory for " << origin_ << ":"
+                << plugin_name_;
+    DecrementTaskCount();
+    return;
+  }
+
+  // No error, process the files returned. No need to do this if we have
+  // already decided to delete all the data for this origin.
+  if (!delete_this_origin_data_) {
+    storage::AsyncFileUtil* file_util = filesystem_context_->GetAsyncFileUtil(
+        storage::kFileSystemTypePluginPrivate);
+    for (const auto& file : file_list) {
+      DVLOG(3) << __FUNCTION__ << " file: " << file.name;
+      DCHECK(!file.is_directory);  // Nested directories not implemented.
+
+      std::unique_ptr<storage::FileSystemOperationContext> operation_context =
+          base::WrapUnique(
+              new storage::FileSystemOperationContext(filesystem_context_));
+      storage::FileSystemURL file_url = filesystem_context_->CrackURL(
+          GURL(root + StringTypeToString(file.name)));
+      IncrementTaskCount();
+      file_util->GetFileInfo(
+          std::move(operation_context), file_url,
+          storage::FileSystemOperation::GET_METADATA_FIELD_SIZE |
+              storage::FileSystemOperation::GET_METADATA_FIELD_LAST_MODIFIED,
+          base::Bind(&PluginPrivateDataByOriginChecker::OnFileInfo,
+                     base::Unretained(this), StringTypeToString(file.name)));
+    }
+  }
+
+  // If there are more files in this directory, wait for the next call.
+  if (has_more)
+    return;
+
+  DecrementTaskCount();
+}
+
+void PluginPrivateDataByOriginChecker::OnFileInfo(
+    const std::string& file_name,
+    base::File::Error result,
+    const base::File::Info& file_info) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+
+  if (result == base::File::FILE_OK) {
+    DVLOG(3) << __FUNCTION__ << " name: " << file_name
+             << ", size: " << file_info.size
+             << ", modified: " << file_info.last_modified;
+    if (file_info.last_modified >= begin_ && file_info.last_modified <= end_)
+      delete_this_origin_data_ = true;
+  }
+
+  DecrementTaskCount();
+}
+
+void PluginPrivateDataByOriginChecker::IncrementTaskCount() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  ++task_count_;
+}
+
+void PluginPrivateDataByOriginChecker::DecrementTaskCount() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DCHECK_GT(task_count_, 0);
+  --task_count_;
+  if (task_count_)
+    return;
+
+  // If there are no more tasks in progress, then run |callback_| on the
+  // proper thread.
+  filesystem_context_->default_file_task_runner()->PostTask(
+      FROM_HERE, base::Bind(callback_, delete_this_origin_data_, origin_));
+  delete this;
+}
+
+// Helper for deleting the plugin private data.
+// All of the operations in this class are done on the file task runner.
+class PluginPrivateDataDeletionHelper {
+ public:
+  PluginPrivateDataDeletionHelper(
+      scoped_refptr<storage::FileSystemContext> filesystem_context,
+      const base::Time begin,
+      const base::Time end,
+      const base::Closure& callback)
+      : filesystem_context_(std::move(filesystem_context)),
+        begin_(begin),
+        end_(end),
+        callback_(callback) {}
+  ~PluginPrivateDataDeletionHelper() {}
+
+  void CheckOriginsOnFileTaskRunner(const std::set<GURL>& origins);
+
+ private:
+  // Keeps track of the pending work. When |task_count_| goes to 0 then
+  // |callback_| is called and this helper object is destroyed.
+  void IncrementTaskCount();
+  void DecrementTaskCount(bool delete_data_for_origin, const GURL& origin);
+
+  // Keep a reference to FileSystemContext until we are done with it.
+  scoped_refptr<storage::FileSystemContext> filesystem_context_;
+
+  const base::Time begin_;
+  const base::Time end_;
+  const base::Closure callback_;
+  int task_count_ = 0;
+};
+
+void PluginPrivateDataDeletionHelper::CheckOriginsOnFileTaskRunner(
+    const std::set<GURL>& origins) {
+  DCHECK(filesystem_context_->default_file_task_runner()
+             ->RunsTasksOnCurrentThread());
+  IncrementTaskCount();
+
+  base::Callback<void(bool, const GURL&)> decrement_callback =
+      base::Bind(&PluginPrivateDataDeletionHelper::DecrementTaskCount,
+                 base::Unretained(this));
+  storage::AsyncFileUtil* async_file_util =
+      filesystem_context_->GetAsyncFileUtil(
+          storage::kFileSystemTypePluginPrivate);
+  storage::ObfuscatedFileUtil* obfuscated_file_util =
+      static_cast<storage::ObfuscatedFileUtil*>(
+          static_cast<storage::AsyncFileUtilAdapter*>(async_file_util)
+              ->sync_file_util());
+  for (const auto& origin : origins) {
+    // Determine the available plugin private filesystem directories
+    // for this origin.
+    base::File::Error error;
+    base::FilePath path = obfuscated_file_util->GetDirectoryForOriginAndType(
+        origin, "", false, &error);
+    if (error != base::File::FILE_OK) {
+      DLOG(ERROR) << "Unable to read directory for " << origin;
+      continue;
+    }
+
+    // Currently the plugin private filesystem is only used by Encrypted
+    // Media Content Decryption Modules, which are treated as pepper plugins.
+    // Each CDM gets a directory based on the mimetype (e.g. plugin
+    // application/x-ppapi-widevine-cdm uses directory
+    // application_x-ppapi-widevine-cdm). Enumerate through the set of
+    // directories so that data from any CDM used by this origin is deleted.
+    base::FileEnumerator file_enumerator(path, false,
+                                         base::FileEnumerator::DIRECTORIES);
+    for (base::FilePath plugin_path = file_enumerator.Next();
+         !plugin_path.empty(); plugin_path = file_enumerator.Next()) {
+      IncrementTaskCount();
+      PluginPrivateDataByOriginChecker* helper =
+          new PluginPrivateDataByOriginChecker(
+              filesystem_context_.get(), origin.GetOrigin(),
+              plugin_path.BaseName().MaybeAsASCII(), begin_, end_,
+              decrement_callback);
+      BrowserThread::PostTask(
+          BrowserThread::IO, FROM_HERE,
+          base::Bind(&PluginPrivateDataByOriginChecker::CheckFilesOnIOThread,
+                     base::Unretained(helper)));
+
+      // |helper| will delete itself when it is done.
+    }
+  }
+
+  // Cancels out the call to IncrementTaskCount() at the start of this method.
+  // If there are no origins specified then this will cause this helper to
+  // be destroyed.
+  DecrementTaskCount(false, GURL());
+}
+
+void PluginPrivateDataDeletionHelper::IncrementTaskCount() {
+  DCHECK(filesystem_context_->default_file_task_runner()
+             ->RunsTasksOnCurrentThread());
+  ++task_count_;
+}
+
+void PluginPrivateDataDeletionHelper::DecrementTaskCount(
+    bool delete_data_for_origin,
+    const GURL& origin) {
+  DCHECK(filesystem_context_->default_file_task_runner()
+             ->RunsTasksOnCurrentThread());
+
+  // Since the PluginPrivateDataByOriginChecker runs on the IO thread,
+  // delete all the data for |origin| if needed.
+  if (delete_data_for_origin) {
+    DCHECK(!origin.is_empty());
+    DVLOG(3) << "Deleting plugin data for " << origin;
+    storage::FileSystemBackend* backend =
+        filesystem_context_->GetFileSystemBackend(
+            storage::kFileSystemTypePluginPrivate);
+    storage::FileSystemQuotaUtil* quota_util = backend->GetQuotaUtil();
+    base::File::Error result = quota_util->DeleteOriginDataOnFileTaskRunner(
+        filesystem_context_.get(), nullptr, origin,
+        storage::kFileSystemTypePluginPrivate);
+    ALLOW_UNUSED_LOCAL(result);
+    DLOG_IF(ERROR, result != base::File::FILE_OK)
+        << "Unable to delete the plugin data for " << origin;
+  }
+
+  DCHECK_GT(task_count_, 0);
+  --task_count_;
+  if (task_count_)
+    return;
+
+  // If there are no more tasks in progress, run |callback_| and then
+  // this helper can be deleted.
+  callback_.Run();
+  delete this;
+}
+
+}  // namespace
+
+void ClearPluginPrivateDataOnFileTaskRunner(
+    scoped_refptr<storage::FileSystemContext> filesystem_context,
+    const GURL& storage_origin,
+    const base::Time begin,
+    const base::Time end,
+    const base::Closure& callback) {
+  DCHECK(filesystem_context->default_file_task_runner()
+             ->RunsTasksOnCurrentThread());
+  DVLOG(3) << "Clearing plugin data for origin: " << storage_origin;
+
+  storage::FileSystemBackend* backend =
+      filesystem_context->GetFileSystemBackend(
+          storage::kFileSystemTypePluginPrivate);
+  storage::FileSystemQuotaUtil* quota_util = backend->GetQuotaUtil();
+
+  // Determine the set of origins used.
+  std::set<GURL> origins;
+  quota_util->GetOriginsForTypeOnFileTaskRunner(
+      storage::kFileSystemTypePluginPrivate, &origins);
+
+  if (origins.empty()) {
+    // No origins, so nothing to do.
+    callback.Run();
+    return;
+  }
+
+  // If a specific origin is provided, then check that it is in the list
+  // returned and remove all the other origins.
+  if (!storage_origin.is_empty()) {
+    if (!ContainsKey(origins, storage_origin)) {
+      // Nothing matches, so nothing to do.
+      callback.Run();
+      return;
+    }
+
+    // List should only contain the one value that matches.
+    origins.clear();
+    origins.insert(storage_origin);
+  }
+
+  PluginPrivateDataDeletionHelper* helper = new PluginPrivateDataDeletionHelper(
+      std::move(filesystem_context), begin, end, callback);
+  helper->CheckOriginsOnFileTaskRunner(origins);
+  // |helper| will delete itself when all origins have been checked.
+}
+
+}  // namespace content
diff --git a/content/browser/plugin_private_storage_helper.h b/content/browser/plugin_private_storage_helper.h
new file mode 100644
index 0000000..d23dba37
--- /dev/null
+++ b/content/browser/plugin_private_storage_helper.h
@@ -0,0 +1,37 @@
+// Copyright 2016 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.
+
+#ifndef CONTENT_BROWSER_PLUGIN_PRIVATE_STORAGE_HELPER_H_
+#define CONTENT_BROWSER_PLUGIN_PRIVATE_STORAGE_HELPER_H_
+
+#if !defined(ENABLE_PLUGINS)
+#error This file should only be included when plugins are enabled.
+#endif
+
+#include "base/callback_forward.h"
+#include "base/memory/ref_counted.h"
+#include "base/time/time.h"
+#include "url/gurl.h"
+
+namespace storage {
+class FileSystemContext;
+}
+
+namespace content {
+
+// Clear the plugin private filesystem data in |filesystem_context| for
+// |storage_origin| if any file has a last modified time between |begin|
+// and |end|. If |storage_origin| is not specified, then all available
+// origins are checked. |callback| is called when the operation is complete.
+// This must be called on the file task runner.
+void ClearPluginPrivateDataOnFileTaskRunner(
+    scoped_refptr<storage::FileSystemContext> filesystem_context,
+    const GURL& storage_origin,
+    const base::Time begin,
+    const base::Time end,
+    const base::Closure& callback);
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_PLUGIN_PRIVATE_STORAGE_HELPER_H_
diff --git a/content/browser/storage_partition_impl.cc b/content/browser/storage_partition_impl.cc
index 14cd612..f2a6daa 100644
--- a/content/browser/storage_partition_impl.cc
+++ b/content/browser/storage_partition_impl.cc
@@ -35,6 +35,10 @@
 #include "storage/browser/database/database_tracker.h"
 #include "storage/browser/quota/quota_manager.h"
 
+#if defined(ENABLE_PLUGINS)
+#include "content/browser/plugin_private_storage_helper.h"
+#endif  // defined(ENABLE_PLUGINS)
+
 namespace content {
 
 namespace {
@@ -315,6 +319,7 @@
       storage::QuotaManager* quota_manager,
       storage::SpecialStoragePolicy* special_storage_policy,
       WebRTCIdentityStore* webrtc_identity_store,
+      storage::FileSystemContext* filesystem_context,
       const base::Time begin,
       const base::Time end);
 
@@ -621,7 +626,8 @@
   helper->ClearDataOnUIThread(
       storage_origin, origin_matcher, cookie_matcher, GetPath(), rq_context,
       dom_storage_context_.get(), quota_manager_.get(),
-      special_storage_policy_.get(), webrtc_identity_store_.get(), begin, end);
+      special_storage_policy_.get(), webrtc_identity_store_.get(),
+      filesystem_context_.get(), begin, end);
 }
 
 void StoragePartitionImpl::
@@ -761,6 +767,7 @@
     storage::QuotaManager* quota_manager,
     storage::SpecialStoragePolicy* special_storage_policy,
     WebRTCIdentityStore* webrtc_identity_store,
+    storage::FileSystemContext* filesystem_context,
     const base::Time begin,
     const base::Time end) {
   DCHECK_NE(remove_mask, 0u);
@@ -841,6 +848,16 @@
                    decrement_callback));
   }
 
+#if defined(ENABLE_PLUGINS)
+  if (remove_mask & REMOVE_DATA_MASK_PLUGIN_PRIVATE_DATA) {
+    IncrementTaskCountOnUI();
+    filesystem_context->default_file_task_runner()->PostTask(
+        FROM_HERE, base::Bind(&ClearPluginPrivateDataOnFileTaskRunner,
+                              make_scoped_refptr(filesystem_context),
+                              storage_origin, begin, end, decrement_callback));
+  }
+#endif  // defined(ENABLE_PLUGINS)
+
   DecrementTaskCountOnUI();
 }
 
diff --git a/content/browser/storage_partition_impl_unittest.cc b/content/browser/storage_partition_impl_unittest.cc
index 6aa0c1b..a504280 100644
--- a/content/browser/storage_partition_impl_unittest.cc
+++ b/content/browser/storage_partition_impl_unittest.cc
@@ -30,6 +30,15 @@
 #include "storage/browser/quota/quota_manager.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+#if defined(ENABLE_PLUGINS)
+#include "ppapi/shared_impl/ppapi_constants.h"
+#include "storage/browser/fileapi/async_file_util.h"
+#include "storage/browser/fileapi/file_system_context.h"
+#include "storage/browser/fileapi/file_system_operation_context.h"
+#include "storage/browser/fileapi/isolated_context.h"
+#include "storage/common/fileapi/file_system_util.h"
+#endif  // defined(ENABLE_PLUGINS)
+
 using net::CanonicalCookie;
 
 namespace content {
@@ -44,6 +53,11 @@
 const char kTestOrigin3[] = "http://host3:1/";
 const char kTestOriginDevTools[] = "chrome-devtools://abcdefghijklmnopqrstuvw/";
 
+#if defined(ENABLE_PLUGINS)
+const char kWidevineCdmPluginId[] = "application_x-ppapi-widevine-cdm";
+const char kClearKeyCdmPluginId[] = "application_x-ppapi-clearkey-cdm";
+#endif  // defined(ENABLE_PLUGINS)
+
 const GURL kOrigin1(kTestOrigin1);
 const GURL kOrigin2(kTestOrigin2);
 const GURL kOrigin3(kTestOrigin3);
@@ -231,6 +245,199 @@
   DISALLOW_COPY_AND_ASSIGN(RemoveLocalStorageTester);
 };
 
+#if defined(ENABLE_PLUGINS)
+class RemovePluginPrivateDataTester {
+ public:
+  explicit RemovePluginPrivateDataTester(
+      storage::FileSystemContext* filesystem_context)
+      : filesystem_context_(filesystem_context) {}
+
+  // Add some files to the PluginPrivateFileSystem. They are created as follows:
+  //   kOrigin1 - ClearKey - 1 file - timestamp 10 days ago
+  //   kOrigin2 - Widevine - 2 files - timestamps now and 60 days ago
+  void AddPluginPrivateTestData() {
+    base::Time now = base::Time::Now();
+    base::Time ten_days_ago = now - base::TimeDelta::FromDays(10);
+    base::Time sixty_days_ago = now - base::TimeDelta::FromDays(60);
+
+    // Create a PluginPrivateFileSystem for ClearKey and add a single file
+    // with a timestamp of 1 day ago.
+    std::string clearkey_fsid =
+        CreateFileSystem(kClearKeyCdmPluginId, kOrigin1);
+    clearkey_file_ = CreateFile(kOrigin1, clearkey_fsid, "foo");
+    SetFileTimestamp(clearkey_file_, ten_days_ago);
+
+    // Create a second PluginPrivateFileSystem for Widevine and add two files
+    // with different times.
+    std::string widevine_fsid =
+        CreateFileSystem(kWidevineCdmPluginId, kOrigin2);
+    storage::FileSystemURL widevine_file1 =
+        CreateFile(kOrigin2, widevine_fsid, "bar1");
+    storage::FileSystemURL widevine_file2 =
+        CreateFile(kOrigin2, widevine_fsid, "bar2");
+    SetFileTimestamp(widevine_file1, now);
+    SetFileTimestamp(widevine_file2, sixty_days_ago);
+  }
+
+  // Returns true, if the given origin exists in a PluginPrivateFileSystem.
+  bool DataExistsForOrigin(const GURL& origin) {
+    AwaitCompletionHelper await_completion;
+    bool data_exists_for_origin = false;
+    filesystem_context_->default_file_task_runner()->PostTask(
+        FROM_HERE, base::Bind(&RemovePluginPrivateDataTester::
+                                  CheckIfDataExistsForOriginOnFileTaskRunner,
+                              base::Unretained(this), origin,
+                              &data_exists_for_origin, &await_completion));
+    await_completion.BlockUntilNotified();
+    return data_exists_for_origin;
+  }
+
+  // Opens the file created for ClearKey (in kOrigin1) for writing. Caller
+  // needs to verify if the file was opened or not.
+  base::File OpenClearKeyFileForWrite() {
+    AwaitCompletionHelper await_completion;
+    base::File file;
+    storage::AsyncFileUtil* async_file_util =
+        filesystem_context_->GetAsyncFileUtil(
+            storage::kFileSystemTypePluginPrivate);
+    std::unique_ptr<storage::FileSystemOperationContext> operation_context =
+        base::WrapUnique(
+            new storage::FileSystemOperationContext(filesystem_context_));
+    async_file_util->CreateOrOpen(
+        std::move(operation_context), clearkey_file_,
+        base::File::FLAG_OPEN | base::File::FLAG_WRITE,
+        base::Bind(&RemovePluginPrivateDataTester::OnFileOpened,
+                   base::Unretained(this), &file, &await_completion));
+    await_completion.BlockUntilNotified();
+    return file;
+  }
+
+ private:
+  // Creates a PluginPrivateFileSystem for the |plugin_name| and |origin|
+  // provided. Returns the file system ID for the created
+  // PluginPrivateFileSystem.
+  std::string CreateFileSystem(const std::string& plugin_name,
+                               const GURL& origin) {
+    AwaitCompletionHelper await_completion;
+    std::string fsid = storage::IsolatedContext::GetInstance()
+                           ->RegisterFileSystemForVirtualPath(
+                               storage::kFileSystemTypePluginPrivate,
+                               ppapi::kPluginPrivateRootName, base::FilePath());
+    EXPECT_TRUE(storage::ValidateIsolatedFileSystemId(fsid));
+    filesystem_context_->OpenPluginPrivateFileSystem(
+        origin, storage::kFileSystemTypePluginPrivate, fsid, plugin_name,
+        storage::OPEN_FILE_SYSTEM_CREATE_IF_NONEXISTENT,
+        base::Bind(&RemovePluginPrivateDataTester::OnFileSystemOpened,
+                   base::Unretained(this), &await_completion));
+    await_completion.BlockUntilNotified();
+    return fsid;
+  }
+
+  // Creates a file named |file_name| in the PluginPrivateFileSystem identified
+  // by |origin| and |fsid|. Returns the URL for the created file. The file
+  // must not already exist or the test will fail.
+  storage::FileSystemURL CreateFile(const GURL& origin,
+                                    const std::string& fsid,
+                                    const std::string& file_name) {
+    AwaitCompletionHelper await_completion;
+    std::string root = storage::GetIsolatedFileSystemRootURIString(
+        origin, fsid, ppapi::kPluginPrivateRootName);
+    storage::FileSystemURL file_url =
+        filesystem_context_->CrackURL(GURL(root + file_name));
+    storage::AsyncFileUtil* file_util = filesystem_context_->GetAsyncFileUtil(
+        storage::kFileSystemTypePluginPrivate);
+    std::unique_ptr<storage::FileSystemOperationContext> operation_context =
+        base::WrapUnique(
+            new storage::FileSystemOperationContext(filesystem_context_));
+    operation_context->set_allowed_bytes_growth(
+        storage::QuotaManager::kNoLimit);
+    file_util->EnsureFileExists(
+        std::move(operation_context), file_url,
+        base::Bind(&RemovePluginPrivateDataTester::OnFileCreated,
+                   base::Unretained(this), &await_completion));
+    await_completion.BlockUntilNotified();
+    return file_url;
+  }
+
+  // Sets the last_access_time and last_modified_time to |time_stamp| on the
+  // file specified by |file_url|. The file must already exist.
+  void SetFileTimestamp(const storage::FileSystemURL& file_url,
+                        const base::Time& time_stamp) {
+    AwaitCompletionHelper await_completion;
+    storage::AsyncFileUtil* file_util = filesystem_context_->GetAsyncFileUtil(
+        storage::kFileSystemTypePluginPrivate);
+    std::unique_ptr<storage::FileSystemOperationContext> operation_context =
+        base::WrapUnique(
+            new storage::FileSystemOperationContext(filesystem_context_));
+    file_util->Touch(std::move(operation_context), file_url, time_stamp,
+                     time_stamp,
+                     base::Bind(&RemovePluginPrivateDataTester::OnFileTouched,
+                                base::Unretained(this), &await_completion));
+    await_completion.BlockUntilNotified();
+  }
+
+  void OnFileSystemOpened(AwaitCompletionHelper* await_completion,
+                          base::File::Error result) {
+    EXPECT_EQ(base::File::FILE_OK, result) << base::File::ErrorToString(result);
+    await_completion->Notify();
+  }
+
+  void OnFileCreated(AwaitCompletionHelper* await_completion,
+                     base::File::Error result,
+                     bool created) {
+    EXPECT_EQ(base::File::FILE_OK, result) << base::File::ErrorToString(result);
+    EXPECT_TRUE(created);
+    await_completion->Notify();
+  }
+
+  void OnFileTouched(AwaitCompletionHelper* await_completion,
+                     base::File::Error result) {
+    EXPECT_EQ(base::File::FILE_OK, result) << base::File::ErrorToString(result);
+    await_completion->Notify();
+  }
+
+  void OnFileOpened(base::File* file_result,
+                    AwaitCompletionHelper* await_completion,
+                    base::File file,
+                    const base::Closure& on_close_callback) {
+    *file_result = std::move(file);
+    await_completion->Notify();
+  }
+
+  // If |origin| exists in the PluginPrivateFileSystem, set
+  // |data_exists_for_origin| to true, false otherwise.
+  void CheckIfDataExistsForOriginOnFileTaskRunner(
+      const GURL& origin,
+      bool* data_exists_for_origin,
+      AwaitCompletionHelper* await_completion) {
+    storage::FileSystemBackend* backend =
+        filesystem_context_->GetFileSystemBackend(
+            storage::kFileSystemTypePluginPrivate);
+    storage::FileSystemQuotaUtil* quota_util = backend->GetQuotaUtil();
+
+    // Determine the set of origins used.
+    std::set<GURL> origins;
+    quota_util->GetOriginsForTypeOnFileTaskRunner(
+        storage::kFileSystemTypePluginPrivate, &origins);
+    *data_exists_for_origin = origins.find(origin) != origins.end();
+
+    // AwaitCompletionHelper and MessageLoop don't work on a
+    // SequencedTaskRunner, so post a task on the IO thread.
+    BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
+                            base::Bind(&AwaitCompletionHelper::Notify,
+                                       base::Unretained(await_completion)));
+  }
+
+  // We don't own this pointer.
+  storage::FileSystemContext* filesystem_context_;
+
+  // Keep track of the URL for the ClearKey file so that it can be written to.
+  storage::FileSystemURL clearkey_file_;
+
+  DISALLOW_COPY_AND_ASSIGN(RemovePluginPrivateDataTester);
+};
+#endif  // defined(ENABLE_PLUGINS)
+
 bool IsWebSafeSchemeForTest(const std::string& scheme) {
   return scheme == "http";
 }
@@ -344,14 +551,27 @@
       time, time, run_loop->QuitClosure());
 }
 
+#if defined(ENABLE_PLUGINS)
+void ClearPluginPrivateData(content::StoragePartition* partition,
+                            const GURL& storage_origin,
+                            const base::Time delete_begin,
+                            const base::Time delete_end,
+                            base::RunLoop* run_loop) {
+  partition->ClearData(
+      StoragePartitionImpl::REMOVE_DATA_MASK_PLUGIN_PRIVATE_DATA,
+      StoragePartition::QUOTA_MANAGED_STORAGE_MASK_ALL, storage_origin,
+      StoragePartition::OriginMatcherFunction(), delete_begin, delete_end,
+      run_loop->QuitClosure());
+}
+#endif  // defined(ENABLE_PLUGINS)
+
 }  // namespace
 
 class StoragePartitionImplTest : public testing::Test {
  public:
   StoragePartitionImplTest()
       : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP),
-        browser_context_(new TestBrowserContext()) {
-  }
+        browser_context_(new TestBrowserContext()) {}
 
   MockQuotaManager* GetMockManager() {
     if (!quota_manager_.get()) {
@@ -975,6 +1195,102 @@
   EXPECT_TRUE(tester.DOMStorageExistsForOrigin(kOrigin3));
 }
 
+#if defined(ENABLE_PLUGINS)
+TEST_F(StoragePartitionImplTest, RemovePluginPrivateDataForever) {
+  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
+      BrowserContext::GetDefaultStoragePartition(browser_context()));
+
+  RemovePluginPrivateDataTester tester(partition->GetFileSystemContext());
+  tester.AddPluginPrivateTestData();
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin2));
+
+  base::RunLoop run_loop;
+  base::ThreadTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::Bind(&ClearPluginPrivateData, partition, GURL(),
+                            base::Time(), base::Time::Max(), &run_loop));
+  run_loop.Run();
+
+  EXPECT_FALSE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_FALSE(tester.DataExistsForOrigin(kOrigin2));
+}
+
+TEST_F(StoragePartitionImplTest, RemovePluginPrivateDataLastWeek) {
+  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
+      BrowserContext::GetDefaultStoragePartition(browser_context()));
+  base::Time a_week_ago = base::Time::Now() - base::TimeDelta::FromDays(7);
+
+  RemovePluginPrivateDataTester tester(partition->GetFileSystemContext());
+  tester.AddPluginPrivateTestData();
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin2));
+
+  base::RunLoop run_loop;
+  base::ThreadTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::Bind(&ClearPluginPrivateData, partition, GURL(),
+                            a_week_ago, base::Time::Max(), &run_loop));
+  run_loop.Run();
+
+  // Origin1 has 1 file from 10 days ago, so it should remain around.
+  // Origin2 has a current file, so it should be removed (even though the
+  // second file is much older).
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_FALSE(tester.DataExistsForOrigin(kOrigin2));
+}
+
+TEST_F(StoragePartitionImplTest, RemovePluginPrivateDataForOrigin) {
+  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
+      BrowserContext::GetDefaultStoragePartition(browser_context()));
+
+  RemovePluginPrivateDataTester tester(partition->GetFileSystemContext());
+  tester.AddPluginPrivateTestData();
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin2));
+
+  base::RunLoop run_loop;
+  base::ThreadTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::Bind(&ClearPluginPrivateData, partition, kOrigin1,
+                            base::Time(), base::Time::Max(), &run_loop));
+  run_loop.Run();
+
+  // Only Origin1 should be deleted.
+  EXPECT_FALSE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin2));
+}
+
+TEST_F(StoragePartitionImplTest, RemovePluginPrivateDataWhileWriting) {
+  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
+      BrowserContext::GetDefaultStoragePartition(browser_context()));
+
+  RemovePluginPrivateDataTester tester(partition->GetFileSystemContext());
+  tester.AddPluginPrivateTestData();
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_TRUE(tester.DataExistsForOrigin(kOrigin2));
+
+  const char test_data[] = {0, 1, 2, 3, 4, 5};
+  base::File file = tester.OpenClearKeyFileForWrite();
+  EXPECT_TRUE(file.IsValid());
+  EXPECT_EQ(static_cast<int>(arraysize(test_data)),
+            file.Write(0, test_data, arraysize(test_data)));
+
+  base::RunLoop run_loop;
+  base::ThreadTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::Bind(&ClearPluginPrivateData, partition, GURL(),
+                            base::Time(), base::Time::Max(), &run_loop));
+  run_loop.Run();
+
+  EXPECT_FALSE(tester.DataExistsForOrigin(kOrigin1));
+  EXPECT_FALSE(tester.DataExistsForOrigin(kOrigin2));
+
+  const char more_data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+  EXPECT_EQ(static_cast<int>(arraysize(more_data)),
+            file.WriteAtCurrentPos(more_data, arraysize(more_data)));
+
+  base::File file2 = tester.OpenClearKeyFileForWrite();
+  EXPECT_FALSE(file2.IsValid());
+}
+#endif  // defined(ENABLE_PLUGINS)
+
 TEST(StoragePartitionImplStaticTest, CreatePredicateForHostCookies) {
   GURL url("http://www.example.com/");
   GURL url2("https://www.example.com/");
diff --git a/content/content_browser.gypi b/content/content_browser.gypi
index 61814aa..9aca7c4 100644
--- a/content/content_browser.gypi
+++ b/content/content_browser.gypi
@@ -1812,6 +1812,8 @@
       'browser/plugin_content_origin_whitelist.h',
       'browser/plugin_data_remover_impl.cc',
       'browser/plugin_data_remover_impl.h',
+      'browser/plugin_private_storage_helper.cc',
+      'browser/plugin_private_storage_helper.h',
       'browser/plugin_service_impl.cc',
       'browser/plugin_service_impl.h',
       'browser/ppapi_plugin_process_host.cc',
diff --git a/content/public/browser/storage_partition.h b/content/public/browser/storage_partition.h
index 937e3ab7..6943bf5 100644
--- a/content/public/browser/storage_partition.h
+++ b/content/public/browser/storage_partition.h
@@ -86,6 +86,7 @@
     REMOVE_DATA_MASK_WEBRTC_IDENTITY = 1 << 7,
     REMOVE_DATA_MASK_SERVICE_WORKERS = 1 << 8,
     REMOVE_DATA_MASK_CACHE_STORAGE = 1 << 9,
+    REMOVE_DATA_MASK_PLUGIN_PRIVATE_DATA = 1 << 10,
     REMOVE_DATA_MASK_ALL = 0xFFFFFFFF,
 
     // Corresponds to storage::kStorageTypeTemporary.
diff --git a/ppapi/shared_impl/file_system_util.cc b/ppapi/shared_impl/file_system_util.cc
index 25650d1..a8a5263 100644
--- a/ppapi/shared_impl/file_system_util.cc
+++ b/ppapi/shared_impl/file_system_util.cc
@@ -5,6 +5,7 @@
 #include "ppapi/shared_impl/file_system_util.h"
 
 #include "base/logging.h"
+#include "ppapi/shared_impl/ppapi_constants.h"
 
 namespace ppapi {
 
@@ -26,7 +27,7 @@
     case PP_ISOLATEDFILESYSTEMTYPE_PRIVATE_CRX:
       return "crxfs";
     case PP_ISOLATEDFILESYSTEMTYPE_PRIVATE_PLUGINPRIVATE:
-      return "pluginprivate";
+      return kPluginPrivateRootName;
     default:
       NOTREACHED() << type;
       return std::string();
diff --git a/ppapi/shared_impl/ppapi_constants.h b/ppapi/shared_impl/ppapi_constants.h
index 118db25..c26d9a7 100644
--- a/ppapi/shared_impl/ppapi_constants.h
+++ b/ppapi/shared_impl/ppapi_constants.h
@@ -23,6 +23,9 @@
 const char kPowerSaverTestPluginName[] = "libpower_saver_test_plugin.so";
 #endif
 
+// Name of the root directory in the plugin private file system.
+const char kPluginPrivateRootName[] = "pluginprivate";
+
 }  // namespace ppapi
 
 #endif  // PPAPI_SHARED_IMPL_PPAPI_CONSTANTS_H_