1 /*
2 Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License, version 2.0,
6 as published by the Free Software Foundation.
7
8 This program is also distributed with certain software (including
9 but not limited to OpenSSL) that is licensed under separate terms,
10 as designated in a particular file or component or in included license
11 documentation. The authors of MySQL hereby grant you an additional
12 permission to link the program and your derivative works with the
13 separately licensed software that they have included with MySQL.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 */
24
25 #include "keyring/keyring_manager.h"
26
27 #ifndef _WIN32
28 #include <sys/stat.h>
29 #include <unistd.h>
30 #else
31 #include <windows.h>
32 #endif
33
34 #include <fstream>
35 #include <set>
36 #include <stdexcept>
37
38 #include "common.h"
39 #include "dim.h"
40 #include "gtest/gtest.h"
41 #include "keyring/keyring_memory.h"
42 #include "random_generator.h"
43 #include "test/helpers.h"
44
45 using namespace testing;
46
47 class TemporaryFileCleaner {
48 public:
~TemporaryFileCleaner()49 ~TemporaryFileCleaner() {
50 if (!getenv("TEST_DONT_DELETE_FILES")) {
51 for (auto path : tmp_files_) {
52 #ifndef _WIN32
53 ::unlink(path.c_str());
54 #else
55 DeleteFile(path.c_str()) ? 0 : -1;
56 #endif
57 }
58 }
59 }
60
add(const std::string & path)61 const std::string &add(const std::string &path) {
62 tmp_files_.insert(path);
63 return path;
64 }
65
66 private:
67 std::set<std::string> tmp_files_;
68 };
69
70 #ifdef _WIN32
71 // Copied from keyring_file.cc
72
73 // Smart pointers for WinAPI structures that use C-style memory management.
74 using SecurityDescriptorPtr =
75 std::unique_ptr<SECURITY_DESCRIPTOR,
76 mysql_harness::StdFreeDeleter<SECURITY_DESCRIPTOR>>;
77 using SidPtr = std::unique_ptr<SID, mysql_harness::StdFreeDeleter<SID>>;
78
79 /**
80 * Retrieves file's DACL security descriptor.
81 *
82 * @param[in] file_name File name.
83 *
84 * @return File's DACL security descriptor.
85 *
86 * @throw std::exception Failed to retrieve security descriptor.
87 */
get_security_descriptor(const std::string & file_name)88 static SecurityDescriptorPtr get_security_descriptor(
89 const std::string &file_name) {
90 static constexpr SECURITY_INFORMATION kReqInfo = DACL_SECURITY_INFORMATION;
91
92 // Get the size of the descriptor.
93 DWORD sec_desc_size;
94
95 if (GetFileSecurityA(file_name.c_str(), kReqInfo, nullptr, 0,
96 &sec_desc_size) == FALSE) {
97 auto error = GetLastError();
98
99 // We expect to receive `ERROR_INSUFFICIENT_BUFFER`.
100 if (error != ERROR_INSUFFICIENT_BUFFER) {
101 throw std::runtime_error("GetFileSecurity() failed (" + file_name +
102 "): " + std::to_string(error));
103 }
104 }
105
106 SecurityDescriptorPtr sec_desc(
107 static_cast<SECURITY_DESCRIPTOR *>(std::malloc(sec_desc_size)));
108
109 if (GetFileSecurityA(file_name.c_str(), kReqInfo, sec_desc.get(),
110 sec_desc_size, &sec_desc_size) == FALSE) {
111 throw std::runtime_error("GetFileSecurity() failed (" + file_name +
112 "): " + std::to_string(GetLastError()));
113 }
114
115 return sec_desc;
116 }
117
118 /**
119 * Verifies permissions of an access ACE entry.
120 *
121 * @param[in] access_ace Access ACE entry.
122 *
123 * @throw std::exception Everyone has access to the ACE access entry or
124 * an error occurred.
125 */
check_ace_access_rights(ACCESS_ALLOWED_ACE * access_ace)126 static void check_ace_access_rights(ACCESS_ALLOWED_ACE *access_ace) {
127 SID *sid = reinterpret_cast<SID *>(&access_ace->SidStart);
128 DWORD sid_size = SECURITY_MAX_SID_SIZE;
129 SidPtr everyone_sid(static_cast<SID *>(std::malloc(sid_size)));
130
131 if (CreateWellKnownSid(WinWorldSid, nullptr, everyone_sid.get(), &sid_size) ==
132 FALSE) {
133 throw std::runtime_error("CreateWellKnownSid() failed: " +
134 std::to_string(GetLastError()));
135 }
136
137 if (EqualSid(sid, everyone_sid.get())) {
138 if (access_ace->Mask & (FILE_EXECUTE)) {
139 throw std::runtime_error(
140 "Invalid keyring file access rights "
141 "(Execute privilege granted to Everyone).");
142 }
143 if (access_ace->Mask &
144 (FILE_WRITE_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) {
145 throw std::runtime_error(
146 "Invalid keyring file access rights "
147 "(Write privilege granted to Everyone).");
148 }
149 if (access_ace->Mask &
150 (FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES)) {
151 throw std::runtime_error(
152 "Invalid keyring file access rights "
153 "(Read privilege granted to Everyone).");
154 }
155 }
156 }
157
158 /**
159 * Verifies access permissions in a DACL.
160 *
161 * @param[in] dacl DACL to be verified.
162 *
163 * @throw std::exception DACL contains an ACL entry that grants full access to
164 * Everyone or an error occurred.
165 */
check_acl_access_rights(ACL * dacl)166 static void check_acl_access_rights(ACL *dacl) {
167 ACL_SIZE_INFORMATION dacl_size_info;
168
169 if (GetAclInformation(dacl, &dacl_size_info, sizeof(dacl_size_info),
170 AclSizeInformation) == FALSE) {
171 throw std::runtime_error("GetAclInformation() failed: " +
172 std::to_string(GetLastError()));
173 }
174
175 for (DWORD ace_idx = 0; ace_idx < dacl_size_info.AceCount; ++ace_idx) {
176 LPVOID ace = nullptr;
177
178 if (GetAce(dacl, ace_idx, &ace) == FALSE) {
179 throw std::runtime_error("GetAce() failed: " +
180 std::to_string(GetLastError()));
181 continue;
182 }
183
184 if (static_cast<ACE_HEADER *>(ace)->AceType == ACCESS_ALLOWED_ACE_TYPE)
185 check_ace_access_rights(static_cast<ACCESS_ALLOWED_ACE *>(ace));
186 }
187 }
188
189 /**
190 * Verifies access permissions in a security descriptor.
191 *
192 * @param[in] sec_desc Security descriptor to be verified.
193 *
194 * @throw std::exception Security descriptor grants full access to
195 * Everyone or an error occurred.
196 */
check_security_descriptor_access_rights(SecurityDescriptorPtr sec_desc)197 static void check_security_descriptor_access_rights(
198 SecurityDescriptorPtr sec_desc) {
199 BOOL dacl_present;
200 ACL *dacl;
201 BOOL dacl_defaulted;
202
203 if (GetSecurityDescriptorDacl(sec_desc.get(), &dacl_present, &dacl,
204 &dacl_defaulted) == FALSE) {
205 throw std::runtime_error("GetSecurityDescriptorDacl() failed: " +
206 std::to_string(GetLastError()));
207 }
208
209 if (!dacl_present) {
210 // No DACL means: no access allowed. Which is fine.
211 return;
212 }
213
214 if (!dacl) {
215 // Empty DACL means: all access allowed.
216 throw std::runtime_error(
217 "Invalid keyring file access rights "
218 "(Everyone has full access rights).");
219 }
220
221 check_acl_access_rights(dacl);
222 }
223 #endif // _WIN32
224
check_file_private(const std::string & file)225 static bool check_file_private(const std::string &file) {
226 #ifdef _WIN32
227 try {
228 check_security_descriptor_access_rights(get_security_descriptor(file));
229 return true;
230 } catch (...) {
231 return false;
232 }
233 #else
234 struct stat st;
235 if (stat(file.c_str(), &st) < 0) {
236 throw std::runtime_error(file + ": " + strerror(errno));
237 }
238 if ((st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == (S_IRUSR | S_IWUSR))
239 return true;
240 return false;
241 #endif
242 }
243
244 class FileChangeChecker {
245 public:
FileChangeChecker(const std::string & file)246 FileChangeChecker(const std::string &file) : path_(file) {
247 std::ifstream f;
248 std::stringstream ss;
249 f.open(file, std::ifstream::binary);
250 if (f.fail())
251 throw std::runtime_error(file + " " + mysql_harness::get_strerror(errno));
252 ss << f.rdbuf();
253 contents_ = ss.str();
254 f.close();
255 }
256
check_unchanged()257 bool check_unchanged() {
258 std::ifstream f;
259 std::stringstream ss;
260 f.open(path_, std::ifstream::binary);
261 if (f.fail())
262 throw std::runtime_error(path_ + " " +
263 mysql_harness::get_strerror(errno));
264 ss << f.rdbuf();
265 f.close();
266 return ss.str() == contents_;
267 }
268
269 private:
270 std::string path_;
271 std::string contents_;
272 };
273
file_exists(const std::string & file)274 static bool file_exists(const std::string &file) {
275 return mysql_harness::Path(file).exists();
276 }
277
278 TmpDir tmp_dir;
279
TEST(KeyringManager,init_tests)280 TEST(KeyringManager, init_tests) {
281 mysql_harness::DIM::instance().set_RandomGenerator(
282 []() {
283 static mysql_harness::FakeRandomGenerator rg;
284 return &rg;
285 },
286 [](mysql_harness::RandomGeneratorInterface *) {}
287 // don't delete our static!
288 );
289 }
290
TEST(KeyringManager,init_with_key)291 TEST(KeyringManager, init_with_key) {
292 TemporaryFileCleaner cleaner;
293
294 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
295 mysql_harness::init_keyring_with_key(cleaner.add(tmp_dir.file("keyring")),
296 "secret", true);
297 {
298 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
299 EXPECT_NE(kr, nullptr);
300
301 kr->store("foo", "bar", "baz");
302 mysql_harness::flush_keyring();
303 EXPECT_TRUE(check_file_private(tmp_dir.file("keyring")));
304
305 // this key will not be saved to disk b/c of missing flush
306 kr->store("account", "password", "");
307 EXPECT_EQ(kr->fetch("foo", "bar"), "baz");
308
309 EXPECT_EQ(kr->fetch("account", "password"), "");
310 }
311 mysql_harness::reset_keyring();
312 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
313
314 EXPECT_FALSE(file_exists(tmp_dir.file("badkeyring")));
315 ASSERT_THROW(mysql_harness::init_keyring_with_key(tmp_dir.file("badkeyring"),
316 "secret", false),
317 std::runtime_error);
318 EXPECT_FALSE(file_exists(tmp_dir.file("badkeyring")));
319
320 #ifndef _WIN32
321 ASSERT_THROW(
322 mysql_harness::init_keyring_with_key("/badkeyring", "secret", false),
323 std::runtime_error);
324 EXPECT_FALSE(file_exists("/badkeyring"));
325
326 ASSERT_THROW(
327 mysql_harness::init_keyring_with_key("/badkeyring", "secret", true),
328 std::runtime_error);
329 EXPECT_FALSE(file_exists("/badkeyring"));
330 #endif
331
332 ASSERT_THROW(mysql_harness::init_keyring_with_key(tmp_dir.file("keyring"),
333 "badkey", false),
334 mysql_harness::decryption_error);
335
336 ASSERT_THROW(
337 mysql_harness::init_keyring_with_key(tmp_dir.file("keyring"), "", false),
338 mysql_harness::decryption_error);
339
340 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
341
342 mysql_harness::init_keyring_with_key(tmp_dir.file("keyring"), "secret",
343 false);
344 {
345 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
346
347 EXPECT_EQ(kr->fetch("foo", "bar"), "baz");
348 ASSERT_THROW(kr->fetch("account", "password"), std::out_of_range);
349 }
350
351 mysql_harness::reset_keyring();
352 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
353 // no key no service
354 ASSERT_THROW(mysql_harness::init_keyring_with_key(
355 cleaner.add(tmp_dir.file("xkeyring")), "", true),
356 std::runtime_error);
357 EXPECT_FALSE(file_exists(tmp_dir.file("xkeyring")));
358
359 // try to open non-existing keyring
360 ASSERT_THROW(
361 mysql_harness::init_keyring_with_key(
362 cleaner.add(tmp_dir.file("invalidkeyring")), "secret", false),
363 std::runtime_error);
364 EXPECT_FALSE(file_exists(tmp_dir.file("invalidkeyring")));
365
366 // check if keyring is created even if empty
367 mysql_harness::init_keyring_with_key(
368 cleaner.add(tmp_dir.file("emptykeyring")), "secret2", true);
369 EXPECT_TRUE(file_exists(tmp_dir.file("emptykeyring")));
370 mysql_harness::reset_keyring();
371 }
372
TEST(KeyringManager,init_with_key_file)373 TEST(KeyringManager, init_with_key_file) {
374 TemporaryFileCleaner cleaner;
375
376 EXPECT_FALSE(file_exists(tmp_dir.file("keyring")));
377 EXPECT_FALSE(file_exists(tmp_dir.file("keyfile")));
378
379 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
380 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("keyring")),
381 cleaner.add(tmp_dir.file("keyfile")), true);
382 EXPECT_TRUE(file_exists(tmp_dir.file("keyring")));
383 EXPECT_TRUE(file_exists(tmp_dir.file("keyfile")));
384 {
385 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
386 EXPECT_NE(kr, nullptr);
387
388 kr->store("foo", "bar", "baz");
389 mysql_harness::flush_keyring();
390 EXPECT_TRUE(check_file_private(tmp_dir.file("keyring")));
391 EXPECT_TRUE(check_file_private(tmp_dir.file("keyfile")));
392
393 // this key will not be saved to disk b/c of missing flush
394 kr->store("account", "password", "");
395 EXPECT_EQ(kr->fetch("foo", "bar"), "baz");
396
397 EXPECT_EQ(kr->fetch("account", "password"), "");
398 }
399 mysql_harness::reset_keyring();
400 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
401
402 FileChangeChecker check_kf(tmp_dir.file("keyfile"));
403 FileChangeChecker check_kr(tmp_dir.file("keyring"));
404
405 EXPECT_FALSE(file_exists(tmp_dir.file("badkeyring")));
406 EXPECT_TRUE(file_exists(tmp_dir.file("keyfile")));
407 ASSERT_THROW(mysql_harness::init_keyring(tmp_dir.file("badkeyring"),
408 tmp_dir.file("keyfile"), false),
409 std::runtime_error);
410 EXPECT_FALSE(file_exists(tmp_dir.file("badkeyring")));
411
412 #ifndef _WIN32
413 ASSERT_THROW(mysql_harness::init_keyring("/badkeyring",
414 tmp_dir.file("keyfile"), false),
415 std::runtime_error);
416 EXPECT_FALSE(file_exists("/badkeyring"));
417
418 ASSERT_THROW(
419 mysql_harness::init_keyring("/badkeyring", tmp_dir.file("keyfile"), true),
420 std::runtime_error);
421 EXPECT_FALSE(file_exists("/badkeyring"));
422 EXPECT_TRUE(check_kf.check_unchanged());
423
424 ASSERT_THROW(
425 mysql_harness::init_keyring(tmp_dir.file("keyring"), "/keyfile", false),
426 std::runtime_error);
427 EXPECT_FALSE(file_exists("/keyfile"));
428
429 ASSERT_THROW(mysql_harness::init_keyring("/keyring", "/keyfile", false),
430 std::runtime_error);
431 EXPECT_FALSE(file_exists("/keyring"));
432 EXPECT_FALSE(file_exists("/keyfile"));
433 #endif
434 ASSERT_THROW(mysql_harness::init_keyring(tmp_dir.file("keyring"), "", false),
435 std::invalid_argument);
436
437 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
438
439 // ensure that none of the tests above touched the keyring files
440 EXPECT_TRUE(check_kf.check_unchanged());
441 EXPECT_TRUE(check_kr.check_unchanged());
442
443 EXPECT_TRUE(file_exists(tmp_dir.file("keyring")));
444 EXPECT_TRUE(file_exists(tmp_dir.file("keyfile")));
445 // reopen it
446 mysql_harness::init_keyring(tmp_dir.file("keyring"), tmp_dir.file("keyfile"),
447 false);
448 {
449 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
450
451 EXPECT_EQ(kr->fetch("foo", "bar"), "baz");
452
453 ASSERT_THROW(kr->fetch("account", "password"), std::out_of_range);
454 }
455 mysql_harness::reset_keyring();
456 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
457
458 // try to reopen keyring with bad key file
459 ASSERT_THROW(mysql_harness::init_keyring(tmp_dir.file("keyring"),
460 tmp_dir.file("badkeyfile"), false),
461 std::runtime_error);
462
463 // try to reopen bad keyring with right keyfile
464 ASSERT_THROW(mysql_harness::init_keyring(tmp_dir.file("badkeyring"),
465 tmp_dir.file("keyfile"), false),
466 std::runtime_error);
467
468 ASSERT_THROW(mysql_harness::init_keyring(tmp_dir.file("badkeyring"),
469 tmp_dir.file("badkeyfile"), false),
470 std::runtime_error);
471 EXPECT_TRUE(mysql_harness::get_keyring() == nullptr);
472
473 // ensure that none of the tests above touched the keyring files
474 EXPECT_TRUE(check_kf.check_unchanged());
475 EXPECT_TRUE(check_kr.check_unchanged());
476
477 // create a new keyring reusing the same keyfile, which should result in
478 // 2 master keys stored in the same keyfile
479 EXPECT_FALSE(file_exists(tmp_dir.file("keyring2")));
480 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("keyring2")),
481 cleaner.add(tmp_dir.file("keyfile")), true);
482 EXPECT_TRUE(file_exists(tmp_dir.file("keyring2")));
483 {
484 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
485 EXPECT_NE(kr, nullptr);
486
487 kr->store("user", "pass", "hooray");
488 mysql_harness::flush_keyring();
489 EXPECT_TRUE(check_file_private(tmp_dir.file("keyring2")));
490
491 mysql_harness::flush_keyring();
492 EXPECT_TRUE(file_exists(tmp_dir.file("keyring2")));
493 }
494 mysql_harness::reset_keyring();
495
496 // the orignal keyring should still be unchanged, but not the keyfile
497 bool b1 = check_kf.check_unchanged();
498 bool b2 = check_kr.check_unchanged();
499 EXPECT_FALSE(b1);
500 EXPECT_TRUE(b2);
501
502 // now try to reopen both keyrings
503 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("keyring2")),
504 cleaner.add(tmp_dir.file("keyfile")), false);
505 {
506 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
507 EXPECT_EQ(kr->fetch("user", "pass"), "hooray");
508 }
509 mysql_harness::reset_keyring();
510
511 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("keyring")),
512 cleaner.add(tmp_dir.file("keyfile")), false);
513 {
514 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
515 EXPECT_EQ(kr->fetch("foo", "bar"), "baz");
516 }
517 mysql_harness::reset_keyring();
518
519 // now try to open with bogus key file
520 ASSERT_THROW(
521 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("keyring")),
522 cleaner.add(tmp_dir.file("keyring2")), false),
523 std::runtime_error);
524 }
525
TEST(KeyringManager,regression)526 TEST(KeyringManager, regression) {
527 TemporaryFileCleaner cleaner;
528
529 // init keyring with no create flag was writing to existing file on open
530 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("keyring")),
531 cleaner.add(tmp_dir.file("keyfile")), true);
532 mysql_harness::Keyring *kr = mysql_harness::get_keyring();
533 kr->store("1", "2", "3");
534 mysql_harness::flush_keyring();
535 mysql_harness::reset_keyring();
536
537 FileChangeChecker check_kf(tmp_dir.file("keyfile"));
538 FileChangeChecker check_kr(tmp_dir.file("keyring"));
539
540 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("keyring")),
541 cleaner.add(tmp_dir.file("keyfile")), false);
542 EXPECT_TRUE(check_kf.check_unchanged());
543 EXPECT_TRUE(check_kr.check_unchanged());
544
545 ASSERT_THROW(
546 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("bogus1")),
547 cleaner.add(tmp_dir.file("bogus2")), false),
548 std::runtime_error);
549 ASSERT_THROW(
550 mysql_harness::init_keyring(cleaner.add(tmp_dir.file("bogus1")),
551 cleaner.add(tmp_dir.file("keyfile")), false),
552 std::runtime_error);
553 EXPECT_FALSE(file_exists(tmp_dir.file("bogus1")));
554 EXPECT_FALSE(file_exists(tmp_dir.file("bogus2")));
555
556 EXPECT_TRUE(check_kf.check_unchanged());
557 EXPECT_TRUE(check_kr.check_unchanged());
558
559 mysql_harness::reset_keyring();
560 }
561
main(int argc,char ** argv)562 int main(int argc, char **argv) {
563 ::testing::InitGoogleTest(&argc, argv);
564 return RUN_ALL_TESTS();
565 }
566