1 /*
2   Copyright (c) 2019, 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_frontend.h"
26 
27 #include <algorithm>
28 #include <cctype>  // isprint()
29 #include <fstream>
30 #include <sstream>
31 #include <vector>
32 
33 #ifdef RAPIDJSON_NO_SIZETYPEDEFINE
34 // if we build within the server, it will set RAPIDJSON_NO_SIZETYPEDEFINE
35 // globally and require to include my_rapidjson_size_t.h
36 #include "my_rapidjson_size_t.h"
37 #endif
38 
39 #include <rapidjson/document.h>
40 #include <rapidjson/prettywriter.h>
41 
42 #include "keyring/keyring_file.h"
43 #include "keyring/master_key_file.h"
44 #include "mysql/harness/arg_handler.h"
45 #include "mysql/harness/utility/string.h"
46 #include "mysqlrouter/keyring_info.h"
47 #include "mysqlrouter/utils.h"
48 #include "print_version.h"  // build_version
49 #include "random_generator.h"
50 #include "welcome_copyright_notice.h"  // ORACLE_WELCOME_COPYRIGHT_NOTICE
51 
52 using namespace std::string_literals;
53 
54 static const unsigned kKeyLength{32};
55 
init_from_arguments(const std::vector<std::string> & arguments)56 void KeyringFrontend::init_from_arguments(
57     const std::vector<std::string> &arguments) {
58   prepare_command_options();
59   try {
60     arg_handler_.process(arguments);
61   } catch (const std::invalid_argument &e) {
62     // unknown options
63     throw UsageError(e.what());
64   }
65 }
66 
KeyringFrontend(const std::string & exe_name,const std::vector<std::string> & args,std::istream & is,std::ostream & os,std::ostream & es)67 KeyringFrontend::KeyringFrontend(const std::string &exe_name,
68                                  const std::vector<std::string> &args,
69                                  std::istream &is, std::ostream &os,
70                                  std::ostream &es)
71     : program_name_{exe_name},
72       arg_handler_{true},
73       cin_{is},
74       cout_{os},
75       cerr_{es} {
76   init_from_arguments(args);
77 }
78 
get_version()79 std::string KeyringFrontend::get_version() noexcept {
80   std::stringstream os;
81 
82   std::string version_string;
83   build_version(std::string(MYSQL_ROUTER_PACKAGE_NAME), &version_string);
84 
85   os << version_string << std::endl;
86   os << ORACLE_WELCOME_COPYRIGHT_NOTICE("2019") << std::endl;
87 
88   return os.str();
89 }
90 
get_help(const size_t screen_width) const91 std::string KeyringFrontend::get_help(const size_t screen_width) const {
92   std::stringstream os;
93 
94   os << "Usage" << std::endl;
95   os << std::endl;
96 
97   os << mysql_harness::join(
98             mysql_harness::utility::wrap_string(
99                 program_name_ + " " + "[opts] <cmd> <filename> [<username>]",
100                 screen_width, 2),
101             "\n")
102      << std::endl;
103   os << mysql_harness::join(
104             mysql_harness::utility::wrap_string(program_name_ + " " + "--help",
105                                                 screen_width, 2),
106             "\n")
107      << std::endl;
108   os << mysql_harness::join(
109             mysql_harness::utility::wrap_string(
110                 program_name_ + " " + "--version", screen_width, 2),
111             "\n")
112      << std::endl;
113 
114   os << std::endl;
115   os << "Commands" << std::endl;
116   os << std::endl;
117 
118   std::vector<std::pair<std::string, std::string>> cmd_help{
119       {"init", "initialize a keyring."},
120       {"set", "add or overwrite account of <username> in <filename>."},
121       {"delete", "delete entry from keyring."},
122       {"list", "list all entries in keyring."},
123       {"export", "export all entries of keyring as JSON."},
124       {"get", "field from keyring entry"},
125       {"master-delete", "keyring from master-keyfile"},
126       {"master-list", "list entries from master-keyfile"},
127       {"master-rename", "renames and entry in a master-keyfile"},
128   };
129 
130   for (const auto &kv : cmd_help) {
131     os << "  " << kv.first << std::endl
132        << mysql_harness::join(
133               mysql_harness::utility::wrap_string(kv.second, screen_width, 6),
134               "\n")
135        << std::endl;
136   }
137 
138   os << std::endl;
139 
140   os << "Options" << std::endl;
141 
142   os << std::endl;
143 
144   for (const auto &line : arg_handler_.option_descriptions(screen_width, 6)) {
145     os << line << std::endl;
146   }
147 
148   return os.str();
149 }
150 
151 /**
152  * load the masterkeyfile.
153  *
154  * @throws FrontendError on anything else.
155  * @returns success
156  * @retval true if loaded
157  * @retval false if file didn't exist
158  */
master_key_file_load(mysql_harness::MasterKeyFile & mkf)159 static bool master_key_file_load(mysql_harness::MasterKeyFile &mkf) {
160   try {
161     mkf.load();
162   } catch (const std::system_error &e) {
163     // if it doesn't exist, we'll later create it
164     if (e.code() != std::errc::no_such_file_or_directory) {
165       throw FrontendError("opening master-key-file failed: "s + e.what());
166     }
167 
168     return false;
169   } catch (const std::exception &e) {
170     throw FrontendError("opening master-key-file failed: "s + e.what());
171   }
172 
173   return true;
174 }
175 
176 /**
177  * prepare the keyring.
178  *
179  * if keyring doesn't exist, generates a new random
180  *
181  * @throws FrontendError on anything else
182  * @returns if keyring changed and the keyrings random
183  */
keyring_file_prepare(mysql_harness::KeyringFile & kf,const std::string & keyring_filename)184 static std::pair<bool, std::string> keyring_file_prepare(
185     mysql_harness::KeyringFile &kf, const std::string &keyring_filename) {
186   if (keyring_filename.empty()) {
187     throw UsageError("expected <keyring> to be not empty");
188   }
189 
190   try {
191     return {false, kf.read_header(keyring_filename)};
192   } catch (const std::system_error &e) {
193     // if it doesn't exist, we'll later create it
194     if (e.code() != std::errc::no_such_file_or_directory) {
195       throw FrontendError(e.what());
196     }
197 
198     mysql_harness::RandomGenerator rg;
199     std::string kf_random = rg.generate_strong_password(kKeyLength);
200     kf.set_header(kf_random);
201 
202     return {true, kf_random};
203   } catch (const std::exception &e) {
204     throw FrontendError(e.what());
205   }
206 }
207 
keyring_file_load(mysql_harness::KeyringFile & kf,const std::string & keyring_filename,const std::string & kf_key)208 static bool keyring_file_load(mysql_harness::KeyringFile &kf,
209                               const std::string &keyring_filename,
210                               const std::string &kf_key) {
211   if (keyring_filename.empty()) {
212     throw UsageError("expected <keyring> to be not empty");
213   }
214 
215   // load it, if it exists to be able to later save it with the same content
216   try {
217     kf.load(keyring_filename, kf_key);
218 
219     return true;
220   } catch (const std::system_error &e) {
221     if (e.code() != std::errc::no_such_file_or_directory) {
222       throw FrontendError(e.what());
223     }
224 
225     return false;
226   } catch (const std::exception &e) {
227     throw FrontendError("loading failed?"s + e.what());
228   }
229 }
230 
231 /**
232  * prepare master-key-file for keyring.
233  *
234  * if keyring-file isn't known in master-key-file:
235  *
236  * - generates encryption-key for keyring
237  * - adds keyring to master-key-file
238  *
239  * otherwise, gets encryption-key from master-key-file
240  *
241  * @returns if-master-key-file-changed and the encryption-key for the keyring
242  */
master_key_file_prepare(mysql_harness::MasterKeyFile & mkf,mysql_harness::KeyringFile & kf,const std::string & keyring_filename,const std::string & kf_random)243 static std::pair<bool, std::string> master_key_file_prepare(
244     mysql_harness::MasterKeyFile &mkf, mysql_harness::KeyringFile &kf,
245     const std::string &keyring_filename, const std::string &kf_random) {
246   std::string kf_key;
247   try {
248     kf_key = mkf.get(keyring_filename, kf_random);
249     if (kf_key.empty()) {
250       // master-keyring doesn't exist or keyfile not known in master-keyring
251       mysql_harness::RandomGenerator rg;
252       kf_key = rg.generate_strong_password(kKeyLength);
253 
254       mkf.add(keyring_filename, kf_key, kf_random);
255 
256       return {true, kf_key};
257     } else {
258       keyring_file_load(kf, keyring_filename, kf_key);
259     }
260   } catch (const mysql_harness::decryption_error &) {
261     // file is known, but our key doesn't match
262     throw FrontendError(
263         "master-key-file knows key-file, but key doesn't match.");
264   }
265 
266   return {false, kf_key};
267 }
268 
cmd_init_with_master_key_file(const std::string & keyring_filename,const std::string & master_keyring_filename)269 static void cmd_init_with_master_key_file(
270     const std::string &keyring_filename,
271     const std::string &master_keyring_filename) {
272   mysql_harness::KeyringFile kf;
273   bool kf_changed{false};
274   std::string kf_random;
275   std::tie(kf_changed, kf_random) = keyring_file_prepare(kf, keyring_filename);
276 
277   if (!kf_changed) {
278     size_t file_size;
279     try {
280       std::fstream f(keyring_filename);
281       f.seekg(0, std::ios_base::end);
282       file_size = f.tellg();
283       f.close();
284     } catch (const std::exception &e) {
285       throw FrontendError(e.what());
286     }
287 
288     if (file_size > (4                   // marker
289                      + 4                 // header length
290                      + kf_random.size()  // header
291                      + 4                 // payload-signature
292                      + 4                 // payload-version
293                      + 4                 // entries
294                      + 4                 // no idea
295                      )) {
296       // keyring exists and header was parsed
297       throw FrontendError("keyfile '" + keyring_filename +
298                           "' already exists and has entries");
299     }
300   }
301 
302   mysql_harness::MasterKeyFile mkf(master_keyring_filename);
303 
304   master_key_file_load(mkf);
305 
306   bool mkf_changed{false};
307   std::string kf_key;
308   std::tie(mkf_changed, kf_key) =
309       master_key_file_prepare(mkf, kf, keyring_filename, kf_random);
310 
311   // save master-key-file first to not create a keyring entry that can't be
312   // decoded.
313   try {
314     if (mkf_changed) mkf.save();
315   } catch (const std::exception &e) {
316     throw FrontendError("failed saving master-key-file: "s + e.what());
317   }
318   try {
319     if (kf_changed) kf.save(keyring_filename, kf_key);
320   } catch (const std::exception &e) {
321     throw FrontendError("failed saving keyring: "s + e.what());
322   }
323 }
324 
master_key_reader_load(const std::string & master_key_reader,const std::string & keyring_filename)325 static std::pair<bool, std::string> master_key_reader_load(
326     const std::string &master_key_reader, const std::string &keyring_filename) {
327   try {
328     KeyringInfo kinfo;
329     kinfo.set_master_key_reader(master_key_reader);
330     if (!kinfo.read_master_key()) {
331       throw FrontendError("failed reading master-key for '" + keyring_filename +
332                           "' from master-key-reader '"s + master_key_reader +
333                           "'");
334     }
335     std::string kf_key = kinfo.get_master_key();
336     if (kf_key.empty()) {
337       // master-keyring doesn't exist or keyfile not known in master-keyring
338       mysql_harness::RandomGenerator rg;
339 
340       return {true, rg.generate_strong_password(kKeyLength)};
341     } else {
342       return {false, kf_key};
343     }
344   } catch (const FrontendError &) {
345     throw;
346   } catch (const std::exception &e) {
347     throw FrontendError(
348         "failed reading master-key from --master-key-reader: "s + e.what());
349   }
350 }
351 
cmd_init_with_master_key_reader(const std::string & keyring_filename,const std::string & master_key_reader,const std::string & master_key_writer)352 static void cmd_init_with_master_key_reader(
353     const std::string &keyring_filename, const std::string &master_key_reader,
354     const std::string &master_key_writer) {
355   mysql_harness::KeyringFile kf;
356   bool kf_changed{false};
357   std::string kf_random;
358   std::tie(kf_changed, kf_random) = keyring_file_prepare(kf, keyring_filename);
359 
360   std::string kf_key;
361   bool mk_changed{false};
362   std::tie(mk_changed, kf_key) =
363       master_key_reader_load(master_key_reader, keyring_filename);
364 
365   keyring_file_load(kf, keyring_filename, kf_key);
366 
367   if (mk_changed) {
368     KeyringInfo kinfo;
369     kinfo.set_master_key_writer(master_key_writer);
370     if (!kinfo.write_master_key()) {
371       throw FrontendError("failed writing master-key for '" + keyring_filename +
372                           "' to master-key-writer '" + master_key_writer + "'");
373     }
374   }
375   try {
376     if (kf_changed) kf.save(keyring_filename, kf_key);
377   } catch (const std::exception &e) {
378     throw FrontendError("failed saving keyfile: "s + e.what());
379   }
380 }
381 
cmd_init_with_master_key(const std::string & keyring_filename,const std::string & kf_key)382 static void cmd_init_with_master_key(const std::string &keyring_filename,
383                                      const std::string &kf_key) {
384   if (kf_key.empty()) {
385     throw FrontendError("expected master-key for '" + keyring_filename +
386                         "' to be not empty, but it is");
387   }
388 
389   bool kf_changed{false};
390   mysql_harness::KeyringFile kf;
391   std::string kf_random;
392   std::tie(kf_changed, kf_random) = keyring_file_prepare(kf, keyring_filename);
393 
394   keyring_file_load(kf, keyring_filename, kf_key);
395 
396   try {
397     if (kf_changed) kf.save(keyring_filename, kf_key);
398   } catch (const std::exception &e) {
399     throw FrontendError("failed saving keyfile: "s + e.what());
400   }
401 }
402 
cmd_master_delete(const std::string & master_keyring_filename,const std::string & keyring_filename)403 static void cmd_master_delete(const std::string &master_keyring_filename,
404                               const std::string &keyring_filename) {
405   mysql_harness::MasterKeyFile mkf(master_keyring_filename);
406   try {
407     mkf.load();
408   } catch (const std::exception &e) {
409     throw FrontendError("opening master-key-file failed: "s + e.what());
410   }
411 
412   bool mkf_changed = mkf.remove(keyring_filename);
413 
414   if (mkf_changed) {
415     mkf.save();
416   } else {
417     throw FrontendError("Keyring '" + keyring_filename +
418                         "' not found in master-key-file '" +
419                         master_keyring_filename + "'");
420   }
421 }
422 
cmd_master_list(std::ostream & cout,const std::string & master_keyring_filename)423 static void cmd_master_list(std::ostream &cout,
424                             const std::string &master_keyring_filename) {
425   mysql_harness::MasterKeyFile mkf(master_keyring_filename);
426   try {
427     mkf.load();
428   } catch (const std::exception &e) {
429     throw FrontendError("opening master-key-file failed: "s + e.what());
430   }
431 
432   for (auto it = mkf.entries().begin(); it != mkf.entries().end(); ++it) {
433     cout << it->first << std::endl;
434   }
435 }
436 
cmd_master_rename(const std::string & master_keyring_filename,const std::string & old_key,const std::string & new_key)437 static void cmd_master_rename(const std::string &master_keyring_filename,
438                               const std::string &old_key,
439                               const std::string &new_key) {
440   mysql_harness::MasterKeyFile mkf(master_keyring_filename);
441   try {
442     mkf.load();
443   } catch (const std::exception &e) {
444     throw FrontendError("opening master-key-file failed: "s + e.what());
445   }
446 
447   try {
448     mkf.add_encrypted(new_key, mkf.get_encrypted(old_key));
449   } catch (const std::out_of_range &) {
450     throw FrontendError("old-key '" + old_key +
451                         "' not found in master-key-file '" +
452                         master_keyring_filename + "'");
453   } catch (const std::invalid_argument &) {
454     throw FrontendError("new-key '" + new_key +
455                         "' already exists in master-key-file '" +
456                         master_keyring_filename + "'");
457   }
458   mkf.remove(old_key);
459 
460   mkf.save();
461 }
462 
cmd_export(std::ostream & os,const mysql_harness::KeyringFile & kf)463 static void cmd_export(std::ostream &os, const mysql_harness::KeyringFile &kf) {
464   rapidjson::StringBuffer json_buf;
465   rapidjson::PrettyWriter<rapidjson::StringBuffer> json_writer(json_buf);
466   rapidjson::Document json_doc;
467   rapidjson::Document::AllocatorType &allocator = json_doc.GetAllocator();
468   json_doc.SetObject();
469   for (auto const &entry : kf.entries()) {
470     rapidjson::Value el;
471 
472     el.SetObject();
473 
474     for (auto const &kv : entry.second) {
475       el.AddMember(
476           rapidjson::Value(kv.first.c_str(), kv.first.size(), allocator),
477           rapidjson::Value(kv.second.c_str(), kv.second.size(), allocator),
478           allocator);
479     }
480     json_doc.AddMember(
481         rapidjson::Value(entry.first.c_str(), entry.first.size(), allocator),
482         el.Move(), allocator);
483 
484   }  // free json_doc and json_writer early
485   json_doc.Accept(json_writer);
486 
487   os << json_buf.GetString() << std::endl;
488 }
489 
cmd_list(std::ostream & os,mysql_harness::KeyringFile & kf,const std::string & username)490 static bool cmd_list(std::ostream &os, mysql_harness::KeyringFile &kf,
491                      const std::string &username) {
492   bool found{false};
493   if (username.empty()) {
494     found = true;
495     for (auto const &entry : kf.entries()) {
496       os << entry.first << std::endl;
497     }
498   } else {
499     for (auto const &entry : kf.entries()) {
500       if (entry.first == username) {
501         found = true;
502         for (auto const &kv : entry.second) {
503           os << kv.first << std::endl;
504         }
505       }
506     }
507   }
508   return found;
509 }
510 
cmd_delete(mysql_harness::KeyringFile & kf,const std::string & username,const std::string & field)511 static bool cmd_delete(mysql_harness::KeyringFile &kf,
512                        const std::string &username, const std::string &field) {
513   if (field.empty()) {
514     return kf.remove(username);
515   } else {
516     return kf.remove_attribute(username, field);
517   }
518 }
519 
cmd_set(mysql_harness::KeyringFile & kf,const std::string & username,const std::string & field,const std::string & value)520 static void cmd_set(mysql_harness::KeyringFile &kf, const std::string &username,
521                     const std::string &field, const std::string &value) {
522   kf.store(username, field, value);
523 }
524 
cmd_get(std::ostream & os,mysql_harness::KeyringFile & kf,const std::string & username,const std::string & field)525 static void cmd_get(std::ostream &os, mysql_harness::KeyringFile &kf,
526                     const std::string &username, const std::string &field) {
527   try {
528     os << kf.fetch(username, field) << std::endl;
529   } catch (const std::out_of_range &) {
530     throw FrontendError("'" + field + "' not found for user '" + username +
531                         "'");
532   }
533 }
534 
535 /**
536  * prepare arguments and cmds.
537  *
538  * - check command name
539  * - check argument counts
540  *
541  * @throws UsageError on error
542  */
prepare_args()543 void KeyringFrontend::prepare_args() {
544   // handle positional args
545   const auto &rest_args = arg_handler_.get_rest_arguments();
546   auto rest_args_count = rest_args.size();
547 
548   if (config_.cmd != Cmd::ShowHelp && config_.cmd != Cmd::ShowVersion) {
549     if (rest_args_count == 0) {
550       throw UsageError("expected a <cmd>");
551     }
552 
553     std::map<std::string, KeyringFrontend::Cmd> cmds{
554         {"init", KeyringFrontend::Cmd::Init},
555         {"set", KeyringFrontend::Cmd::Set},
556         {"delete", KeyringFrontend::Cmd::Delete},
557         {"list", KeyringFrontend::Cmd::List},
558         {"export", KeyringFrontend::Cmd::Export},
559         {"get", KeyringFrontend::Cmd::Get},
560         {"master-delete", KeyringFrontend::Cmd::MasterDelete},
561         {"master-list", KeyringFrontend::Cmd::MasterList},
562         {"master-rename", KeyringFrontend::Cmd::MasterRename},
563     };
564 
565     const auto cmd = cmds.find(rest_args[0]);
566     if (cmd == cmds.end()) {
567       throw UsageError("unknown command: " + rest_args[0]);
568     }
569 
570     config_.cmd = cmd->second;
571 
572     --rest_args_count;
573   }
574 
575   switch (config_.cmd) {
576     case Cmd::MasterDelete:
577     case Cmd::Init:
578     case Cmd::Export:
579       if (rest_args_count != 1) {
580         throw UsageError("expected one argument <filename>, got " +
581                          std::to_string(rest_args_count) + " arguments");
582       }
583       config_.keyring_filename = rest_args[1];
584       break;
585     case Cmd::MasterList:
586     case Cmd::ShowVersion:
587     case Cmd::ShowHelp:
588       if (rest_args_count != 0) {
589         throw UsageError("expected no extra arguments, got " +
590                          std::to_string(rest_args_count) + " arguments");
591       }
592       break;
593     case Cmd::List:
594       if (rest_args_count < 1 || rest_args_count > 2) {
595         throw UsageError("expected <filename> and optionally <username>, got " +
596                          std::to_string(rest_args_count) + " arguments");
597       }
598 
599       config_.keyring_filename = rest_args[1];
600       if (rest_args_count > 1) config_.username = rest_args[2];
601       break;
602     case Cmd::Get:
603       if (rest_args_count != 3) {
604         throw UsageError("expected <filename> <username> <key>, got " +
605                          std::to_string(rest_args_count) + " arguments");
606       }
607 
608       config_.keyring_filename = rest_args[1];
609       config_.username = rest_args[2];
610       config_.field = rest_args[3];
611       break;
612     case Cmd::Set:
613       if (rest_args_count != 4 && rest_args_count != 3) {
614         throw UsageError(
615             "expected <filename> <username> <key>, optionally <value>, got " +
616             std::to_string(rest_args_count) + " arguments");
617       }
618 
619       config_.keyring_filename = rest_args[1];
620       config_.username = rest_args[2];
621       config_.field = rest_args[3];
622       if (rest_args_count == 4) {
623         config_.value = rest_args[4];
624       } else {
625         config_.value =
626             mysqlrouter::prompt_password("value for " + config_.field);
627       }
628       break;
629     case Cmd::Delete:
630       if (rest_args_count != 2 && rest_args_count != 3) {
631         throw UsageError(
632             "expected <filename> <username>, and optionally <key>, got " +
633             std::to_string(rest_args_count) + " arguments");
634       }
635 
636       config_.keyring_filename = rest_args[1];
637       config_.username = rest_args[2];
638       if (rest_args_count == 3) config_.field = rest_args[3];
639       break;
640     case Cmd::MasterRename:
641       if (rest_args_count != 2) {
642         throw UsageError("expected 2 arguments <old-key> <new-key>, got " +
643                          std::to_string(rest_args_count) + " arguments");
644       }
645 
646       config_.keyring_filename = rest_args[1];
647       config_.username = rest_args[2];
648       break;
649   }
650 }
651 
run()652 int KeyringFrontend::run() {
653   prepare_args();
654 
655   switch (config_.cmd) {
656     case Cmd::ShowHelp:
657       cout_ << get_help() << std::endl;
658       return EXIT_SUCCESS;
659     case Cmd::ShowVersion:
660       cout_ << get_version() << std::endl;
661       return EXIT_SUCCESS;
662     default:
663       break;
664   }
665 
666   if (!config_.master_keyring_filename.empty() &&
667       !config_.master_key_reader.empty()) {
668     throw UsageError(
669         "--master-key-file and --master-key-reader can't be used together");
670   }
671 
672   if (!config_.master_keyring_filename.empty() &&
673       !config_.master_key_writer.empty()) {
674     throw UsageError(
675         "--master-key-file and --master-key-writer can't be used together");
676   }
677 
678   switch (config_.cmd) {
679     case Cmd::Init:
680       if (!config_.master_keyring_filename.empty()) {
681         cmd_init_with_master_key_file(config_.keyring_filename,
682                                       config_.master_keyring_filename);
683 
684       } else if (!config_.master_key_reader.empty() ||
685                  !config_.master_key_writer.empty()) {
686         cmd_init_with_master_key_reader(config_.keyring_filename,
687                                         config_.master_key_reader,
688                                         config_.master_key_writer);
689 
690       } else {
691         cmd_init_with_master_key(
692             config_.keyring_filename,
693             mysqlrouter::prompt_password("Please enter master key"));
694       }
695 
696       return EXIT_SUCCESS;
697     case Cmd::MasterDelete:
698       if (config_.master_keyring_filename.empty()) {
699         throw UsageError("expected --master-key-file to be not empty");
700       }
701 
702       cmd_master_delete(config_.master_keyring_filename,
703                         config_.keyring_filename);
704 
705       return EXIT_SUCCESS;
706     case Cmd::MasterList:
707       if (config_.master_keyring_filename.empty()) {
708         throw UsageError("expected --master-key-file to be not empty");
709       }
710 
711       cmd_master_list(cout_, config_.master_keyring_filename);
712       return EXIT_SUCCESS;
713     case Cmd::MasterRename:
714       // master-rename uses the 'config_' struct slightly differently then
715       // the other commands:
716       //
717       // * config_.keyring_filename -> <old_key>
718       // * config_.username         -> <new_key>
719       //
720       if (config_.master_keyring_filename.empty()) {
721         throw UsageError("expected --master-key-file to be not empty");
722       }
723       if (config_.keyring_filename.empty()) {
724         throw UsageError("expected <old-key> to be not empty");
725       }
726 
727       if (config_.username.empty()) {
728         throw UsageError("expected <new-key> to be not empty");
729       }
730 
731       for (const char c : config_.keyring_filename) {
732         if (!std::isprint(c)) {
733           throw UsageError(
734               "expected <old-key> to contain only printable characters");
735         }
736       }
737 
738       for (const char c : config_.username) {
739         if (!std::isprint(c)) {
740           throw UsageError(
741               "expected <new-key> to contain only printable characters");
742         }
743       }
744       cmd_master_rename(config_.master_keyring_filename,
745                         config_.keyring_filename, config_.username);
746       return EXIT_SUCCESS;
747     default:
748       break;
749   }
750 
751   // Cmd::Init, Cmd::Master* are already handled.
752   //
753   // All other commands require a key from
754   // - the master-key-ring,
755   // - stdin
756   // - master-key-reader
757 
758   std::string kf_key;
759   mysql_harness::KeyringFile kf;
760 
761   if (!config_.master_keyring_filename.empty()) {
762     std::string kf_random;
763     try {
764       kf_random = kf.read_header(config_.keyring_filename);
765     } catch (const std::exception &e) {
766       throw FrontendError("opening keyring failed: "s + e.what());
767     }
768     mysql_harness::MasterKeyFile mkf(config_.master_keyring_filename);
769     try {
770       mkf.load();
771       kf_key = mkf.get(config_.keyring_filename, kf_random);
772     } catch (const std::exception &e) {
773       throw FrontendError("opening master-key-file failed: "s + e.what());
774     }
775 
776     if (kf_key.empty()) {
777       throw FrontendError("couldn't find master key for " +
778                           config_.keyring_filename + " in master-key-file " +
779                           config_.master_keyring_filename);
780     }
781   } else if (!config_.master_key_reader.empty()) {
782     KeyringInfo kinfo;
783     kinfo.set_master_key_reader(config_.master_key_reader);
784     if (!kinfo.read_master_key()) {
785       throw FrontendError(
786           "failed reading master-key for '" + config_.keyring_filename +
787           "' from master-key-reader '" + config_.master_key_reader + "'");
788     }
789     kf_key = kinfo.get_master_key();
790   } else {
791     kf_key = mysqlrouter::prompt_password("Please enter master key");
792   }
793 
794   if (kf_key.empty()) {
795     throw FrontendError("expected master-key for '" + config_.keyring_filename +
796                         "' to be not empty, but it is");
797   }
798 
799   // load keyring
800   keyring_file_load(kf, config_.keyring_filename, kf_key);
801 
802   bool kf_changed = false;
803   switch (config_.cmd) {
804     case Cmd::ShowHelp:
805     case Cmd::ShowVersion:
806     case Cmd::Init:
807     case Cmd::MasterList:
808     case Cmd::MasterDelete:
809     case Cmd::MasterRename:
810       // unreachable
811       abort();
812       break;
813     case Cmd::Get:
814       cmd_get(cout_, kf, config_.username, config_.field);
815       break;
816     case Cmd::Set:
817       cmd_set(kf, config_.username, config_.field, config_.value);
818       kf_changed = true;
819 
820       break;
821     case Cmd::Delete: {
822       if (!cmd_delete(kf, config_.username, config_.field)) {
823         return EXIT_FAILURE;
824       }
825       kf_changed = true;
826       break;
827     }
828     case Cmd::Export:
829       cmd_export(cout_, kf);
830       break;
831     case Cmd::List:
832       if (!cmd_list(cout_, kf, config_.username)) {
833         return EXIT_FAILURE;
834       }
835       break;
836   }
837 
838   if (kf_changed) {
839     kf.save(config_.keyring_filename, kf_key);
840   }
841 
842   return EXIT_SUCCESS;
843 }
844 
prepare_command_options()845 void KeyringFrontend::prepare_command_options() {
846   // prepare default-kdf-name and the list of supported names
847   arg_handler_.add_option(
848       CmdOption::OptionNames({"-?", "--help"}), "Display this help and exit.",
849       CmdOptionValueReq::none, "", [this](const std::string &) {
850         if (config_.cmd != Cmd::ShowVersion) {
851           config_.cmd = Cmd::ShowHelp;
852         }
853       });
854   arg_handler_.add_option(
855       //
856       CmdOption::OptionNames({"-V", "--version"}),
857       "Display version information and exit.", CmdOptionValueReq::none, "",
858       [this](const std::string &) {
859         if (config_.cmd != Cmd::ShowHelp) {
860           config_.cmd = Cmd::ShowVersion;
861         }
862       });
863   arg_handler_.add_option(
864       CmdOption::OptionNames({"--master-key-file"}),
865       "Filename of the master keyfile.", CmdOptionValueReq::required, "",
866       [this](const std::string &v) {
867         if (v.empty()) {
868           throw UsageError("expected --master-key-file to be not empty.");
869         }
870         config_.master_keyring_filename = v;
871       });
872   arg_handler_.add_option(
873       CmdOption::OptionNames({"--master-key-reader"}),
874       "Executable which provides the master key for the keyfile.",
875       CmdOptionValueReq::required, "", [this](const std::string &v) {
876         if (v.empty()) {
877           throw UsageError("expected --master-key-reader to be not empty.");
878         }
879         config_.master_key_reader = v;
880       });
881   arg_handler_.add_option(
882       CmdOption::OptionNames({"--master-key-writer"}),
883       "Executable which can store the master key for the keyfile.",
884       CmdOptionValueReq::required, "", [this](const std::string &v) {
885         if (v.empty()) {
886           throw UsageError("expected --master-key-writer to be not empty.");
887         }
888         config_.master_key_writer = v;
889       });
890 }
891