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