diff --git a/src/cpp-utils/io/Console.h b/src/cpp-utils/io/Console.h index 8330ee72..7dd322df 100644 --- a/src/cpp-utils/io/Console.h +++ b/src/cpp-utils/io/Console.h @@ -13,10 +13,11 @@ namespace cpputils { class Console { public: - virtual ~Console() {} + virtual ~Console() = default; virtual unsigned int ask(const std::string &question, const std::vector &options) = 0; virtual bool askYesNo(const std::string &question, bool defaultValue) = 0; // NoninteractiveConsole will just return the default value without asking the user. virtual void print(const std::string &output) = 0; + virtual std::string askPassword(const std::string &question) = 0; }; } diff --git a/src/cpp-utils/io/IOStreamConsole.cpp b/src/cpp-utils/io/IOStreamConsole.cpp index b1a9698a..3ee5f56d 100644 --- a/src/cpp-utils/io/IOStreamConsole.cpp +++ b/src/cpp-utils/io/IOStreamConsole.cpp @@ -1,5 +1,6 @@ #include "IOStreamConsole.h" #include +#include "DontEchoStdinToStdoutRAII.h" using std::ostream; using std::istream; @@ -91,8 +92,24 @@ bool IOStreamConsole::askYesNo(const string &question, bool /*defaultValue*/) { return _askForChoice("Your choice [y/n]: ", _parseYesNo()); } -void IOStreamConsole::print(const std::string &output) { +void IOStreamConsole::print(const string &output) { _output << output << std::flush; } +string IOStreamConsole::askPassword(const string &question) { + DontEchoStdinToStdoutRAII _stdin_input_is_hidden_as_long_as_this_is_in_scope; + + _output << question << std::flush; + string result; + std::getline(_input, result); + _output << std::endl; + + //Remove trailing newline + if (result[result.size()-1] == '\n') { + result.resize(result.size()-1); + } + + return result; +} + } diff --git a/src/cpp-utils/io/IOStreamConsole.h b/src/cpp-utils/io/IOStreamConsole.h index 513d3308..61d72f7e 100644 --- a/src/cpp-utils/io/IOStreamConsole.h +++ b/src/cpp-utils/io/IOStreamConsole.h @@ -13,6 +13,7 @@ namespace cpputils { unsigned int ask(const std::string &question, const std::vector &options) override; bool askYesNo(const std::string &question, bool defaultValue) override; void print(const std::string &output) override; + std::string askPassword(const std::string &question) override; private: template Return _askForChoice(const std::string &question, std::function (const std::string&)> parse); diff --git a/src/cpp-utils/io/NoninteractiveConsole.cpp b/src/cpp-utils/io/NoninteractiveConsole.cpp index 5c458b12..e81ba94e 100644 --- a/src/cpp-utils/io/NoninteractiveConsole.cpp +++ b/src/cpp-utils/io/NoninteractiveConsole.cpp @@ -21,4 +21,8 @@ unsigned int NoninteractiveConsole::ask(const string &/*question*/, const vector throw std::logic_error("Tried to ask a multiple choice question in noninteractive mode"); } +string NoninteractiveConsole::askPassword(const string &question) { + return _baseConsole->askPassword(question); +} + } diff --git a/src/cpp-utils/io/NoninteractiveConsole.h b/src/cpp-utils/io/NoninteractiveConsole.h index fc6bf880..3fc1f72c 100644 --- a/src/cpp-utils/io/NoninteractiveConsole.h +++ b/src/cpp-utils/io/NoninteractiveConsole.h @@ -14,6 +14,7 @@ namespace cpputils { unsigned int ask(const std::string &question, const std::vector &options) override; bool askYesNo(const std::string &question, bool defaultValue) override; void print(const std::string &output) override; + std::string askPassword(const std::string &question) override; private: std::shared_ptr _baseConsole; diff --git a/src/cryfs-cli/Cli.cpp b/src/cryfs-cli/Cli.cpp index e6089b66..93e2aa3b 100644 --- a/src/cryfs-cli/Cli.cpp +++ b/src/cryfs-cli/Cli.cpp @@ -124,34 +124,38 @@ namespace cryfs { return true; } - string Cli::_askPasswordForExistingFilesystem() { - string password = _askPasswordFromStdin("Password: "); - while (!_checkPassword(password)) { - password = _askPasswordFromStdin("Password: "); - } - return password; + function Cli::_askPasswordForExistingFilesystem(std::shared_ptr console) { + return [console] () { + string password = console->askPassword("Password: "); + while (!_checkPassword(password)) { + password = console->askPassword("Password: "); + } + return password; + }; }; - string Cli::_askPasswordForNewFilesystem() { - string password; - bool again = false; - do { - password = _askPasswordFromStdin("Password: "); - if (!_checkPassword(password)) { - again = true; - continue; - } - if (!_confirmPassword(password)) { - again = true; - continue; - } - again = false; - } while(again); - return password; + function Cli::_askPasswordForNewFilesystem(std::shared_ptr console) { + return [console] () { + string password; + bool again = false; + do { + password = console->askPassword("Password: "); + if (!_checkPassword(password)) { + again = true; + continue; + } + if (!_confirmPassword(console.get(), password)) { + again = true; + continue; + } + again = false; + } while (again); + return password; + }; } - bool Cli::_confirmPassword(const string &password) { - string confirmPassword = _askPasswordFromStdin("Confirm Password: "); + bool Cli::_confirmPassword(cpputils::Console* console, const string &password) { + string confirmPassword = console->askPassword("Confirm Password: "); if (password != confirmPassword) { std::cout << "Passwords don't match" << std::endl; return false; @@ -159,29 +163,15 @@ namespace cryfs { return true; } - string Cli::_askPasswordNoninteractive() { + function Cli::_askPasswordNoninteractive(std::shared_ptr console) { //TODO Test - string password = _askPasswordFromStdin("Password: "); - if (!_checkPassword(password)) { - throw CryfsException("Invalid password. Password cannot be empty.", ErrorCode::EmptyPassword); - } - return password; - } - - string Cli::_askPasswordFromStdin(const string &prompt) { - DontEchoStdinToStdoutRAII _stdin_input_is_hidden_as_long_as_this_is_in_scope; - - std::cout << prompt << std::flush; - string result; - std::getline(cin, result); - std::cout << std::endl; - - //Remove trailing newline - if (result[result.size()-1] == '\n') { - result.resize(result.size()-1); - } - - return result; + return [console] () { + string password = console->askPassword("Password: "); + if (!_checkPassword(password)) { + throw CryfsException("Invalid password. Password cannot be empty.", ErrorCode::EmptyPassword); + } + return password; + }; } bf::path Cli::_determineConfigFile(const ProgramOptions &options) { @@ -218,13 +208,13 @@ namespace cryfs { optional Cli::_loadOrCreateConfigFile(bf::path configFilePath, LocalStateDir localStateDir, const optional &cipher, const optional &blocksizeBytes, bool allowFilesystemUpgrade, const optional &missingBlockIsIntegrityViolation, bool allowReplacedFilesystem) { if (_noninteractive) { return CryConfigLoader(_console, _keyGenerator, std::move(localStateDir), _scryptSettings, - &Cli::_askPasswordNoninteractive, - &Cli::_askPasswordNoninteractive, + Cli::_askPasswordNoninteractive(_console), + Cli::_askPasswordNoninteractive(_console), cipher, blocksizeBytes, missingBlockIsIntegrityViolation).loadOrCreate(std::move(configFilePath), allowFilesystemUpgrade, allowReplacedFilesystem); } else { return CryConfigLoader(_console, _keyGenerator, std::move(localStateDir), _scryptSettings, - &Cli::_askPasswordForExistingFilesystem, - &Cli::_askPasswordForNewFilesystem, + Cli::_askPasswordForExistingFilesystem(_console), + Cli::_askPasswordForNewFilesystem(_console), cipher, blocksizeBytes, missingBlockIsIntegrityViolation).loadOrCreate(std::move(configFilePath), allowFilesystemUpgrade, allowReplacedFilesystem); } } diff --git a/src/cryfs-cli/Cli.h b/src/cryfs-cli/Cli.h index 290eaa23..afcc7dfd 100644 --- a/src/cryfs-cli/Cli.h +++ b/src/cryfs-cli/Cli.h @@ -27,11 +27,10 @@ namespace cryfs { void _checkConfigIntegrity(const boost::filesystem::path& basedir, const LocalStateDir& localStateDir, const CryConfigFile& config, bool allowReplacedFilesystem); boost::optional _loadOrCreateConfigFile(boost::filesystem::path configFilePath, LocalStateDir localStateDir, const boost::optional &cipher, const boost::optional &blocksizeBytes, bool allowFilesystemUpgrade, const boost::optional &missingBlockIsIntegrityViolation, bool allowReplacedFilesystem); boost::filesystem::path _determineConfigFile(const program_options::ProgramOptions &options); - static std::string _askPasswordForExistingFilesystem(); - static std::string _askPasswordForNewFilesystem(); - static std::string _askPasswordNoninteractive(); - static std::string _askPasswordFromStdin(const std::string &prompt); - static bool _confirmPassword(const std::string &password); + static std::function _askPasswordForExistingFilesystem(std::shared_ptr console); + static std::function _askPasswordForNewFilesystem(std::shared_ptr console); + static std::function _askPasswordNoninteractive(std::shared_ptr console); + static bool _confirmPassword(cpputils::Console* console, const std::string &password); static bool _checkPassword(const std::string &password); void _showVersion(cpputils::unique_ref httpClient); void _initLogfile(const program_options::ProgramOptions &options); diff --git a/test/cpp-utils/CMakeLists.txt b/test/cpp-utils/CMakeLists.txt index 68801c33..4afd0818 100644 --- a/test/cpp-utils/CMakeLists.txt +++ b/test/cpp-utils/CMakeLists.txt @@ -27,6 +27,7 @@ set(SOURCES io/ConsoleTest_AskYesNo.cpp io/ConsoleTest_Print.cpp io/ConsoleTest_Ask.cpp + io/ConsoleTest_AskPassword.cpp random/RandomIncludeTest.cpp lock/LockPoolIncludeTest.cpp lock/ConditionBarrierIncludeTest.cpp diff --git a/test/cpp-utils/io/ConsoleTest.h b/test/cpp-utils/io/ConsoleTest.h index d05c1031..2ba8f40a 100644 --- a/test/cpp-utils/io/ConsoleTest.h +++ b/test/cpp-utils/io/ConsoleTest.h @@ -23,6 +23,11 @@ public: return _console.askYesNo(question, true); }); } + std::future askPassword(const std::string &question) { + return std::async(std::launch::async, [this, question]() { + return _console.askPassword(question); + }); + } void print(const std::string &output) { _console.print(output); } @@ -63,6 +68,10 @@ public: return _console.askYesNo(question); } + std::future askPassword(const std::string &question) { + return _console.askPassword(question); + } + void print(const std::string &output) { _console.print(output); } diff --git a/test/cpp-utils/io/ConsoleTest_AskPassword.cpp b/test/cpp-utils/io/ConsoleTest_AskPassword.cpp new file mode 100644 index 00000000..d7db1874 --- /dev/null +++ b/test/cpp-utils/io/ConsoleTest_AskPassword.cpp @@ -0,0 +1,22 @@ +#include "ConsoleTest.h" + +using std::stringstream; +using std::string; +using std::istream; +using std::ostream; + +class ConsoleTest_AskPassword: public ConsoleTest {}; + +TEST_F(ConsoleTest_AskPassword, InputSomePassword) { + auto chosen = askPassword("Please enter my password:"); + EXPECT_OUTPUT_LINE("Please enter my password", ':'); + sendInputLine("this is the password"); + EXPECT_EQ("this is the password", chosen.get()); +} + +TEST_F(ConsoleTest_AskPassword, InputEmptyPassword) { + auto chosen = askPassword("Please enter my password:"); + EXPECT_OUTPUT_LINE("Please enter my password", ':'); + sendInputLine(""); + EXPECT_EQ("", chosen.get()); +} diff --git a/test/cryfs-cli/testutils/CliTest.h b/test/cryfs-cli/testutils/CliTest.h index 719d65e6..9aafc1bb 100644 --- a/test/cryfs-cli/testutils/CliTest.h +++ b/test/cryfs-cli/testutils/CliTest.h @@ -42,9 +42,8 @@ public: _args.push_back(arg); } auto &keyGenerator = cpputils::Random::PseudoRandom(); - // Write 2x 'pass\n' to stdin so Cryfs can read it as password (+ password confirmation prompt) - std::cin.putback('\n'); std::cin.putback('s'); std::cin.putback('s'); std::cin.putback('a'); std::cin.putback('p'); - std::cin.putback('\n'); std::cin.putback('s'); std::cin.putback('s'); std::cin.putback('a'); std::cin.putback('p'); + ON_CALL(*console, askPassword(testing::StrEq("Password: "))).WillByDefault(testing::Return("pass")); + ON_CALL(*console, askPassword(testing::StrEq("Confirm Password: "))).WillByDefault(testing::Return("pass")); // Run Cryfs return cryfs::Cli(keyGenerator, cpputils::SCrypt::TestSettings, console).main(_args.size(), _args.data(), _httpClient()); } diff --git a/test/cryfs/testutils/MockConsole.h b/test/cryfs/testutils/MockConsole.h index 420503be..5e4c67c5 100644 --- a/test/cryfs/testutils/MockConsole.h +++ b/test/cryfs/testutils/MockConsole.h @@ -10,6 +10,7 @@ public: MOCK_METHOD1(print, void(const std::string&)); MOCK_METHOD2(ask, unsigned int(const std::string&, const std::vector&)); MOCK_METHOD2(askYesNo, bool(const std::string&, bool)); + MOCK_METHOD1(askPassword, std::string(const std::string&)); }; ACTION_P(ChooseCipher, cipherName) {