1 // Copyright 2014 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 "extensions/browser/computed_hashes.h"
6 #include "base/base64.h"
7 #include "base/files/file_path.h"
8 #include "base/files/scoped_temp_dir.h"
9 #include "base/strings/stringprintf.h"
10 #include "build/build_config.h"
11 #include "crypto/sha2.h"
12 #include "extensions/browser/content_verifier/content_verifier_utils.h"
13 #include "extensions/common/constants.h"
14 #include "testing/gtest/include/gtest/gtest.h"
15
16 namespace {
17
18 constexpr bool kIsDotSpaceSuffixIgnored =
19 extensions::content_verifier_utils::IsDotSpaceFilenameSuffixIgnored();
20 constexpr bool kIsFileAccessCaseInsensitive =
21 !extensions::content_verifier_utils::IsFileAccessCaseSensitive();
22
23 // Helper to return base64 encode result by value.
Base64Encode(const std::string & data)24 std::string Base64Encode(const std::string& data) {
25 std::string result;
26 base::Base64Encode(data, &result);
27 return result;
28 }
29
30 struct HashInfo {
31 base::FilePath path;
32 int block_size;
33 std::vector<std::string> hashes;
34 };
35
WriteThenReadComputedHashes(const std::vector<HashInfo> & hash_infos,extensions::ComputedHashes * result)36 testing::AssertionResult WriteThenReadComputedHashes(
37 const std::vector<HashInfo>& hash_infos,
38 extensions::ComputedHashes* result) {
39 base::ScopedTempDir scoped_dir;
40 if (!scoped_dir.CreateUniqueTempDir())
41 return testing::AssertionFailure() << "Failed to create temp dir.";
42
43 base::FilePath computed_hashes_path =
44 scoped_dir.GetPath().AppendASCII("computed_hashes.json");
45 extensions::ComputedHashes::Data computed_hashes_data;
46 for (const auto& info : hash_infos)
47 computed_hashes_data.Add(info.path, info.block_size, info.hashes);
48
49 if (!extensions::ComputedHashes(std::move(computed_hashes_data))
50 .WriteToFile(computed_hashes_path)) {
51 return testing::AssertionFailure()
52 << "Failed to write computed_hashes.json";
53 }
54 extensions::ComputedHashes::Status computed_hashes_status;
55 base::Optional<extensions::ComputedHashes> computed_hashes =
56 extensions::ComputedHashes::CreateFromFile(computed_hashes_path,
57 &computed_hashes_status);
58 if (!computed_hashes)
59 return testing::AssertionFailure()
60 << "Failed to read computed_hashes.json (status: "
61 << static_cast<int>(computed_hashes_status) << ")";
62 *result = std::move(computed_hashes.value());
63
64 return testing::AssertionSuccess();
65 }
66
67 } // namespace
68
69 namespace extensions {
70
TEST(ComputedHashesTest,ComputedHashes)71 TEST(ComputedHashesTest, ComputedHashes) {
72 // We'll add hashes for 2 files, one of which uses a subdirectory
73 // path. The first file will have a list of 1 block hash, and the
74 // second file will have 2 block hashes.
75 base::FilePath path1(FILE_PATH_LITERAL("foo.txt"));
76 base::FilePath path2 =
77 base::FilePath(FILE_PATH_LITERAL("foo")).AppendASCII("bar.txt");
78 std::vector<std::string> hashes1 = {crypto::SHA256HashString("first")};
79 std::vector<std::string> hashes2 = {crypto::SHA256HashString("second"),
80 crypto::SHA256HashString("third")};
81 const int kBlockSize1 = 4096;
82 const int kBlockSize2 = 2048;
83
84 ComputedHashes computed_hashes{ComputedHashes::Data()};
85 ASSERT_TRUE(WriteThenReadComputedHashes(
86 {{path1, kBlockSize1, hashes1}, {path2, kBlockSize2, hashes2}},
87 &computed_hashes));
88
89 // After reading hashes back assert that we got what we wrote.
90 std::vector<std::string> read_hashes1;
91 std::vector<std::string> read_hashes2;
92
93 int block_size = 0;
94 EXPECT_TRUE(computed_hashes.GetHashes(path1, &block_size, &read_hashes1));
95 EXPECT_EQ(block_size, 4096);
96 block_size = 0;
97 EXPECT_TRUE(computed_hashes.GetHashes(path2, &block_size, &read_hashes2));
98 EXPECT_EQ(block_size, 2048);
99
100 EXPECT_EQ(hashes1, read_hashes1);
101 EXPECT_EQ(hashes2, read_hashes2);
102
103 // Make sure we can lookup hashes for a file using incorrect case
104 base::FilePath path1_badcase(FILE_PATH_LITERAL("FoO.txt"));
105 std::vector<std::string> read_hashes1_badcase;
106 EXPECT_EQ(kIsFileAccessCaseInsensitive,
107 computed_hashes.GetHashes(path1_badcase, &block_size,
108 &read_hashes1_badcase));
109 if (kIsFileAccessCaseInsensitive) {
110 EXPECT_EQ(4096, block_size);
111 EXPECT_EQ(hashes1, read_hashes1_badcase);
112 }
113
114 // Finally make sure that we can retrieve the hashes for the subdir
115 // path even when that path contains forward slashes (on windows).
116 base::FilePath path2_fwd_slashes =
117 base::FilePath::FromUTF8Unsafe("foo/bar.txt");
118 block_size = 0;
119 EXPECT_TRUE(
120 computed_hashes.GetHashes(path2_fwd_slashes, &block_size, &read_hashes2));
121 EXPECT_EQ(hashes2, read_hashes2);
122 }
123
124 // Note: the expected hashes used in this test were generated using linux
125 // command line tools. E.g., from a bash prompt:
126 // $ printf "hello world" | openssl dgst -sha256 -binary | base64
127 //
128 // The file with multiple-blocks expectations were generated by doing:
129 // $ for i in `seq 500 ; do printf "hello world" ; done > hello.txt
130 // $ dd if=hello.txt bs=4096 count=1 | openssl dgst -sha256 -binary | base64
131 // $ dd if=hello.txt skip=1 bs=4096 count=1 |
132 // openssl dgst -sha256 -binary | base64
TEST(ComputedHashesTest,GetHashesForContent)133 TEST(ComputedHashesTest, GetHashesForContent) {
134 const int block_size = 4096;
135
136 // Simple short input.
137 std::string content1 = "hello world";
138 std::string content1_expected_hash =
139 "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=";
140 std::vector<std::string> hashes1 =
141 ComputedHashes::GetHashesForContent(content1, block_size);
142 ASSERT_EQ(1u, hashes1.size());
143 EXPECT_EQ(content1_expected_hash, Base64Encode(hashes1[0]));
144
145 // Multiple blocks input.
146 std::string content2;
147 for (int i = 0; i < 500; i++)
148 content2 += "hello world";
149 const char* content2_expected_hashes[] = {
150 "bvtt5hXo8xvHrlzGAhhoqPL/r+4zJXHx+6wAvkv15V8=",
151 "lTD45F7P6I/HOdi8u7FLRA4qzAYL+7xSNVeusG6MJI0="};
152 std::vector<std::string> hashes2 =
153 ComputedHashes::GetHashesForContent(content2, block_size);
154 ASSERT_EQ(2u, hashes2.size());
155 EXPECT_EQ(content2_expected_hashes[0], Base64Encode(hashes2[0]));
156 EXPECT_EQ(content2_expected_hashes[1], Base64Encode(hashes2[1]));
157
158 // Now an empty input.
159 std::string content3;
160 std::vector<std::string> hashes3 =
161 ComputedHashes::GetHashesForContent(content3, block_size);
162 ASSERT_EQ(1u, hashes3.size());
163 ASSERT_EQ(std::string("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="),
164 Base64Encode(hashes3[0]));
165 }
166
167 // Tests that dot/space path suffixes are treated correctly in
168 // ComputedHashes::InitFromFile.
169 //
170 // Regression test for https://crbug.com/696208.
TEST(ComputedHashesTest,DotSpaceSuffix)171 TEST(ComputedHashesTest, DotSpaceSuffix) {
172 const std::string hash_value = crypto::SHA256HashString("test");
173 ComputedHashes computed_hashes{ComputedHashes::Data()};
174 // Add hashes for "foo.html" to computed_hashes.json.
175 ASSERT_TRUE(WriteThenReadComputedHashes(
176 {
177 {base::FilePath(FILE_PATH_LITERAL("foo.html")),
178 extension_misc::kContentVerificationDefaultBlockSize,
179 {hash_value}},
180 },
181 &computed_hashes));
182 std::vector<std::string> read_hashes;
183
184 struct TestCase {
185 const char* path;
186 bool expect_hash;
187
188 std::string ToString() const {
189 return base::StringPrintf("path = %s, expect_hash = %d", path,
190 expect_hash);
191 }
192 } test_cases[] = {
193 // Sanity check: existing file.
194 {"foo.html", true},
195 // Sanity check: non existent file.
196 {"notfound.html", false},
197 // Path with "." suffix, along with incorrect case for the same.
198 {"foo.html.", kIsDotSpaceSuffixIgnored},
199 {"fOo.html.", kIsDotSpaceSuffixIgnored},
200 // Path with " " suffix, along with incorrect case for the same.
201 {"foo.html ", kIsDotSpaceSuffixIgnored},
202 {"fOo.html ", kIsDotSpaceSuffixIgnored},
203 // Path with ". " suffix, along with incorrect case for the same.
204 {"foo.html. ", kIsDotSpaceSuffixIgnored},
205 {"fOo.html. ", kIsDotSpaceSuffixIgnored},
206 // Path with " ." suffix, along with incorrect case for the same.
207 {"foo.html .", kIsDotSpaceSuffixIgnored},
208 {"fOo.html .", kIsDotSpaceSuffixIgnored},
209 };
210
211 for (const auto& test_case : test_cases) {
212 SCOPED_TRACE(test_case.ToString());
213 int block_size = 0;
214 std::vector<std::string> read_hashes;
215 EXPECT_EQ(
216 test_case.expect_hash,
217 computed_hashes.GetHashes(base::FilePath().AppendASCII(test_case.path),
218 &block_size, &read_hashes));
219
220 if (test_case.expect_hash) {
221 EXPECT_EQ(block_size,
222 extension_misc::kContentVerificationDefaultBlockSize);
223 ASSERT_EQ(1u, read_hashes.size());
224 EXPECT_EQ(hash_value, read_hashes[0]);
225 }
226 }
227 }
228
229 } // namespace extensions
230