resource_cache.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/policy/core/common/cloud/resource_cache.h"
6
7#include "base/base64.h"
8#include "base/callback.h"
9#include "base/file_util.h"
10#include "base/files/file_enumerator.h"
11#include "base/logging.h"
12#include "base/numerics/safe_conversions.h"
13#include "base/sequenced_task_runner.h"
14#include "base/strings/string_util.h"
15
16namespace policy {
17
18namespace {
19
20// Verifies that |value| is not empty and encodes it into base64url format,
21// which is safe to use as a file name on all platforms.
22bool Base64Encode(const std::string& value, std::string* encoded) {
23  DCHECK(!value.empty());
24  if (value.empty())
25    return false;
26  base::Base64Encode(value, encoded);
27  base::ReplaceChars(*encoded, "+", "-", encoded);
28  base::ReplaceChars(*encoded, "/", "_", encoded);
29  return true;
30}
31
32// Decodes all elements of |input| from base64url format and stores the decoded
33// elements in |output|.
34bool Base64Encode(const std::set<std::string>& input,
35                  std::set<std::string>* output) {
36  output->clear();
37  for (std::set<std::string>::const_iterator it = input.begin();
38       it != input.end(); ++it) {
39    std::string encoded;
40    if (!Base64Encode(*it, &encoded)) {
41      output->clear();
42      return false;
43    }
44    output->insert(encoded);
45  }
46  return true;
47}
48
49// Decodes |encoded| from base64url format and verifies that the result is not
50// emtpy.
51bool Base64Decode(const std::string& encoded, std::string* value) {
52  std::string buffer;
53  base::ReplaceChars(encoded, "-", "+", &buffer);
54  base::ReplaceChars(buffer, "_", "/", &buffer);
55  return base::Base64Decode(buffer, value) && !value->empty();
56}
57
58}  // namespace
59
60ResourceCache::ResourceCache(
61    const base::FilePath& cache_dir,
62    scoped_refptr<base::SequencedTaskRunner> task_runner)
63    : cache_dir_(cache_dir),
64      task_runner_(task_runner) {
65}
66
67ResourceCache::~ResourceCache() {
68  DCHECK(task_runner_->RunsTasksOnCurrentThread());
69}
70
71bool ResourceCache::Store(const std::string& key,
72                          const std::string& subkey,
73                          const std::string& data) {
74  DCHECK(task_runner_->RunsTasksOnCurrentThread());
75  base::FilePath subkey_path;
76  // Delete the file before writing to it. This ensures that the write does not
77  // follow a symlink planted at |subkey_path|, clobbering a file outside the
78  // cache directory. The mechanism is meant to foil file-system-level attacks
79  // where a symlink is planted in the cache directory before Chrome has
80  // started. An attacker controlling a process running concurrently with Chrome
81  // would be able to race against the protection by re-creating the symlink
82  // between these two calls. There is nothing in file_util that could be used
83  // to protect against such races, especially as the cache is cross-platform
84  // and therefore cannot use any POSIX-only tricks.
85  int size = base::checked_cast<int>(data.size());
86  return VerifyKeyPathAndGetSubkeyPath(key, true, subkey, &subkey_path) &&
87         base::DeleteFile(subkey_path, false) &&
88         (base::WriteFile(subkey_path, data.data(), size) == size);
89}
90
91bool ResourceCache::Load(const std::string& key,
92                         const std::string& subkey,
93                         std::string* data) {
94  DCHECK(task_runner_->RunsTasksOnCurrentThread());
95  base::FilePath subkey_path;
96  // Only read from |subkey_path| if it is not a symlink.
97  if (!VerifyKeyPathAndGetSubkeyPath(key, false, subkey, &subkey_path) ||
98      base::IsLink(subkey_path)) {
99    return false;
100  }
101  data->clear();
102  return base::ReadFileToString(subkey_path, data);
103}
104
105void ResourceCache::LoadAllSubkeys(
106    const std::string& key,
107    std::map<std::string, std::string>* contents) {
108  DCHECK(task_runner_->RunsTasksOnCurrentThread());
109  contents->clear();
110  base::FilePath key_path;
111  if (!VerifyKeyPath(key, false, &key_path))
112    return;
113
114  base::FileEnumerator enumerator(key_path, false, base::FileEnumerator::FILES);
115  for (base::FilePath path = enumerator.Next(); !path.empty();
116       path = enumerator.Next()) {
117    const std::string encoded_subkey = path.BaseName().MaybeAsASCII();
118    std::string subkey;
119    std::string data;
120    // Only read from |subkey_path| if it is not a symlink and its name is
121    // a base64-encoded string.
122    if (!base::IsLink(path) &&
123        Base64Decode(encoded_subkey, &subkey) &&
124        base::ReadFileToString(path, &data)) {
125      (*contents)[subkey].swap(data);
126    }
127  }
128}
129
130void ResourceCache::Delete(const std::string& key, const std::string& subkey) {
131  DCHECK(task_runner_->RunsTasksOnCurrentThread());
132  base::FilePath subkey_path;
133  if (VerifyKeyPathAndGetSubkeyPath(key, false, subkey, &subkey_path))
134    base::DeleteFile(subkey_path, false);
135  // Delete() does nothing if the directory given to it is not empty. Hence, the
136  // call below deletes the directory representing |key| if its last subkey was
137  // just removed and does nothing otherwise.
138  base::DeleteFile(subkey_path.DirName(), false);
139}
140
141void ResourceCache::Clear(const std::string& key) {
142  DCHECK(task_runner_->RunsTasksOnCurrentThread());
143  base::FilePath key_path;
144  if (VerifyKeyPath(key, false, &key_path))
145    base::DeleteFile(key_path, true);
146}
147
148void ResourceCache::FilterSubkeys(const std::string& key,
149                                  const SubkeyFilter& test) {
150  DCHECK(task_runner_->RunsTasksOnCurrentThread());
151
152  base::FilePath key_path;
153  if (!VerifyKeyPath(key, false, &key_path))
154    return;
155
156  base::FileEnumerator enumerator(key_path, false, base::FileEnumerator::FILES);
157  for (base::FilePath subkey_path = enumerator.Next();
158       !subkey_path.empty(); subkey_path = enumerator.Next()) {
159    std::string subkey;
160    // Delete files with invalid names, and files whose subkey doesn't pass the
161    // filter.
162    if (!Base64Decode(subkey_path.BaseName().MaybeAsASCII(), &subkey) ||
163        test.Run(subkey)) {
164      base::DeleteFile(subkey_path, true);
165    }
166  }
167
168  // Delete() does nothing if the directory given to it is not empty. Hence, the
169  // call below deletes the directory representing |key| if all of its subkeys
170  // were just removed and does nothing otherwise.
171  base::DeleteFile(key_path, false);
172}
173
174void ResourceCache::PurgeOtherKeys(const std::set<std::string>& keys_to_keep) {
175  DCHECK(task_runner_->RunsTasksOnCurrentThread());
176  std::set<std::string> encoded_keys_to_keep;
177  if (!Base64Encode(keys_to_keep, &encoded_keys_to_keep))
178    return;
179
180  base::FileEnumerator enumerator(
181      cache_dir_, false, base::FileEnumerator::DIRECTORIES);
182  for (base::FilePath path = enumerator.Next(); !path.empty();
183       path = enumerator.Next()) {
184    const std::string name(path.BaseName().MaybeAsASCII());
185    if (encoded_keys_to_keep.find(name) == encoded_keys_to_keep.end())
186      base::DeleteFile(path, true);
187  }
188}
189
190void ResourceCache::PurgeOtherSubkeys(
191    const std::string& key,
192    const std::set<std::string>& subkeys_to_keep) {
193  DCHECK(task_runner_->RunsTasksOnCurrentThread());
194  base::FilePath key_path;
195  if (!VerifyKeyPath(key, false, &key_path))
196    return;
197
198  std::set<std::string> encoded_subkeys_to_keep;
199  if (!Base64Encode(subkeys_to_keep, &encoded_subkeys_to_keep))
200    return;
201
202  base::FileEnumerator enumerator(key_path, false, base::FileEnumerator::FILES);
203  for (base::FilePath path = enumerator.Next(); !path.empty();
204       path = enumerator.Next()) {
205    const std::string name(path.BaseName().MaybeAsASCII());
206    if (encoded_subkeys_to_keep.find(name) == encoded_subkeys_to_keep.end())
207      base::DeleteFile(path, false);
208  }
209  // Delete() does nothing if the directory given to it is not empty. Hence, the
210  // call below deletes the directory representing |key| if all of its subkeys
211  // were just removed and does nothing otherwise.
212  base::DeleteFile(key_path, false);
213}
214
215bool ResourceCache::VerifyKeyPath(const std::string& key,
216                                  bool allow_create,
217                                  base::FilePath* path) {
218  std::string encoded;
219  if (!Base64Encode(key, &encoded))
220    return false;
221  *path = cache_dir_.AppendASCII(encoded);
222  return allow_create ? base::CreateDirectory(*path) :
223                        base::DirectoryExists(*path);
224}
225
226bool ResourceCache::VerifyKeyPathAndGetSubkeyPath(const std::string& key,
227                                                  bool allow_create_key,
228                                                  const std::string& subkey,
229                                                  base::FilePath* path) {
230  base::FilePath key_path;
231  std::string encoded;
232  if (!VerifyKeyPath(key, allow_create_key, &key_path) ||
233      !Base64Encode(subkey, &encoded)) {
234    return false;
235  }
236  *path = key_path.AppendASCII(encoded);
237  return true;
238}
239
240
241}  // namespace policy
242